using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace GxMcp.Worker.Helpers
{
    public sealed class XmlEquivalenceDiff
    {
        public string Path { get; set; }
        public string Summary { get; set; }
        public string[] LeftAttributes { get; set; }
        public string[] RightAttributes { get; set; }
        // Rejected by SDK = present in input (right) but absent in persisted (left).
        public string[] RejectedAttributes { get; set; }
        // Added by SDK = present in persisted (left) but absent in input (right).
        public string[] AddedAttributes { get; set; }
        public string ElementName { get; set; }
    }

    public static class XmlEquivalence
    {
        public static bool AreEquivalent(string a, string b, out string diffSummary)
        {
            return AreEquivalent(a, b, out diffSummary, out _);
        }

        public static bool AreEquivalent(string a, string b, out string diffSummary, out XmlEquivalenceDiff structuredDiff)
        {
            diffSummary = null;
            structuredDiff = null;
            if (ReferenceEquals(a, b)) return true;
            if (string.IsNullOrEmpty(a) && string.IsNullOrEmpty(b)) return true;
            if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b))
            {
                diffSummary = "One side empty.";
                structuredDiff = new XmlEquivalenceDiff { Summary = diffSummary };
                return false;
            }

            XDocument da, db;
            try { da = XDocument.Parse(a, LoadOptions.PreserveWhitespace); }
            catch (Exception ex) { diffSummary = "Left parse error: " + ex.Message; structuredDiff = new XmlEquivalenceDiff { Summary = diffSummary }; return false; }
            try { db = XDocument.Parse(b, LoadOptions.PreserveWhitespace); }
            catch (Exception ex) { diffSummary = "Right parse error: " + ex.Message; structuredDiff = new XmlEquivalenceDiff { Summary = diffSummary }; return false; }

            var ok = ElementsEqual(da.Root, db.Root, "/", out diffSummary, out structuredDiff);
            if (!ok && structuredDiff == null)
                structuredDiff = new XmlEquivalenceDiff { Summary = diffSummary };
            return ok;
        }

        private static bool ElementsEqual(XElement x, XElement y, string path, out string diff, out XmlEquivalenceDiff structured)
        {
            diff = null;
            structured = null;
            if (x == null && y == null) return true;
            if (x == null || y == null) { diff = "Missing element at " + path; return false; }
            if (x.Name != y.Name) { diff = "Element name differs at " + path + ": '" + x.Name + "' vs '" + y.Name + "'"; return false; }

            var ax = x.Attributes().OrderBy(a => a.Name.ToString(), StringComparer.Ordinal).ToList();
            var ay = y.Attributes().OrderBy(a => a.Name.ToString(), StringComparer.Ordinal).ToList();
            var lNames = ax.Select(a => a.Name.LocalName).ToArray();
            var rNames = ay.Select(a => a.Name.LocalName).ToArray();
            if (ax.Count != ay.Count)
            {
                var lSet = new HashSet<string>(lNames, StringComparer.Ordinal);
                var rSet = new HashSet<string>(rNames, StringComparer.Ordinal);
                var rejected = rNames.Where(n => !lSet.Contains(n)).ToArray(); // requested but missing after save → SDK rejected
                var added = lNames.Where(n => !rSet.Contains(n)).ToArray();    // SDK injected on persist
                diff = "Attribute count differs at " + path + x.Name + " (" + ax.Count + " vs " + ay.Count + ")"
                       + ": left=[" + string.Join(",", lNames) + "] right=[" + string.Join(",", rNames) + "]"
                       + (rejected.Length > 0 ? "; rejectedByPersist=[" + string.Join(",", rejected) + "]" : "")
                       + (added.Length > 0 ? "; addedByPersist=[" + string.Join(",", added) + "]" : "");
                structured = new XmlEquivalenceDiff
                {
                    Path = path + x.Name,
                    ElementName = x.Name.LocalName,
                    Summary = diff,
                    LeftAttributes = lNames,
                    RightAttributes = rNames,
                    RejectedAttributes = rejected,
                    AddedAttributes = added
                };
                return false;
            }
            for (int i = 0; i < ax.Count; i++)
            {
                if (ax[i].Name != ay[i].Name)
                {
                    diff = "Attribute name differs at " + path + x.Name + ": '" + ax[i].Name + "' vs '" + ay[i].Name + "'";
                    structured = new XmlEquivalenceDiff { Path = path + x.Name, ElementName = x.Name.LocalName, Summary = diff, LeftAttributes = lNames, RightAttributes = rNames };
                    return false;
                }
                if (!string.Equals(ax[i].Value, ay[i].Value, StringComparison.Ordinal))
                {
                    diff = "Attribute '" + ax[i].Name + "' differs at " + path + x.Name
                           + ": '" + Truncate(ax[i].Value) + "' vs '" + Truncate(ay[i].Value) + "'";
                    structured = new XmlEquivalenceDiff { Path = path + x.Name, ElementName = x.Name.LocalName, Summary = diff, LeftAttributes = lNames, RightAttributes = rNames };
                    return false;
                }
            }

            var cx = SignificantChildren(x).ToList();
            var cy = SignificantChildren(y).ToList();
            if (cx.Count != cy.Count)
            {
                diff = "Child count differs at " + path + x.Name + " (" + cx.Count + " vs " + cy.Count + ")";
                return false;
            }

            for (int i = 0; i < cx.Count; i++)
            {
                var nx = cx[i];
                var ny = cy[i];
                if (nx.NodeType != ny.NodeType)
                {
                    diff = "Node type differs at " + path + x.Name + "[" + i + "]: " + nx.NodeType + " vs " + ny.NodeType;
                    return false;
                }

                if (nx is XElement ex2 && ny is XElement ey2)
                {
                    if (!ElementsEqual(ex2, ey2, path + x.Name + "/", out diff, out structured)) return false;
                }
                else if (nx is XText tx && ny is XText ty)
                {
                    var vx = (tx.Value ?? string.Empty).Trim();
                    var vy = (ty.Value ?? string.Empty).Trim();
                    if (!string.Equals(vx, vy, StringComparison.Ordinal))
                    {
                        diff = "Text differs at " + path + x.Name + "[" + i + "]: '" + Truncate(vx) + "' vs '" + Truncate(vy) + "'";
                        return false;
                    }
                }
            }
            return true;
        }

        private static IEnumerable<XNode> SignificantChildren(XElement e)
        {
            foreach (var n in e.Nodes())
            {
                if (n is XText t)
                {
                    if (string.IsNullOrWhiteSpace(t.Value)) continue;
                    yield return t;
                }
                else if (n is XComment) continue;
                else yield return n;
            }
        }

        private static string Truncate(string s)
        {
            if (s == null) return string.Empty;
            return s.Length <= 80 ? s : s.Substring(0, 80) + "…";
        }

        // v2.3.8 Task 4.6 — line-based diff used by patch verification to decide whether
        // a post-save divergence is INSIDE the edited window (real rollback condition)
        // or OUTSIDE it (SDK normalized an untouched line — e.g. DATETIME(10,5)→(8,5);
        // treat as a side-effect, not a verification failure). Despite living on
        // XmlEquivalence, the helper is text-line based — the plan calls it `HunkDiff`
        // because the larger feature classifies on top of XML/source verification.
        //
        // Walks both line arrays, finds the first and last differing lines, and emits
        // one hunk per maximal contiguous run of differences. Each hunk records the
        // 1-based starting line in `before`, plus the joined `Before` / `After` text.
        public sealed class LineHunk
        {
            public int Line;        // 1-based line in 'before' where the hunk starts
            public string Before;   // joined removed lines (may be empty for pure insertions)
            public string After;    // joined inserted lines (may be empty for pure deletions)
            public int BeforeLineCount;
            public int AfterLineCount;
        }

        public static List<LineHunk> HunkDiff(string before, string after)
        {
            var result = new List<LineHunk>();
            string[] b = SplitLines(before);
            string[] a = SplitLines(after);

            // Trim shared prefix.
            int prefix = 0;
            int minLen = Math.Min(b.Length, a.Length);
            while (prefix < minLen && string.Equals(b[prefix], a[prefix], StringComparison.Ordinal))
                prefix++;

            // Trim shared suffix.
            int suffix = 0;
            while (suffix < (minLen - prefix)
                   && string.Equals(b[b.Length - 1 - suffix], a[a.Length - 1 - suffix], StringComparison.Ordinal))
                suffix++;

            int bEnd = b.Length - suffix;       // exclusive
            int aEnd = a.Length - suffix;       // exclusive

            if (prefix >= bEnd && prefix >= aEnd) return result; // identical

            // Inside the differing slab, walk pairwise; group runs of differing lines.
            int i = prefix, j = prefix;
            while (i < bEnd || j < aEnd)
            {
                // Find a run of differences.
                int hunkStartBefore = i;
                var beforeRun = new List<string>();
                var afterRun = new List<string>();

                while (i < bEnd && j < aEnd && !string.Equals(b[i], a[j], StringComparison.Ordinal))
                {
                    beforeRun.Add(b[i]);
                    afterRun.Add(a[j]);
                    i++; j++;
                }
                if (i >= bEnd || j >= aEnd)
                {
                    // Tail asymmetry: leftover lines on one side.
                    while (i < bEnd) { beforeRun.Add(b[i++]); }
                    while (j < aEnd) { afterRun.Add(a[j++]); }
                }

                if (beforeRun.Count > 0 || afterRun.Count > 0)
                {
                    result.Add(new LineHunk
                    {
                        Line = hunkStartBefore + 1, // 1-based
                        Before = string.Join("\n", beforeRun),
                        After = string.Join("\n", afterRun),
                        BeforeLineCount = beforeRun.Count,
                        AfterLineCount = afterRun.Count
                    });
                }

                // Skip any matching middle (rare for simple diffs since we already trimmed
                // prefix/suffix, but defensive in case of multiple disjoint hunks).
                while (i < bEnd && j < aEnd && string.Equals(b[i], a[j], StringComparison.Ordinal))
                { i++; j++; }
            }

            return result;
        }

        // True when a hunk overlaps the inclusive 1-based edit window [windowStart, windowEnd].
        // Pure insertions (BeforeLineCount==0) are treated as touching `Line` only.
        public static bool HunkOverlapsWindow(LineHunk h, int windowStart, int windowEnd)
        {
            if (h == null) return false;
            if (windowStart <= 0 || windowEnd <= 0 || windowEnd < windowStart) return false;
            int hStart = h.Line;
            int hEnd = h.Line + Math.Max(0, h.BeforeLineCount - 1);
            if (h.BeforeLineCount == 0) hEnd = h.Line; // pure insertion
            return hEnd >= windowStart && hStart <= windowEnd;
        }

        private static string[] SplitLines(string s)
        {
            if (string.IsNullOrEmpty(s)) return new string[0];
            return s.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n');
        }
    }
}
