namespace RolemasterDb.ImportTool.Parsing;

internal sealed class StandardOcrBootstrapper
{
    private const int AnchorConfidenceWarningThreshold = 85;
    private const int HeaderTopTolerance = 12;

    public StandardTableLayout Bootstrap(ExtractedCriticalSource source, StandardTableAxisTemplate template)
    {
        var fragments = source.Fragments;
        var headerFragments = FindHeaderFragments(fragments, template);
        var columnCenters = headerFragments
            .OrderBy(item => item.Left)
            .Select(item => (Key: NormalizeHeaderText(item.Text), CenterX: item.CenterX))
            .ToList();
        var headerTop = headerFragments.Max(item => item.Top);
        var keyTop = CriticalTableParserSupport.FindKeyTop(fragments);
        var leftCutoff = ResolveRowLabelLeftCutoff(headerFragments);
        var rowLabelFragments = CriticalTableParserSupport.FindRowLabelFragments(
            fragments,
            leftCutoff,
            headerTop + CriticalTableParserSupport.HeaderToRowLabelMinimumGap,
            keyTop);
        var rowAnchors = CriticalTableParserSupport.CreateRowAnchors(rowLabelFragments);
        var bodyStartTop = CriticalTableParserSupport.ResolveBodyStartTop(headerTop, rowAnchors);
        var warnings = new List<string>();

        if (rowAnchors.Count != template.RollBandLabels.Count)
        {
            throw new InvalidOperationException(
                $"OCR bootstrap found {rowAnchors.Count} row anchors but template '{template.Slug}' expects {template.RollBandLabels.Count}.");
        }

        var actualLabels = rowAnchors.Select(item => item.Label).ToList();
        if (!actualLabels.SequenceEqual(template.RollBandLabels, StringComparer.Ordinal))
        {
            throw new InvalidOperationException(
                $"OCR bootstrap row anchors do not match template '{template.Slug}'.");
        }

        var fuzzyHeaders = headerFragments
            .Where(item => !string.Equals(item.Text, NormalizeHeaderText(item.Text), StringComparison.Ordinal))
            .ToList();
        if (fuzzyHeaders.Count > 0)
        {
            warnings.Add(
                $"OCR header normalization was applied for: {string.Join(", ", fuzzyHeaders.Select(item => $"'{item.Text}' -> '{NormalizeHeaderText(item.Text)}'"))}.");
        }

        var lowConfidenceAnchors = headerFragments
            .Concat(rowLabelFragments)
            .Where(item => item.Confidence is int confidence && confidence < AnchorConfidenceWarningThreshold)
            .Select(item => $"'{item.Text}' ({item.Confidence})")
            .ToList();
        if (lowConfidenceAnchors.Count > 0)
        {
            warnings.Add($"Low-confidence OCR anchors: {string.Join(", ", lowConfidenceAnchors)}.");
        }

        return new StandardTableLayout(
            headerFragments,
            columnCenters,
            rowAnchors,
            headerTop,
            bodyStartTop,
            keyTop,
            leftCutoff,
            warnings);
    }

    private static List<PositionedTextFragment> FindHeaderFragments(
        IReadOnlyList<PositionedTextFragment> fragments,
        StandardTableAxisTemplate template)
    {
        var headerCandidates = fragments
            .Where(item => TryNormalizeHeaderText(item.Text, out _))
            .OrderBy(item => item.Top)
            .ThenBy(item => item.Left)
            .ToList();

        foreach (var group in GroupHeaderCandidates(headerCandidates))
        {
            var ordered = group.OrderBy(item => item.Left).ToList();
            var labels = ordered.Select(item => NormalizeHeaderText(item.Text)).ToList();
            if (labels.SequenceEqual(template.ColumnKeys, StringComparer.Ordinal))
            {
                return ordered;
            }
        }

        throw new InvalidOperationException("Could not find the OCR standard-table A-E header row.");
    }

    private static string NormalizeHeaderText(string value)
    {
        if (!TryNormalizeHeaderText(value, out var normalized))
        {
            throw new InvalidOperationException($"Unsupported OCR header fragment '{value}'.");
        }

        return normalized;
    }

    private static bool TryNormalizeHeaderText(string value, out string normalized)
    {
        normalized = value.Trim().ToUpperInvariant();
        if (normalized is "A" or "B" or "D" or "E")
        {
            return true;
        }

        if (normalized is "C" or "CC")
        {
            normalized = "C";
            return true;
        }

        return false;
    }

    private static IEnumerable<List<PositionedTextFragment>> GroupHeaderCandidates(IReadOnlyList<PositionedTextFragment> fragments)
    {
        var groups = new List<List<PositionedTextFragment>>();

        foreach (var fragment in fragments)
        {
            if (groups.Count == 0 || Math.Abs(groups[^1][0].Top - fragment.Top) > HeaderTopTolerance)
            {
                groups.Add([fragment]);
                continue;
            }

            groups[^1].Add(fragment);
        }

        return groups;
    }

    private static int ResolveRowLabelLeftCutoff(IReadOnlyList<PositionedTextFragment> headerFragments)
    {
        var ordered = headerFragments.OrderBy(item => item.Left).ToList();
        if (ordered.Count < 2)
        {
            return Math.Max(0, ordered[0].Left - 10);
        }

        var firstColumnGap = ordered[1].Left - ordered[0].Left;
        return Math.Max(0, ordered[0].Left - (firstColumnGap / 2));
    }
}
