using System.Collections.Concurrent;
using ClaudeDo.Data.Git;

namespace ClaudeDo.Worker.Git;

public sealed record DirtyBaseWarning(int ModifiedCount, int UntrackedCount);

/// <summary>
/// Detects uncommitted changes in a list's working directory before a task forks a worktree
/// from HEAD. WorktreeManager.ResolveBaseCommitAsync forks from the commit tip, never the
/// working tree, so local edits sitting there are silently invisible to every task queued
/// against that list -- each one starts without them and can collide re-creating the same
/// files. Non-blocking by design: callers surface the counts as a heads-up, never refuse the
/// transition.
/// </summary>
public sealed class BaseDirtyChecker
{
    // Long enough to collapse every task in one batch_update_task_status call over the same
    // list into a single `git status` invocation; short enough that a later, independent queue
    // action re-checks instead of trusting a stale answer.
    private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(10);

    private readonly GitService _git;
    private readonly ILogger<BaseDirtyChecker> _logger;
    private readonly ConcurrentDictionary<string, (DateTime ExpiresAtUtc, DirtyBaseWarning? Warning)> _cache = new();

    public BaseDirtyChecker(GitService git, ILogger<BaseDirtyChecker> logger)
    {
        _git = git;
        _logger = logger;
    }

    public async Task<DirtyBaseWarning?> CheckAsync(string? workingDir, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(workingDir))
            return null;

        string key;
        try { key = Path.GetFullPath(workingDir); }
        catch { return null; }

        if (_cache.TryGetValue(key, out var cached) && cached.ExpiresAtUtc > DateTime.UtcNow)
            return cached.Warning;

        var warning = await ComputeAsync(key, ct);
        _cache[key] = (DateTime.UtcNow.Add(CacheTtl), warning);
        return warning;
    }

    private async Task<DirtyBaseWarning?> ComputeAsync(string workingDir, CancellationToken ct)
    {
        try
        {
            if (!await _git.IsGitRepoAsync(workingDir, ct))
                return null;

            var porcelain = await _git.GetStatusPorcelainAsync(workingDir, ct);
            return Parse(porcelain);
        }
        catch (OperationCanceledException) { throw; }
        catch (Exception ex)
        {
            // Never blocks a queue/run transition on a check that only exists to warn.
            _logger.LogWarning(ex, "Base-dirty check failed for {WorkingDir}", workingDir);
            return null;
        }
    }

    private static DirtyBaseWarning? Parse(string porcelain)
    {
        if (string.IsNullOrWhiteSpace(porcelain)) return null;

        int modified = 0, untracked = 0;
        foreach (var raw in porcelain.Split('\n', StringSplitOptions.RemoveEmptyEntries))
        {
            var line = raw.TrimEnd('\r');
            if (line.Length == 0) continue;
            if (line.StartsWith("??", StringComparison.Ordinal)) untracked++;
            else modified++;
        }
        return modified == 0 && untracked == 0 ? null : new DirtyBaseWarning(modified, untracked);
    }
}
