using System.Text.Json;
using System.Text.RegularExpressions;
using JobTrackerApi.Models;

namespace JobTrackerApi.Services;

// Phase 2.1-a — the diff half of the review-and-merge workflow. Pure comparison of the user's current
// Career Profile against a freshly-extracted profile; produces structured, user-facing changes. It
// applies NOTHING — the merge engine consumes an accepted diff separately. No DB, no AI, no state.
//
// Conservative by design (the approved policy): items match only when they clearly refer to the same
// thing (company+title, institution+qualification, project/certification name, language/skill text),
// a field is only ever proposed as an *update* when the extracted value is non-empty and differs, and
// nothing the user already has is ever marked for deletion.

public enum CvChangeKind { Add, Update }

public sealed record CvFieldChange(string Field, string? OldValue, string? NewValue);

public sealed class CvItemChange
{
    public string Id { get; init; } = "";
    public CvChangeKind Kind { get; init; }
    public string Category { get; init; } = "";
    public string Label { get; init; } = "";
    public string Confidence { get; init; } = "Medium"; // High | Medium | Low
    public List<CvFieldChange> FieldChanges { get; init; } = new();
}

public sealed class CvCategoryDiff
{
    public string Category { get; init; } = "";
    public List<CvItemChange> Added { get; init; } = new();
    public List<CvItemChange> Updated { get; init; } = new();
    public int UnchangedCount { get; init; }
    public int LowConfidenceCount => Added.Concat(Updated).Count(c => c.Confidence == "Low");
}

public sealed class CvImportDiff
{
    public List<CvCategoryDiff> Categories { get; init; } = new();
    public int TotalAdded => Categories.Sum(c => c.Added.Count);
    public int TotalUpdated => Categories.Sum(c => c.Updated.Count);
    public int TotalLowConfidence => Categories.Sum(c => c.LowConfidenceCount);
    public bool HasChanges => TotalAdded > 0 || TotalUpdated > 0;
}

public interface ICvProfileDiffService
{
    CvImportDiff Diff(StructuredCvProfile current, StructuredCvProfile extracted);
    StructuredCvProfile Merge(StructuredCvProfile current, StructuredCvProfile extracted, IReadOnlySet<string>? acceptedLowConfidenceIds = null);
}

public sealed class CvProfileDiffService : ICvProfileDiffService
{
    public CvImportDiff Diff(StructuredCvProfile current, StructuredCvProfile extracted)
    {
        current ??= new StructuredCvProfile();
        extracted ??= new StructuredCvProfile();

        return new CvImportDiff
        {
            Categories = new List<CvCategoryDiff>
            {
                DiffContact(current.Contact, extracted.Contact),
                DiffSummary(current.Summary, extracted.Summary),
                DiffList("Experience", current.Jobs, extracted.Jobs, JobKey, JobLabel, JobFields, JobConfidence),
                DiffList("Education", current.Education, extracted.Education, EduKey, EduLabel, EduFields, EduConfidence),
                DiffList("Projects", current.Projects, extracted.Projects, p => Norm(p.Name), p => p.Name ?? "Project", ProjectFields, p => Confidence(p.Name, p.Bullets.Count > 0)),
                DiffList("Certifications", current.Certifications, extracted.Certifications, c => Norm(c.Name), c => c.Name ?? "Certification", CertFields, c => Confidence(c.Name, !string.IsNullOrWhiteSpace(c.Issuer))),
                DiffLanguages(current.Languages, extracted.Languages),
                DiffScalars("Skills", current.Skills, extracted.Skills),
                DiffScalars("Interests", current.Interests, extracted.Interests),
                DiffScalars("Awards", current.Awards, extracted.Awards),
                DiffScalars("Publications", current.Publications, extracted.Publications),
                DiffScalars("Organisations", current.Organisations, extracted.Organisations),
                DiffScalars("References", current.References, extracted.References),
            }.Where(c => c is not null).Select(c => c!).ToList(),
        };
    }

