using System.Text.RegularExpressions;
using PlotLine.Models;

namespace PlotLine.Services;

public sealed record CharacterAppearanceFact(
    string CanonicalName,
    IReadOnlyList<string> Aliases,
    string AttributeType,
    string NormalisedValue,
    string EvidenceFragment,
    decimal Confidence,
    bool IsExplicit,
    string SourceScene);

public static class CharacterAppearanceExtractionService
{
    private static readonly Regex MissCallMeRegex = new(@"\bMiss\s+(?<surname>[A-Z][A-Za-z'’-]+)\b[^.!?]{0,80}?\b(?:call me|called)\s+(?<first>[A-Z][A-Za-z'’-]+)\b", RegexOptions.Compiled);
    private static readonly Regex HairRegex = new(@"\b(?<name>Mum|Mother|Mrs\s+[A-Z][A-Za-z'’-]+|Miss\s+[A-Z][A-Za-z'’-]+|[A-Z][A-Za-z'’-]+)\b[^.!?]{0,90}?\b(?<length>(?i:long|short|shoulder-length|shoulder length|medium-length|medium length))?\s*(?<qualifier>(?i:natural)\s+)?(?<colour>(?i:red|auburn|ginger|blonde|fair|black|brown|brunette|grey|gray|white))\s+(?i:hair)\b", RegexOptions.Compiled);
    private static readonly Regex HairAdjectiveRegex = new(@"\b(?<name>Mum|Mother|Mrs\s+[A-Z][A-Za-z'’-]+|Miss\s+[A-Z][A-Za-z'’-]+|[A-Z][A-Za-z'’-]+)\b[^.!?]{0,60}?\b(?<colour>(?i:blonde|fair-haired|red-haired|auburn-haired|ginger-haired|black-haired|brown-haired|brunette|grey-haired|gray-haired|white-haired))\b", RegexOptions.Compiled);
    private static readonly Regex HairLeadingRegex = new(@"\b(?<length>(?i:long|short|shoulder-length|shoulder length|medium-length|medium length))?\s*(?<qualifier>(?i:natural)\s+)?(?<colour>(?i:red|auburn|ginger|blonde|fair|black|brown|brunette|grey|gray|white))\s+(?i:hair)(?:ed)?\s+(?<name>(?i:girl|woman|man|boy))\b", RegexOptions.Compiled);
    private static readonly Regex HairAdjectiveLeadingRegex = new(@"\b(?<colour>(?i:blonde|fair-haired|red-haired|auburn-haired|ginger-haired|black-haired|brown-haired|brunette|grey-haired|gray-haired|white-haired))\s+(?<name>(?i:girl|woman|man|boy|young woman|young man))\b", RegexOptions.Compiled);
    private static readonly Regex AgeRegex = new(@"\b(?<name>Mum|Mother|Mrs\s+[A-Z][A-Za-z'’-]+|Miss\s+[A-Z][A-Za-z'’-]+|[A-Z][A-Za-z'’-]+)\b[^.!?]{0,90}?\b(?<age>(?i:late teenage|teenage|fifteen|sixteen|seventeen|eighteen|nineteen|young woman|young man|adult woman|adult man|middle-aged|mature|mother|mum|driving examiner|driving instructor|teacher))\b", RegexOptions.Compiled);

    public static IReadOnlyList<CharacterAppearanceFact> Extract(string sourceText, IEnumerable<string>? knownCharacterNames = null, string sourceScene = "Fixture")
    {
        var facts = new List<CharacterAppearanceFact>();
        if (string.IsNullOrWhiteSpace(sourceText))
        {
            return facts;
        }

        var aliases = AliasMap(sourceText, knownCharacterNames);
        foreach (Match match in HairRegex.Matches(sourceText))
        {
            var rawName = match.Groups["name"].Value.Trim();
            AddHairFacts(facts, CanonicalName(rawName, aliases), AliasesFor(rawName, aliases), match, sourceScene);
        }

        foreach (Match match in HairAdjectiveRegex.Matches(sourceText))
        {
            var rawName = match.Groups["name"].Value.Trim();
            AddHairFacts(facts, CanonicalName(rawName, aliases), AliasesFor(rawName, aliases), match, sourceScene);
        }

        foreach (Match match in HairLeadingRegex.Matches(sourceText))
        {
            var descriptor = match.Groups["name"].Value.Trim();
            var candidate = (knownCharacterNames ?? []).FirstOrDefault(name => sourceText.Contains(name, StringComparison.OrdinalIgnoreCase));
            if (!string.IsNullOrWhiteSpace(candidate))
            {
                AddHairFacts(facts, CanonicalName(candidate, aliases), AliasesFor(candidate, aliases), match, sourceScene);
            }
            else
            {
                AddHairFacts(facts, descriptor, [descriptor], match, sourceScene);
            }
        }

        foreach (Match match in HairAdjectiveLeadingRegex.Matches(sourceText))
        {
            var descriptor = match.Groups["name"].Value.Trim();
            var candidate = (knownCharacterNames ?? []).FirstOrDefault(name => sourceText.Contains(name, StringComparison.OrdinalIgnoreCase));
            AddHairFacts(facts, string.IsNullOrWhiteSpace(candidate) ? descriptor : CanonicalName(candidate, aliases), string.IsNullOrWhiteSpace(candidate) ? [descriptor] : AliasesFor(candidate, aliases), match, sourceScene);
        }

        foreach (Match match in AgeRegex.Matches(sourceText))
        {
            var rawName = match.Groups["name"].Value.Trim();
            var age = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence(rawName, [match.Value], []).AgeBand;
            if (age != StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown)
            {
                facts.Add(new(CanonicalName(rawName, aliases), AliasesFor(rawName, aliases), "AgeBand", age, ShortFragment(match.Value), 0.9m, true, sourceScene));
            }
        }

        foreach (var (alias, canonical) in aliases)
        {
            facts.Add(new(canonical, AliasesFor(alias, aliases), "Alias", alias, AliasEvidence(sourceText, alias, canonical), 0.96m, true, sourceScene));
        }

        return facts
            .GroupBy(fact => $"{fact.CanonicalName}|{fact.AttributeType}|{fact.NormalisedValue}|{fact.EvidenceFragment}", StringComparer.OrdinalIgnoreCase)
            .Select(group => group.First())
            .ToList();
    }

