namespace RolemasterDb.ImportTool.Parsing;

public sealed class GroupedVariantCriticalTableParser
{
    private static readonly ParsedCriticalGroup[] ExpectedGroups =
    [
        new("large", "Large Creatures", 1),
        new("super_large", "Super Large Creatures", 2)
    ];

    private static readonly ParsedCriticalColumn[] ExpectedColumns =
    [
        new("NORMAL", "Normal", "variant", 1),
        new("SLAYING", "Slaying", "variant", 2)
    ];

    public CriticalTableParseResult Parse(CriticalImportManifestEntry entry, ExtractedCriticalSource source)
    {
        var fragments = source.Fragments;
        var pageGeometries = source.PageGeometries;
        var groupHeaders = FindGroupHeaders(fragments);
        var columnHeaders = FindColumnHeaders(fragments);
        var validationErrors = new List<string>();
        var validationWarnings = new List<string>();

        var combinedColumnAnchors = columnHeaders
            .OrderBy(item => item.Left)
            .Select((item, index) =>
            {
                var group = ExpectedGroups[index / ExpectedColumns.Length];
                var column = ExpectedColumns[index % ExpectedColumns.Length];
                return (group.GroupKey, column.ColumnKey, CompositeKey: $"{group.GroupKey}:{column.ColumnKey}", item.CenterX);
            })
            .ToList();

        var headerTop = Math.Max(
            groupHeaders.Max(item => item.Top),
            columnHeaders.Max(item => item.Top));
        var keyTop = CriticalTableParserSupport.FindKeyTop(fragments);
        var affixLegend = CriticalTableParserSupport.ParseAffixLegend(fragments, keyTop);
        var affixLegendSymbols = affixLegend.ClassificationSymbols;
        var leftCutoff = columnHeaders.Min(item => item.Left) - 10;
        var rowLabelFragments = CriticalTableParserSupport.FindRowLabelFragments(
            fragments,
            leftCutoff,
            headerTop + CriticalTableParserSupport.HeaderToRowLabelMinimumGap,
            keyTop);
        var rowAnchors = CriticalTableParserSupport.CreateRowAnchors(rowLabelFragments);
        var bodyStartTop = CriticalTableParserSupport.ResolveBodyStartTop(headerTop, rowAnchors);

        if (rowAnchors.Count == 0)
        {
            validationErrors.Add("No roll-band labels were found in the source artifact.");
        }

        var columnCenters = combinedColumnAnchors
            .Select(item => (item.CompositeKey, item.CenterX))
            .ToList();

        var excludedFragments = groupHeaders.Concat(columnHeaders).ToList();
        var bodyFragments = CriticalTableParserSupport.BuildBodyFragments(
            fragments,
            bodyStartTop,
            keyTop,
            leftCutoff,
            rowAnchors,
            excludedFragments,
            columnCenters,
            affixLegendSymbols);
        var bodyLines = CriticalTableParserSupport.BuildBodyLines(bodyFragments, columnCenters, affixLegendSymbols);

        var parsedRollBands = rowAnchors
            .Select(anchor => CriticalTableParserSupport.CreateRollBand(anchor.Label, anchor.SortOrder))
            .ToList();

        var cellEntries = new List<ColumnarCellEntry>();

        for (var rowIndex = 0; rowIndex < rowAnchors.Count; rowIndex++)
        {
            var rowStart = rowIndex == 0
                ? bodyStartTop
                : CriticalTableParserSupport.ResolveRowBoundaryTop(rowAnchors[rowIndex - 1], rowAnchors[rowIndex], bodyLines);

            var rowEnd = rowIndex == rowAnchors.Count - 1
                ? keyTop - 1
                : CriticalTableParserSupport.ResolveRowBoundaryTop(rowAnchors[rowIndex], rowAnchors[rowIndex + 1], bodyLines);

            var rowFragments = bodyFragments
                .Where(item => item.Top >= rowStart && item.Top < rowEnd)
                .ToList();

            foreach (var anchor in combinedColumnAnchors)
            {
                var cellFragments = rowFragments
                    .Where(item => CriticalTableParserSupport.ResolveColumn(item.CenterX, columnCenters) == anchor.CompositeKey)
                    .OrderBy(item => item.Top)
                    .ThenBy(item => item.Left)
                    .ToList();

                if (cellFragments.Count == 0)
                {
                    validationErrors.Add($"Missing content for roll band '{rowAnchors[rowIndex].Label}', group '{anchor.GroupKey}', column '{anchor.ColumnKey}'.");
                    continue;
                }

                cellEntries.Add(new ColumnarCellEntry(
                    anchor.GroupKey,
                    rowAnchors[rowIndex].Label,
                    rowIndex,
                    anchor.ColumnKey,
                    CriticalTableParserSupport.BuildLines(cellFragments).ToList()));
            }
        }

        CriticalTableParserSupport.RepairLeadingAffixLeakage(cellEntries, affixLegendSymbols);

        var parsedCells = new List<ParsedCriticalCellArtifact>();
        var parsedResults = new List<ParsedCriticalResult>();
        CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegend, parsedCells, parsedResults, validationErrors);

