using System.Diagnostics;
using System.Text;

namespace ClaudeDo.Worker.Lifecycle;

/// <summary>
/// Runs a list's configured post-merge verification command (e.g. a build/test invocation)
/// via cmd.exe, mirroring GitService's ProcessStartInfo discipline (no shell string
/// concatenation beyond the single /c argument cmd.exe itself requires to parse a command line).
/// </summary>
public sealed class VerifyCommandRunner : IVerifyCommandRunner
{
    // Safety cap so a runaway/chatty command can't exhaust memory; only the tail matters anyway.
    private const int MaxOutputChars = 512_000;

    public async Task<VerifyCommandResult> RunAsync(
        string workingDir, string command, TimeSpan timeout, CancellationToken ct)
    {
        var psi = new ProcessStartInfo
        {
            FileName = "cmd.exe",
            WorkingDirectory = workingDir,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true,
            StandardOutputEncoding = Encoding.UTF8,
            StandardErrorEncoding = Encoding.UTF8,
        };
        psi.ArgumentList.Add("/c");
        psi.ArgumentList.Add(command);

        using var process = new Process { StartInfo = psi };
        var output = new StringBuilder();
        var sync = new object();

        void Append(string? line)
        {
            if (line is null) return;
            lock (sync)
            {
                if (output.Length >= MaxOutputChars) return;
                output.AppendLine(line);
            }
        }

        process.OutputDataReceived += (_, e) => Append(e.Data);
        process.ErrorDataReceived += (_, e) => Append(e.Data);

        process.Start();
        process.BeginOutputReadLine();
        process.BeginErrorReadLine();

        using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        cts.CancelAfter(timeout);

        var timedOut = false;

        // On cancellation (timeout or caller): kill the tree. Killing unblocks WaitForExitAsync
        // below and lets the async output readers drain/complete naturally.
        await using var ctr = cts.Token.Register(() =>
        {
            timedOut = !ct.IsCancellationRequested;
            try { process.Kill(entireProcessTree: true); }
            catch { /* already exited */ }
        });

        await process.WaitForExitAsync(CancellationToken.None);

        string finalOutput;
        lock (sync) finalOutput = output.ToString();

        return new VerifyCommandResult(process.ExitCode, timedOut, finalOutput);
    }
}