    public static string CanonicalName(string name, IReadOnlyDictionary<string, string>? aliases = null)
    {
        var clean = (name ?? string.Empty).Trim();
        if (aliases is not null && aliases.TryGetValue(Normalise(clean), out var canonical))
        {
            return canonical;
        }

        return clean;
    }

    public static IReadOnlyDictionary<string, string> AliasMap(string sourceText, IEnumerable<string>? knownCharacterNames = null)
    {
        var aliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        foreach (Match match in MissCallMeRegex.Matches(sourceText ?? string.Empty))
        {
            var first = match.Groups["first"].Value.Trim();
            var surname = match.Groups["surname"].Value.Trim();
            var canonical = $"{first} {surname}";
            aliases[Normalise(first)] = canonical;
            aliases[Normalise($"Miss {surname}")] = canonical;
            aliases[Normalise($"Miss {surname} ({first})")] = canonical;
        }

        return aliases;
    }

    private static void AddHairFacts(List<CharacterAppearanceFact> facts, string canonical, IReadOnlyList<string> aliases, Match match, string sourceScene)
    {
        var colour = StoryIntelligenceIllustrationCompatibility.NormalizeHairColour(match.Groups["colour"].Value);
        if (colour != StoryIntelligenceIllustrationCompatibility.HairColours.Unknown)
        {
            facts.Add(new(canonical, aliases, "HairColour", colour, ShortFragment(match.Value), 0.94m, true, sourceScene));
        }

        var length = StoryIntelligenceIllustrationCompatibility.NormalizeHairLength(match.Groups["length"].Value);
        if (length != "Unknown")
        {
            facts.Add(new(canonical, aliases, "HairLength", length, ShortFragment(match.Value), 0.88m, true, sourceScene));
        }
    }

    private static IReadOnlyList<string> AliasesFor(string name, IReadOnlyDictionary<string, string> aliases)
    {
        var canonical = CanonicalName(name, aliases);
        var list = aliases
            .Where(item => string.Equals(item.Value, canonical, StringComparison.OrdinalIgnoreCase))
            .Select(item => item.Key)
            .Append(Normalise(name))
            .Where(item => !string.IsNullOrWhiteSpace(item))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToList();
        return list.Count == 0 ? [name] : list;
    }

    private static string AliasEvidence(string sourceText, string alias, string canonical)
    {
        var index = sourceText.IndexOf(alias, StringComparison.OrdinalIgnoreCase);
        if (index < 0)
        {
            index = sourceText.IndexOf(canonical.Split(' ')[0], StringComparison.OrdinalIgnoreCase);
        }

        if (index < 0)
        {
            return canonical;
        }

        var start = Math.Max(0, index - 60);
        var length = Math.Min(sourceText.Length - start, 140);
        return ShortFragment(sourceText.Substring(start, length));
    }

    private static string ShortFragment(string value)
        => Regex.Replace(value.Trim(), @"\s+", " ") is var clean && clean.Length > 160 ? clean[..160].TrimEnd() : clean;

    private static string Normalise(string? value)
    {
        var chars = (value ?? string.Empty).Trim().ToLowerInvariant().Select(ch => char.IsLetterOrDigit(ch) ? ch : ' ').ToArray();
        return string.Join(' ', new string(chars).Split(' ', StringSplitOptions.RemoveEmptyEntries));
    }
}
