using System.Text;

namespace ClaudeDo.Ui.ViewModels.Modals;

/// Shared unified-diff parser used by both the per-task diff viewer and the
/// combined (planning) diff viewer so they render identically.
public static class UnifiedDiffParser
{
    public static List<DiffFileViewModel> Parse(string? raw)
    {
        var files = new List<DiffFileViewModel>();
        if (string.IsNullOrWhiteSpace(raw)) return files;

        DiffFileViewModel? current = null;
        int oldLine = 0, newLine = 0;

        foreach (var line in raw.Split('\n'))
        {
            if (line.StartsWith("diff --git ", StringComparison.Ordinal))
            {
                // e.g. "diff --git a/src/Foo.cs b/src/Foo.cs". Paths may each be
                // git-quoted (C-style, octal-escaped) when they contain non-ASCII
                // bytes, and unquoted paths may themselves contain spaces, so the
                // two paths can't be split on a plain ' ' - this is only a best
                // effort default; "--- "/"+++ "/rename lines below correct it.
                var (_, newPath) = SplitGitHeaderPaths(line["diff --git ".Length..]);
                current = new DiffFileViewModel { Path = newPath };
                files.Add(current);
                oldLine = 0; newLine = 0;
                continue;
            }

            if (current == null) continue;

            // File-level metadata that carries the change kind.
            if (line.StartsWith("new file", StringComparison.Ordinal))
            {
                current.Status = DiffFileStatus.Added;
                continue;
            }
            if (line.StartsWith("deleted file", StringComparison.Ordinal))
            {
                current.Status = DiffFileStatus.Deleted;
                continue;
            }
            if (line.StartsWith("rename from ", StringComparison.Ordinal))
            {
                current.Status = DiffFileStatus.Renamed;
                current.OldPath = UnquotePath(line["rename from ".Length..]);
                continue;
            }
            if (line.StartsWith("rename to ", StringComparison.Ordinal))
            {
                current.Status = DiffFileStatus.Renamed;
                current.Path = UnquotePath(line["rename to ".Length..]);
                continue;
            }
            if (line.StartsWith("Binary files", StringComparison.Ordinal) ||
                line.StartsWith("GIT binary patch", StringComparison.Ordinal))
            {
                current.IsBinary = true;
                continue;
            }

            if (line.StartsWith("@@ ", StringComparison.Ordinal))
            {
                // e.g. "@@ -10,7 +10,9 @@"
                ParseHunkHeader(line, out oldLine, out newLine);
                continue;
            }

            // "--- a/..." / "+++ b/..." each carry a single unambiguous path (unlike
            // the two-paths-on-one-line "diff --git" header above), so use them to
            // correct the file's identity - git also appends a disambiguating tab
            // after unquoted paths that contain whitespace, which must be trimmed.
            if (line.StartsWith("--- ", StringComparison.Ordinal))
            {
                var oldPath = UnquotePath(line[4..].TrimEnd('\t'));
                if (oldPath != "/dev/null" && current.Status == DiffFileStatus.Deleted)
                    current.Path = StripPrefix(oldPath, "a/");
                continue;
            }
            if (line.StartsWith("+++ ", StringComparison.Ordinal))
            {
                var newPath = UnquotePath(line[4..].TrimEnd('\t'));
                if (newPath != "/dev/null" && current.Status != DiffFileStatus.Renamed)
                    current.Path = StripPrefix(newPath, "b/");
                continue;
            }

            // Skip remaining diff metadata lines
            if (line.StartsWith("index ", StringComparison.Ordinal) ||
                line.StartsWith("old mode", StringComparison.Ordinal) ||
                line.StartsWith("new mode", StringComparison.Ordinal) ||
                line.StartsWith("similarity index", StringComparison.Ordinal) ||
                line.StartsWith("copy from", StringComparison.Ordinal) ||
                line.StartsWith("copy to", StringComparison.Ordinal))
                continue;

            if (line.StartsWith('+'))
            {
                current.Lines.Add(new DiffLineViewModel
                {
                    Kind = DiffLineKind.Add,
                    NewNo = newLine++,
                    Text = line.Length > 1 ? line[1..] : "",
                });
                current.Additions++;
            }
            else if (line.StartsWith('-'))
            {
                current.Lines.Add(new DiffLineViewModel
                {
                    Kind = DiffLineKind.Del,
                    OldNo = oldLine++,
                    Text = line.Length > 1 ? line[1..] : "",
                });
                current.Deletions++;
            }
            else if (line.StartsWith(' '))
            {
                current.Lines.Add(new DiffLineViewModel
                {
                    Kind = DiffLineKind.Ctx,
                    OldNo = oldLine++,
                    NewNo = newLine++,
                    Text = line.Length > 1 ? line[1..] : "",
                });
            }
        }

        return files;
    }

