using System.Diagnostics;
using System.Text;

namespace ClaudeDo.Data.Git;

public sealed class GitService
{
    public async Task<bool> IsGitRepoAsync(string dir, CancellationToken ct = default)
    {
        var (exitCode, _, _) = await RunGitAsync(dir, ["rev-parse", "--git-dir"], ct);
        return exitCode == 0;
    }

    public async Task<string> RevParseHeadAsync(string dir, CancellationToken ct = default)
    {
        var (exitCode, stdout, stderr) = await RunGitAsync(dir, ["rev-parse", "HEAD"], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git rev-parse HEAD failed (exit {exitCode}): {stderr}");
        return stdout.Trim();
    }

    public async Task WorktreeAddAsync(string repoDir, string branchName, string worktreePath, string baseCommit, CancellationToken ct = default)
    {
        var (exitCode, _, stderr) = await RunGitAsync(repoDir,
            ["worktree", "add", "-b", branchName, worktreePath, baseCommit], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git worktree add failed (exit {exitCode}): {stderr}");
    }

    public async Task<string> GetStatusPorcelainAsync(string workingDirectory, CancellationToken ct = default)
    {
        var (exitCode, stdout, stderr) = await RunGitAsync(workingDirectory, ["status", "--porcelain"], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git status --porcelain failed (exit {exitCode}): {stderr}");
        return stdout;
    }

    public async Task<string> GetCommittedFilesAsync(string worktreePath, string baseCommit, CancellationToken ct = default)
    {
        var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath,
            ["diff", "--name-status", $"{baseCommit}..HEAD"], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git diff --name-status failed (exit {exitCode}): {stderr}");
        return stdout;
    }

    public async Task<bool> HasChangesAsync(string worktreePath, CancellationToken ct = default)
    {
        var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, ["status", "--porcelain"], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git status --porcelain failed (exit {exitCode}): {stderr}");
        return !string.IsNullOrWhiteSpace(stdout);
    }

    public async Task AddAllAsync(string worktreePath, CancellationToken ct = default)
    {
        var (exitCode, _, stderr) = await RunGitAsync(worktreePath, ["add", "-A"], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git add -A failed (exit {exitCode}): {stderr}");
    }

    public async Task CommitAsync(string worktreePath, string message, CancellationToken ct = default)
    {
        // Use -F - (read message from stdin) to handle multi-line messages safely.
        var (exitCode, _, stderr) = await RunGitAsync(worktreePath, ["commit", "-F", "-"], ct, stdinData: message);
        if (exitCode != 0)
            throw new InvalidOperationException($"git commit failed (exit {exitCode}): {stderr}");
    }

    public async Task<string> GetDiffAsync(string worktreePath, CancellationToken ct = default)
    {
        var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath,
            ["diff", "HEAD"], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git diff HEAD failed (exit {exitCode}): {stderr}");
        // If nothing staged vs HEAD, try the index (untracked is never in diff)
        if (string.IsNullOrWhiteSpace(stdout))
        {
            var (e2, s2, _) = await RunGitAsync(worktreePath, ["diff", "--cached"], ct);
            if (e2 == 0) return s2;
        }
        return stdout;
    }

    /// <summary>
    /// Full diff between <paramref name="baseRef"/> and the current working tree
    /// (committed-on-branch changes + uncommitted work). Used for viewing a Claude
    /// task's total impact relative to where the branch started.
    /// </summary>
    public async Task<string> GetBranchDiffAsync(string worktreePath, string baseRef, CancellationToken ct = default)
    {
        var (exitCode, stdout, _) = await RunGitAsync(worktreePath,
            ["diff", baseRef], ct);
        if (exitCode == 0 && !string.IsNullOrWhiteSpace(stdout))
            return stdout;
        // Fallback: whatever the worktree has vs HEAD (uncommitted only).
        return await GetDiffAsync(worktreePath, ct);
    }

    public async Task<string> DiffStatAsync(string worktreePath, string baseCommit, string headCommit, CancellationToken ct = default)
    {
        var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath,
            ["diff", "--stat", $"{baseCommit}..{headCommit}"], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git diff --stat failed (exit {exitCode}): {stderr}");
        return stdout.Trim();
    }

    public async Task<string> GetFileDiffAsync(string worktreePath, string? baseCommit, string relativePath, CancellationToken ct = default)
    {
        string[] args = string.IsNullOrEmpty(baseCommit)
            ? ["diff", "--", relativePath]
            : ["diff", $"{baseCommit}..HEAD", "--", relativePath];
        var (_, stdout, _) = await RunGitAsync(worktreePath, args, ct);
        return stdout;
    }

    public async Task WorktreeRemoveAsync(string repoDir, string worktreePath, bool force = false, CancellationToken ct = default)
    {
        var args = new List<string> { "worktree", "remove" };
        if (force) args.Add("--force");
        args.Add(worktreePath);

        var (exitCode, _, stderr) = await RunGitAsync(repoDir, args, ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git worktree remove failed (exit {exitCode}): {stderr}");
    }

    public async Task<List<string>> ListWorktreePathsForBranchAsync(string repoDir, string branchName, CancellationToken ct = default)
    {
        var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["worktree", "list", "--porcelain"], ct);
        if (exitCode != 0) return new();

        var target = $"refs/heads/{branchName}";
        var paths = new List<string>();
        string? currentPath = null;
        foreach (var raw in stdout.Split('\n'))
        {
            var line = raw.TrimEnd('\r');
            if (line.StartsWith("worktree ", StringComparison.Ordinal))
            {
                currentPath = line["worktree ".Length..].Trim();
            }
            else if (line.StartsWith("branch ", StringComparison.Ordinal))
            {
                var b = line["branch ".Length..].Trim();
                if (b == target && currentPath is not null) paths.Add(currentPath);
            }
            else if (string.IsNullOrWhiteSpace(line))
            {
                currentPath = null;
            }
        }
        return paths;
    }

    public async Task WorktreePruneAsync(string repoDir, CancellationToken ct = default)
    {
        var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["worktree", "prune"], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git worktree prune failed (exit {exitCode}): {stderr}");
    }

    public async Task BranchDeleteAsync(string repoDir, string branchName, bool force = false, CancellationToken ct = default)
    {
        var flag = force ? "-D" : "-d";
        var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["branch", flag, branchName], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git branch {flag} failed (exit {exitCode}): {stderr}");
    }

    public async Task<string> GetCurrentBranchAsync(string repoDir, CancellationToken ct = default)
    {
        var (exitCode, stdout, stderr) = await RunGitAsync(repoDir,
            ["symbolic-ref", "--short", "HEAD"], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git symbolic-ref --short HEAD failed (exit {exitCode}): {stderr}");
        return stdout.Trim();
    }

    public async Task CheckoutBranchAsync(string repoDir, string branchName, CancellationToken ct = default)
    {
        var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["checkout", branchName], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git checkout '{branchName}' failed (exit {exitCode}): {stderr}");
    }

    public async Task<List<string>> ListLocalBranchesAsync(string repoDir, CancellationToken ct = default)
    {
        var (exitCode, stdout, stderr) = await RunGitAsync(repoDir,
            ["branch", "--format=%(refname:short)"], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git branch --format failed (exit {exitCode}): {stderr}");

        return stdout
            .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Where(s => s.Length > 0)
            .ToList();
    }

    public async Task<bool> IsMidMergeAsync(string repoDir, CancellationToken ct = default)
    {
        var (exitCode, stdout, _) = await RunGitAsync(repoDir, ["rev-parse", "--git-dir"], ct);
        if (exitCode != 0) return false;
        var gitDir = stdout.Trim();
        if (!Path.IsPathRooted(gitDir))
            gitDir = Path.Combine(repoDir, gitDir);
        return File.Exists(Path.Combine(gitDir, "MERGE_HEAD"));
    }

    public async Task<(int ExitCode, string Stderr)> MergeNoFfAsync(
        string repoDir, string sourceBranch, string message, CancellationToken ct = default)
    {
        var (exitCode, _, stderr) = await RunGitAsync(repoDir,
            ["merge", "--no-ff", "-m", message, sourceBranch], ct);
        return (exitCode, stderr);
    }

    public async Task MergeAbortAsync(string repoDir, CancellationToken ct = default)
    {
        var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["merge", "--abort"], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git merge --abort failed (exit {exitCode}): {stderr}");
    }

    public async Task<List<string>> ListConflictedFilesAsync(string repoDir, CancellationToken ct = default)
    {
        var (exitCode, stdout, stderr) = await RunGitAsync(repoDir,
            ["diff", "--name-only", "--diff-filter=U"], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"git diff --diff-filter=U failed (exit {exitCode}): {stderr}");

        return stdout
            .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Where(s => s.Length > 0)
            .ToList();
    }

    public async Task MergeFfOnlyAsync(string repoDir, string branchName, CancellationToken ct = default)
    {
        var (exitCode, _, stderr) = await RunGitAsync(repoDir, ["merge", "--ff-only", branchName], ct);
        if (exitCode != 0)
            throw new InvalidOperationException($"Fast-forward merge of '{branchName}' failed. Manual merge required. git stderr: {stderr}");
    }

    private static async Task<(int ExitCode, string Stdout, string Stderr)> RunGitAsync(
        string workDir, IEnumerable<string> args, CancellationToken ct, string? stdinData = null)
    {
        var psi = new ProcessStartInfo
        {
            FileName = "git",
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            RedirectStandardInput = stdinData is not null,
            UseShellExecute = false,
            CreateNoWindow = true,
            StandardOutputEncoding = Encoding.UTF8,
            StandardErrorEncoding = Encoding.UTF8,
            StandardInputEncoding = stdinData is not null ? Encoding.UTF8 : null,
        };
        psi.ArgumentList.Add("-C");
        psi.ArgumentList.Add(workDir);
        foreach (var a in args) psi.ArgumentList.Add(a);

        using var proc = new Process { StartInfo = psi };
        proc.Start();

        // On cancellation: kill the git process tree. Killing closes the
        // redirected pipes, which unblocks the ReadToEndAsync calls below
        // and lets WaitForExitAsync return so the process is reaped.
        // Without this, cancelling mid-git leaves zombie processes.
        await using var ctr = ct.Register(() =>
        {
            try { proc.Kill(entireProcessTree: true); }
            catch { /* already exited */ }
        });

        if (stdinData is not null)
        {
            await proc.StandardInput.WriteAsync(stdinData.AsMemory(), ct);
            proc.StandardInput.Close();
        }

        // Drain output without ct — pipes close when the process exits
        // (whether naturally or via Kill above), so these always complete.
        var stdoutTask = proc.StandardOutput.ReadToEndAsync();
        var stderrTask = proc.StandardError.ReadToEndAsync();

        await proc.WaitForExitAsync(CancellationToken.None);

        var stdout = await stdoutTask;
        var stderr = await stderrTask;

        ct.ThrowIfCancellationRequested();

        return (proc.ExitCode, stdout.TrimEnd(), stderr.TrimEnd());
    }
}