    public StructuredCvProfile Merge(StructuredCvProfile current, StructuredCvProfile extracted, IReadOnlySet<string>? acceptedLowConfidenceIds = null)
    {
        var merged = StructuredCvProfileJson.DeserializePersisted(StructuredCvProfileJson.SerializePersisted(current ?? new StructuredCvProfile()));
        extracted = FilterLowConfidence(extracted ?? new StructuredCvProfile(), acceptedLowConfidenceIds);

        MergeContact(merged.Contact, extracted.Contact);
        if (extracted.Summary.Any(x => !string.IsNullOrWhiteSpace(x))) merged.Summary = extracted.Summary.Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
        MergeList(merged.Jobs, extracted.Jobs, JobKey, MergeJob);
        MergeList(merged.Education, extracted.Education, EduKey, MergeEducation);
        MergeList(merged.Projects, extracted.Projects, p => Norm(p.Name), MergeProject);
        MergeList(merged.Certifications, extracted.Certifications, c => Norm(c.Name), MergeCertification);
        MergeLanguages(merged.Languages, extracted.Languages);
        AppendUnique(merged.Skills, extracted.Skills);
        AppendUnique(merged.Interests, extracted.Interests);
        AppendUnique(merged.Awards, extracted.Awards);
        AppendUnique(merged.Publications, extracted.Publications);
        AppendUnique(merged.Organisations, extracted.Organisations);
        AppendUnique(merged.References, extracted.References);
        MergeList(merged.OtherSections, extracted.OtherSections, x => Norm(x.Title), (a, b) => AppendUnique(a.Items, b.Items));
        if (extracted.Sections.Count > 0) merged.Sections = extracted.Sections;
        foreach (var field in extracted.Metadata.Fields) merged.Metadata.Fields[field.Key] = field.Value;
        return merged;
    }

    private static StructuredCvProfile FilterLowConfidence(StructuredCvProfile source, IReadOnlySet<string>? accepted)
    {
        var filtered = JsonSerializer.Deserialize<StructuredCvProfile>(JsonSerializer.Serialize(source)) ?? new StructuredCvProfile();
        bool Allowed(string category, string key, string confidence) => confidence != "Low" || (accepted?.Contains(ChangeId(category, key)) ?? false);

        if (!Allowed("Contact", string.Empty, Confidence(filtered.Contact.FullName, !string.IsNullOrWhiteSpace(filtered.Contact.Email)))) filtered.Contact = new StructuredCvContact();
        filtered.Jobs = filtered.Jobs.Where(x => Allowed("Experience", JobKey(x), JobConfidence(x))).ToList();
        filtered.Education = filtered.Education.Where(x => Allowed("Education", EduKey(x), EduConfidence(x))).ToList();
        filtered.Projects = filtered.Projects.Where(x => Allowed("Projects", Norm(x.Name), Confidence(x.Name, x.Bullets.Count > 0))).ToList();
        filtered.Certifications = filtered.Certifications.Where(x => Allowed("Certifications", Norm(x.Name), Confidence(x.Name, !string.IsNullOrWhiteSpace(x.Issuer)))).ToList();
        filtered.Languages = filtered.Languages.Where(x => Allowed("Languages", Norm(x.Name), string.IsNullOrWhiteSpace(x.Level) ? "Low" : "High")).ToList();
        return filtered;
    }

    private static void MergeList<T>(List<T> current, IEnumerable<T> extracted, Func<T, string> key, Action<T, T> merge)
    {
        var currentByKey = current.Where(x => key(x).Length > 0).GroupBy(key).ToDictionary(g => g.Key, g => g.First());
        foreach (var incoming in extracted)
        {
            var k = key(incoming);
            if (k.Length > 0 && currentByKey.TryGetValue(k, out var existing)) merge(existing, incoming);
            else if (k.Length > 0 && !currentByKey.ContainsKey(k))
            {
                current.Add(incoming);
                currentByKey[k] = incoming;
            }
        }
    }

    private static void MergeContact(StructuredCvContact current, StructuredCvContact incoming)
    {
        SetIfPresent(incoming.FullName, v => current.FullName = v);
        SetIfPresent(incoming.Headline, v => current.Headline = v);
        SetIfPresent(incoming.Email, v => current.Email = v);
        SetIfPresent(incoming.Phone, v => current.Phone = v);
        SetIfPresent(incoming.Location, v => current.Location = v);
        SetIfPresent(incoming.Website, v => current.Website = v);
        SetIfPresent(incoming.LinkedIn, v => current.LinkedIn = v);
    }

    private static void MergeJob(StructuredCvJob current, StructuredCvJob incoming)
    {
        SetIfPresent(incoming.Location, v => current.Location = v);
        SetIfPresent(incoming.Start, v => current.Start = v);
        SetIfPresent(incoming.End, v => current.End = v);
        current.IsCurrent = incoming.IsCurrent || current.IsCurrent;
        AppendUnique(current.Bullets, incoming.Bullets);
        AppendUnique(current.Skills, incoming.Skills);
    }

