using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;

namespace ClaudeDo.Worker.Planning;

public sealed record SubtaskDiff(
    string SubtaskId,
    string Title,
    string BranchName,
    string BaseCommit,
    string HeadCommit,
    string? DiffStat,
    string UnifiedDiff);

public sealed record CombinedDiffSuccess(string IntegrationBranch, string UnifiedDiff);
public sealed record CombinedDiffFailure(string FirstConflictSubtaskId, IReadOnlyList<string> ConflictedFiles);

public abstract record CombinedDiffResult
{
    public sealed record Ok(CombinedDiffSuccess Value) : CombinedDiffResult;
    public sealed record Failed(CombinedDiffFailure Value) : CombinedDiffResult;
}

public sealed class PlanningAggregator
{
    private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
    private readonly GitService _git;
    private readonly ILogger<PlanningAggregator> _logger;

    public PlanningAggregator(
        IDbContextFactory<ClaudeDoDbContext> dbFactory,
        GitService git,
        ILogger<PlanningAggregator> logger)
    {
        _dbFactory = dbFactory;
        _git = git;
        _logger = logger;
    }

    public async Task<IReadOnlyList<SubtaskDiff>> GetAggregatedDiffAsync(
        string planningTaskId, CancellationToken ct)
    {
        using var ctx = _dbFactory.CreateDbContext();
        var parent = await ctx.Tasks
            .Include(t => t.Worktree)
            .Include(t => t.Children).ThenInclude(c => c.Worktree)
            .SingleOrDefaultAsync(t => t.Id == planningTaskId, ct);
        if (parent is null) return new List<SubtaskDiff>();

        var nodes = new List<TaskEntity>();
        // An improvement parent carries its own code branch — fold it in first so the
        // combined diff matches what the tree-merge will produce.
        if (parent.PlanningPhase == PlanningPhase.None && parent.Worktree is { State: WorktreeState.Active })
            nodes.Add(parent);
        nodes.AddRange(parent.Children.OrderBy(c => c.SortOrder));

        var result = new List<SubtaskDiff>();
        foreach (var node in nodes)
        {
            if (node.Worktree is null) continue;
            var wt = node.Worktree;
            var head = wt.HeadCommit ?? await _git.RevParseHeadAsync(wt.Path, ct);
            string unified;
            try
            {
                unified = await _git.GetBranchDiffAsync(wt.Path, wt.BaseCommit, ct: ct);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "diff failed for node {Id}", node.Id);
                unified = "";
            }
            result.Add(new SubtaskDiff(
                node.Id, node.Title, wt.BranchName, wt.BaseCommit, head, wt.DiffStat, unified));
        }
        return result;
    }

    public async Task<CombinedDiffResult> BuildIntegrationBranchAsync(
        string planningTaskId, string targetBranch, CancellationToken ct)
    {
        var (planning, repoDir, childSubtasks) = await LoadPlanningContextAsync(planningTaskId, ct);

        var integrationBranch = BuildIntegrationBranchName(planning);

        // Reset: checkout target first (so we're never ON the integration branch when deleting it),
        // then delete if exists, then recreate off the target branch.
        await _git.CheckoutBranchAsync(repoDir, targetBranch, ct);

        try { await _git.BranchDeleteAsync(repoDir, integrationBranch, force: true, ct); }
        catch { /* didn't exist */ }

        await GitRawAsync(repoDir, ct, "checkout", "-b", integrationBranch);

        var nodes = new List<TaskEntity>();
        // Fold the improvement parent's own branch in first (planning parents have none).
        if (planning.PlanningPhase == PlanningPhase.None && planning.Worktree is { State: WorktreeState.Active })
            nodes.Add(planning);
        nodes.AddRange(childSubtasks);

        foreach (var node in nodes)
        {
            if (node.Worktree is null) continue;
            var (code, _) = await _git.MergeNoFfAsync(
                repoDir, node.Worktree.BranchName,
                $"Integrate: {node.Title}", ct);
            if (code != 0)
            {
                List<string> files;
                try { files = await _git.ListConflictedFilesAsync(repoDir, ct); }
                catch { files = new(); }

                try { await _git.MergeAbortAsync(repoDir, ct); } catch { }
                try { await _git.CheckoutBranchAsync(repoDir, targetBranch, ct); } catch { }
                try { await _git.BranchDeleteAsync(repoDir, integrationBranch, force: true, ct); } catch { }

                return new CombinedDiffResult.Failed(
                    new CombinedDiffFailure(node.Id, files));
            }
        }

        var unifiedDiff = await GitRawAsync(repoDir, ct, "diff", $"{targetBranch}..{integrationBranch}");
        return new CombinedDiffResult.Ok(new CombinedDiffSuccess(integrationBranch, unifiedDiff));
    }

    public async Task CleanupIntegrationBranchAsync(string planningTaskId, CancellationToken ct)
    {
        var (planning, repoDir, _) = await LoadPlanningContextAsync(planningTaskId, ct);
        var branch = BuildIntegrationBranchName(planning);

        var current = await _git.GetCurrentBranchAsync(repoDir, ct);
        if (string.Equals(current, branch, StringComparison.Ordinal))
        {
            var branches = await _git.ListLocalBranchesAsync(repoDir, ct);
            var target = branches.FirstOrDefault(b => b != branch && !b.StartsWith("claudedo/", StringComparison.Ordinal)) ?? "main";
            await _git.CheckoutBranchAsync(repoDir, target, ct);
        }

        try { await _git.BranchDeleteAsync(repoDir, branch, force: true, ct); }
        catch { /* already gone — idempotent */ }
    }

    private async Task<(TaskEntity planning, string repoDir, IReadOnlyList<TaskEntity> children)>
        LoadPlanningContextAsync(string planningTaskId, CancellationToken ct)
    {
        using var ctx = _dbFactory.CreateDbContext();
        var planning = await ctx.Tasks
            .Include(t => t.List)
            .Include(t => t.Worktree)
            .Include(t => t.Children).ThenInclude(c => c.Worktree)
            .SingleOrDefaultAsync(t => t.Id == planningTaskId, ct)
            ?? throw new KeyNotFoundException($"Planning task '{planningTaskId}' not found.");
        var repoDir = planning.List.WorkingDir
            ?? throw new InvalidOperationException("List has no working directory.");
        var children = planning.Children.OrderBy(c => c.SortOrder).ToList();
        return (planning, repoDir, children);
    }

    internal static string BuildIntegrationBranchName(TaskEntity planning)
    {
        var slug = new string(planning.Title
            .ToLowerInvariant()
            .Select(c => char.IsLetterOrDigit(c) ? c : '-')
            .ToArray())
            .Trim('-');
        if (string.IsNullOrEmpty(slug)) slug = planning.Id[..8];
        if (slug.Length > 40) slug = slug[..40].TrimEnd('-');
        return $"planning/{slug}-integration";
    }

    private static async Task<string> GitRawAsync(string cwd, CancellationToken ct, params string[] args)
    {
        var psi = new System.Diagnostics.ProcessStartInfo("git")
        {
            WorkingDirectory = cwd,
            RedirectStandardOutput = true,
            RedirectStandardError  = true,
            UseShellExecute = false,
        };
        foreach (var a in args) psi.ArgumentList.Add(a);
        using var p = System.Diagnostics.Process.Start(psi)!;
        var stdoutTask = p.StandardOutput.ReadToEndAsync();
        var stderrTask = p.StandardError.ReadToEndAsync();
        try
        {
            await p.WaitForExitAsync(ct);
        }
        catch (OperationCanceledException)
        {
            try { if (!p.HasExited) p.Kill(entireProcessTree: true); } catch { }
            throw;
        }
        var stdout = await stdoutTask;
        var stderr = await stderrTask;
        if (p.ExitCode != 0) throw new InvalidOperationException($"git {string.Join(' ', args)} failed: {stderr}");
        return stdout;
    }
}