        var expectedCellCount = rowAnchors.Count * ExpectedGroups.Length * ExpectedColumns.Length;
        if (parsedCells.Count != expectedCellCount)
        {
            validationErrors.Add($"Expected {expectedCellCount} parsed cells but produced {parsedCells.Count}.");
        }

        var validationReport = new ImportValidationReport(
            validationErrors.Count == 0,
            validationErrors,
            validationWarnings,
            rowAnchors.Count,
            parsedCells.Count);

        var table = new ParsedCriticalTable(
            entry.Slug,
            entry.DisplayName,
            entry.Family,
            Path.GetFileName(entry.PdfPath),
            source.ImportNotes,
            ExpectedGroups,
            ExpectedColumns,
            parsedRollBands,
            parsedResults);

        return new CriticalTableParseResult(table, pageGeometries, fragments, source.RenderProfile, parsedCells, validationReport);
    }

    public CriticalTableParseResult Parse(CriticalImportManifestEntry entry, string xmlContent)
    {
        return Parse(
            entry,
            new ExtractedCriticalSource(
                "xml",
                "Imported from PDF XML extraction.",
                SourceRenderProfile.XmlAligned(),
                CriticalTableParserSupport.LoadPageGeometries(xmlContent),
                CriticalTableParserSupport.LoadFragments(xmlContent)));
    }

    private static List<PositionedTextFragment> FindGroupHeaders(IReadOnlyList<PositionedTextFragment> fragments)
    {
        var expectedLabels = ExpectedGroups.Select(item => item.Label).ToList();
        var headerCandidates = fragments
            .Where(item => expectedLabels.Contains(item.Text.Trim(), StringComparer.OrdinalIgnoreCase))
            .OrderBy(item => item.Top)
            .ThenBy(item => item.Left)
            .ToList();

        foreach (var group in CriticalTableParserSupport.GroupByTop(headerCandidates))
        {
            var ordered = group.OrderBy(item => item.Left).ToList();
            var labels = ordered.Select(item => item.Text.Trim()).ToList();
            if (labels.SequenceEqual(expectedLabels, StringComparer.OrdinalIgnoreCase))
            {
                return ordered;
            }
        }

        throw new InvalidOperationException("Could not find the grouped-variant section headers in the source artifact.");
    }

    private static List<PositionedTextFragment> FindColumnHeaders(IReadOnlyList<PositionedTextFragment> fragments)
    {
        var expectedLabels = new[] { "normal", "slaying", "normal", "slaying" };
        var headerCandidates = fragments
            .Where(item =>
            {
                var normalized = item.Text.Trim().ToLowerInvariant();
                return normalized is "normal" or "slaying";
            })
            .OrderBy(item => item.Top)
            .ThenBy(item => item.Left)
            .ToList();

        foreach (var group in CriticalTableParserSupport.GroupByTop(headerCandidates))
        {
            var ordered = group.OrderBy(item => item.Left).ToList();
            var labels = ordered.Select(item => item.Text.Trim().ToLowerInvariant()).ToList();
            if (labels.SequenceEqual(expectedLabels))
            {
                return ordered;
            }
        }

        throw new InvalidOperationException("Could not find the grouped-variant column header row in the source artifact.");
    }
}