    private static void MergeEducation(StructuredCvEducation current, StructuredCvEducation incoming)
    {
        SetIfPresent(incoming.QualificationLevel, v => current.QualificationLevel = v);
        SetIfPresent(incoming.Location, v => current.Location = v);
        SetIfPresent(incoming.Start, v => current.Start = v);
        SetIfPresent(incoming.End, v => current.End = v);
        AppendUnique(current.Details, incoming.Details);
    }

    private static void MergeProject(StructuredCvProject current, StructuredCvProject incoming)
    {
        SetIfPresent(incoming.Role, v => current.Role = v);
        SetIfPresent(incoming.Location, v => current.Location = v);
        SetIfPresent(incoming.Start, v => current.Start = v);
        SetIfPresent(incoming.End, v => current.End = v);
        AppendUnique(current.Bullets, incoming.Bullets);
        AppendUnique(current.Skills, incoming.Skills);
    }

    private static void MergeCertification(StructuredCvCertification current, StructuredCvCertification incoming)
    {
        SetIfPresent(incoming.Issuer, v => current.Issuer = v);
        SetIfPresent(incoming.Location, v => current.Location = v);
        SetIfPresent(incoming.Date, v => current.Date = v);
        AppendUnique(current.Details, incoming.Details);
    }

    private static void MergeLanguages(List<StructuredCvLanguage> current, IEnumerable<StructuredCvLanguage> extracted)
    {
        var currentByName = current.Where(x => !string.IsNullOrWhiteSpace(x.Name)).GroupBy(x => Norm(x.Name)).ToDictionary(g => g.Key, g => g.First());
        foreach (var incoming in extracted.Where(x => !string.IsNullOrWhiteSpace(x.Name)))
        {
            var k = Norm(incoming.Name);
            if (currentByName.TryGetValue(k, out var existing))
            {
                SetIfPresent(incoming.Level, v => existing.Level = v);
                SetIfPresent(incoming.Notes, v => existing.Notes = v);
            }
            else
            {
                current.Add(incoming);
                currentByName[k] = incoming;
            }
        }
    }

    private static void AppendUnique(List<string> current, IEnumerable<string> incoming)
    {
        var seen = new HashSet<string>(current.Select(Norm).Where(x => x.Length > 0));
        foreach (var value in incoming.Where(x => !string.IsNullOrWhiteSpace(x)))
            if (seen.Add(Norm(value))) current.Add(value.Trim());
    }

    private static void SetIfPresent(string? value, Action<string> set)
    {
        if (!string.IsNullOrWhiteSpace(value)) set(value.Trim());
    }

    // ---- list diff (Experience / Education / Projects / Certifications) --------------------------

    private static CvCategoryDiff DiffList<T>(
        string category,
        List<T> current,
        List<T> extracted,
        Func<T, string> key,
        Func<T, string> label,
        Func<T, T, List<CvFieldChange>> fieldChanges,
        Func<T, string> confidence)
    {
        var currentByKey = new Dictionary<string, T>();
        foreach (var item in current)
        {
            var k = key(item);
            if (!string.IsNullOrEmpty(k)) currentByKey[k] = item;
        }

        var added = new List<CvItemChange>();
        var updated = new List<CvItemChange>();
        var unchanged = 0;

        foreach (var item in extracted)
        {
            var k = key(item);
            if (!string.IsNullOrEmpty(k) && currentByKey.TryGetValue(k, out var existing))
            {
                var changes = fieldChanges(existing, item);
                if (changes.Count > 0)
                    updated.Add(new CvItemChange { Id = ChangeId(category, k), Kind = CvChangeKind.Update, Category = category, Label = label(item), Confidence = confidence(item), FieldChanges = changes });
                else
                    unchanged++;
            }
            else
            {
                added.Add(new CvItemChange { Id = ChangeId(category, k), Kind = CvChangeKind.Add, Category = category, Label = label(item), Confidence = confidence(item) });
            }
        }

        return new CvCategoryDiff { Category = category, Added = added, Updated = updated, UnchangedCount = unchanged };
    }

    // ---- contact (field-level) ------------------------------------------------------------------