    private static void ParseHunkHeader(string header, out int oldStart, out int newStart)
    {
        oldStart = 1; newStart = 1;
        // Format: @@ -<old>,<count> +<new>,<count> @@
        var at = header.IndexOf("@@", 3, StringComparison.Ordinal);
        var inner = at > 0 ? header[3..at].Trim() : header;
        var segs = inner.Split(' ');
        foreach (var seg in segs)
        {
            if (seg.StartsWith('-') && int.TryParse(seg[1..].Split(',')[0], out var o))
                oldStart = o;
            else if (seg.StartsWith('+') && int.TryParse(seg[1..].Split(',')[0], out var n))
                newStart = n;
        }
    }

    /// Splits a "diff --git" header's remainder ("a/&lt;old&gt; b/&lt;new&gt;", where
    /// either side may be individually git-quoted) into its two paths.
    private static (string OldPath, string NewPath) SplitGitHeaderPaths(string content)
    {
        string oldToken, newToken;

        if (content.Length > 0 && content[0] == '"')
        {
            var close = FindClosingQuote(content, 0);
            oldToken = content[..(close + 1)];
            newToken = content[(close + 1)..].TrimStart(' ');
        }
        else
        {
            var quoteAt = content.IndexOf('"');
            if (quoteAt >= 0)
            {
                // Mixed quoting: an unquoted path never contains a literal quote
                // (git would have quoted it), so the quote marks the other side.
                oldToken = content[..quoteAt].TrimEnd(' ');
                newToken = content[quoteAt..];
            }
            else
            {
                var splitAt = content.IndexOf(" b/", StringComparison.Ordinal);
                oldToken = splitAt < 0 ? content : content[..splitAt];
                newToken = splitAt < 0 ? content : content[(splitAt + 1)..];
            }
        }

        return (StripPrefix(UnquotePath(oldToken), "a/"), StripPrefix(UnquotePath(newToken), "b/"));
    }

    private static int FindClosingQuote(string s, int start)
    {
        var i = start + 1;
        while (i < s.Length)
        {
            if (s[i] == '\\') { i += 2; continue; }
            if (s[i] == '"') return i;
            i++;
        }
        return s.Length - 1;
    }

    private static string StripPrefix(string path, string prefix) =>
        path.StartsWith(prefix, StringComparison.Ordinal) ? path[prefix.Length..] : path;

    /// Reverses git's C-style path quoting: a quoted token is wrapped in double
    /// quotes with '\\', '"' and non-ASCII bytes escaped as \nnn octal sequences
    /// (used whenever core.quotePath would otherwise consider the path "unusual").
    private static string UnquotePath(string token)
    {
        if (token.Length < 2 || token[0] != '"' || token[^1] != '"')
            return token;

        var bytes = new List<byte>();
        var end = token.Length - 1;
        var i = 1;
        while (i < end)
        {
            var c = token[i];
            if (c != '\\')
            {
                bytes.Add((byte)c);
                i++;
                continue;
            }

            i++;
            if (i >= end) break;
            switch (token[i])
            {
                case '"': bytes.Add((byte)'"'); i++; break;
                case '\\': bytes.Add((byte)'\\'); i++; break;
                case 'a': bytes.Add(0x07); i++; break;
                case 'b': bytes.Add(0x08); i++; break;
                case 'f': bytes.Add(0x0C); i++; break;
                case 'n': bytes.Add((byte)'\n'); i++; break;
                case 'r': bytes.Add((byte)'\r'); i++; break;
                case 't': bytes.Add((byte)'\t'); i++; break;
                case 'v': bytes.Add(0x0B); i++; break;
                default:
                    if (token[i] is >= '0' and <= '7')
                    {
                        var value = 0;
                        var digits = 0;
                        while (digits < 3 && i < end && token[i] is >= '0' and <= '7')
                        {
                            value = value * 8 + (token[i] - '0');
                            i++;
                            digits++;
                        }
                        bytes.Add((byte)value);
                    }
                    else
                    {
                        bytes.Add((byte)token[i]);
                        i++;
                    }
                    break;
            }
        }

        return Encoding.UTF8.GetString(bytes.ToArray());
    }
}