    private static CvCategoryDiff DiffContact(StructuredCvContact current, StructuredCvContact extracted)
    {
        var fields = new List<CvFieldChange>();
        void Compare(string name, string? oldV, string? newV)
        {
            if (!string.IsNullOrWhiteSpace(newV) && !ValuesEqual(oldV, newV))
                fields.Add(new CvFieldChange(name, oldV, newV));
        }
        Compare("Full name", current.FullName, extracted.FullName);
        Compare("Headline", current.Headline, extracted.Headline);
        Compare("Email", current.Email, extracted.Email);
        Compare("Phone", current.Phone, extracted.Phone);
        Compare("Location", current.Location, extracted.Location);
        Compare("Website", current.Website, extracted.Website);
        Compare("LinkedIn", current.LinkedIn, extracted.LinkedIn);

        var isNew = string.IsNullOrWhiteSpace(current.FullName) && string.IsNullOrWhiteSpace(current.Email);
        var changes = new List<CvItemChange>();
        var added = new List<CvItemChange>();
        var updated = new List<CvItemChange>();
        if (fields.Count > 0)
        {
            var change = new CvItemChange
            {
                Id = "Contact",
                Kind = isNew ? CvChangeKind.Add : CvChangeKind.Update,
                Category = "Contact",
                Label = extracted.FullName ?? "Contact details",
                Confidence = Confidence(extracted.FullName, !string.IsNullOrWhiteSpace(extracted.Email)),
                FieldChanges = fields,
            };
            (isNew ? added : updated).Add(change);
        }
        return new CvCategoryDiff { Category = "Contact", Added = added, Updated = updated };
    }

    private static CvCategoryDiff DiffSummary(List<string> current, List<string> extracted)
    {
        var cur = JoinLines(current);
        var ext = JoinLines(extracted);
        var added = new List<CvItemChange>();
        var updated = new List<CvItemChange>();
        if (!string.IsNullOrWhiteSpace(ext) && !ValuesEqual(cur, ext))
        {
            var change = new CvItemChange
            {
                Id = "Professional summary",
                Kind = string.IsNullOrWhiteSpace(cur) ? CvChangeKind.Add : CvChangeKind.Update,
                Category = "Professional summary",
                Label = "Professional summary",
                Confidence = "Medium",
                FieldChanges = new List<CvFieldChange> { new("Summary", Trunc(cur), Trunc(ext)) },
            };
            (string.IsNullOrWhiteSpace(cur) ? added : updated).Add(change);
        }
        return new CvCategoryDiff { Category = "Professional summary", Added = added, Updated = updated };
    }

    // ---- languages & skills (scalar-ish, dedup by normalized name) -------------------------------

    private static CvCategoryDiff DiffLanguages(List<StructuredCvLanguage> current, List<StructuredCvLanguage> extracted)
    {
        var currentByName = current.Where(l => !string.IsNullOrWhiteSpace(l.Name)).ToDictionary(l => Norm(l.Name), l => l);
        var added = new List<CvItemChange>();
        var updated = new List<CvItemChange>();
        var unchanged = 0;
        foreach (var lang in extracted)
        {
            if (string.IsNullOrWhiteSpace(lang.Name)) continue;
            var k = Norm(lang.Name);
            if (currentByName.TryGetValue(k, out var existing))
            {
                if (!string.IsNullOrWhiteSpace(lang.Level) && !ValuesEqual(existing.Level, lang.Level))
                    updated.Add(new CvItemChange { Id = ChangeId("Languages", k), Kind = CvChangeKind.Update, Category = "Languages", Label = lang.Name!, Confidence = "Medium", FieldChanges = new List<CvFieldChange> { new("Level", existing.Level, lang.Level) } });
                else
                    unchanged++;
            }
            else
            {
                added.Add(new CvItemChange { Id = ChangeId("Languages", k), Kind = CvChangeKind.Add, Category = "Languages", Label = string.IsNullOrWhiteSpace(lang.Level) ? lang.Name! : $"{lang.Name} ({lang.Level})", Confidence = string.IsNullOrWhiteSpace(lang.Level) ? "Low" : "High" });
            }
        }
        return new CvCategoryDiff { Category = "Languages", Added = added, Updated = updated, UnchangedCount = unchanged };
    }

    private static CvCategoryDiff DiffScalars(string category, List<string> current, List<string> extracted)
    {
        var currentSet = new HashSet<string>(current.Select(Norm).Where(s => s.Length > 0));
        var added = new List<CvItemChange>();
        var seen = new HashSet<string>();
        foreach (var item in extracted)
        {
            var k = Norm(item);
            if (k.Length == 0 || !seen.Add(k)) continue;
            if (!currentSet.Contains(k))
                added.Add(new CvItemChange { Id = ChangeId(category, k), Kind = CvChangeKind.Add, Category = category, Label = item.Trim(), Confidence = "High" });
        }
        return new CvCategoryDiff { Category = category, Added = added, UnchangedCount = currentSet.Count };
    }

    // ---- keys / labels / field comparisons -------------------------------------------------------

    private static string JobKey(StructuredCvJob j) => $"{Norm(j.Company)}|{Norm(j.Title)}";
    private static string JobLabel(StructuredCvJob j) => string.Join(" — ", new[] { j.Title, j.Company }.Where(v => !string.IsNullOrWhiteSpace(v)));
    private static List<CvFieldChange> JobFields(StructuredCvJob a, StructuredCvJob b)
    {
        var f = new List<CvFieldChange>();
        AddIf(f, "Dates", DateRange(a.Start, a.End), DateRange(b.Start, b.End));
        AddIf(f, "Location", a.Location, b.Location);
        if (b.Bullets.Count > 0 && !ValuesEqual(JoinLines(a.Bullets), JoinLines(b.Bullets)))
            f.Add(new CvFieldChange("Bullets", $"{a.Bullets.Count} line(s)", $"{b.Bullets.Count} line(s)"));
        return f;
    }
    private static string JobConfidence(StructuredCvJob j) => Confidence(j.Title, !string.IsNullOrWhiteSpace(j.Company) && (!string.IsNullOrWhiteSpace(j.Start) || j.Bullets.Count > 0));

    private static string EduKey(StructuredCvEducation e) => $"{Norm(e.Institution)}|{Norm(e.Qualification)}";
    private static string EduLabel(StructuredCvEducation e) => string.Join(" — ", new[] { e.Qualification, e.Institution }.Where(v => !string.IsNullOrWhiteSpace(v)));
    private static List<CvFieldChange> EduFields(StructuredCvEducation a, StructuredCvEducation b)
    {
        var f = new List<CvFieldChange>();
        AddIf(f, "Dates", DateRange(a.Start, a.End), DateRange(b.Start, b.End));
        AddIf(f, "Location", a.Location, b.Location);
        return f;
    }
    private static string EduConfidence(StructuredCvEducation e) => Confidence(e.Qualification, !string.IsNullOrWhiteSpace(e.Institution));

    private static List<CvFieldChange> ProjectFields(StructuredCvProject a, StructuredCvProject b)
    {
        var f = new List<CvFieldChange>();
        if (b.Bullets.Count > 0 && !ValuesEqual(JoinLines(a.Bullets), JoinLines(b.Bullets)))
            f.Add(new CvFieldChange("Details", $"{a.Bullets.Count} line(s)", $"{b.Bullets.Count} line(s)"));
        return f;
    }

    private static List<CvFieldChange> CertFields(StructuredCvCertification a, StructuredCvCertification b)
    {
        var f = new List<CvFieldChange>();
        AddIf(f, "Issuer", a.Issuer, b.Issuer);
        AddIf(f, "Date", a.Date, b.Date);
        return f;
    }

    // ---- helpers --------------------------------------------------------------------------------

    private static string ChangeId(string category, string key) => key.Length == 0 ? category : $"{category}|{key}";

    private static void AddIf(List<CvFieldChange> f, string name, string? oldV, string? newV)
    {
        if (!string.IsNullOrWhiteSpace(newV) && !ValuesEqual(oldV, newV))
            f.Add(new CvFieldChange(name, oldV, newV));
    }

    private static string Confidence(string? primary, bool corroborated)
    {
        if (string.IsNullOrWhiteSpace(primary)) return "Low";
        return corroborated ? "High" : "Medium";
    }

    private static string DateRange(string? start, string? end)
    {
        var s = (start ?? "").Trim();
        var e = (end ?? "").Trim();
        if (s.Length == 0 && e.Length == 0) return "";
        return $"{s} - {e}".Trim(' ', '-');
    }

    private static bool ValuesEqual(string? a, string? b) => Norm(a) == Norm(b);

    private static string Norm(string? value)
    {
        if (string.IsNullOrWhiteSpace(value)) return "";
        var lowered = value.Trim().ToLowerInvariant();
        return Regex.Replace(lowered, @"[^\p{L}\p{Nd}]+", " ").Trim();
    }

    private static string JoinLines(IEnumerable<string> lines) => string.Join("\n", lines.Where(l => !string.IsNullOrWhiteSpace(l)).Select(l => l.Trim()));
    private static string Trunc(string? s, int max = 140) => string.IsNullOrEmpty(s) ? "" : (s.Length <= max ? s : s[..max] + "…");
}
