using System.Diagnostics;
using System.Text;
using ClaudeDo.Worker.Config;

namespace ClaudeDo.Worker.Runner;

public sealed class ClaudeProcess : IClaudeProcess
{
    private readonly WorkerConfig _cfg;
    private readonly ILogger<ClaudeProcess> _logger;

    public ClaudeProcess(WorkerConfig cfg, ILogger<ClaudeProcess> logger)
    {
        _cfg = cfg;
        _logger = logger;
    }

    public async Task<RunResult> RunAsync(
        string arguments,
        string prompt,
        string workingDirectory,
        Func<string, Task> onStdoutLine,
        CancellationToken ct)
    {
        var psi = new ProcessStartInfo
        {
            FileName = _cfg.ClaudeBin,
            Arguments = arguments,
            WorkingDirectory = workingDirectory,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true,
            StandardOutputEncoding = Encoding.UTF8,
            StandardErrorEncoding = Encoding.UTF8,
        };

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

        await process.StandardInput.WriteAsync(prompt);
        process.StandardInput.Close();

        var analyzer = new StreamAnalyzer();
        var lastStderr = new StringBuilder();

        // On cancellation: kill the tree. Killing closes the redirected pipes,
        // which unblocks the ReadLineAsync loops below (which run without ct
        // so they reliably drain instead of hanging on cancellation).
        await using var ctr = ct.Register(() =>
        {
            try { process.Kill(entireProcessTree: true); }
            catch { /* already exited */ }
        });

        var stdoutTask = Task.Run(async () =>
        {
            while (await process.StandardOutput.ReadLineAsync() is { } line)
            {
                if (string.IsNullOrEmpty(line)) continue;
                await onStdoutLine(line);
                analyzer.ProcessLine(line);
            }
        });

        var stderrTask = Task.Run(async () =>
        {
            while (await process.StandardError.ReadLineAsync() is { } line)
            {
                if (string.IsNullOrEmpty(line)) continue;
                lastStderr.AppendLine(line);
                await onStdoutLine($"[stderr] {line}");
            }
        });

        await Task.WhenAll(stdoutTask, stderrTask);
        await process.WaitForExitAsync(CancellationToken.None);

        // If we were asked to cancel, surface that to the caller now that
        // the process is fully reaped.
        ct.ThrowIfCancellationRequested();

        var exitCode = process.ExitCode;
        var streamResult = analyzer.GetResult();

        if (exitCode == 0 && streamResult.ResultMarkdown is not null)
        {
            return new RunResult
            {
                ExitCode = exitCode,
                ResultMarkdown = streamResult.ResultMarkdown,
                StructuredOutputJson = streamResult.StructuredOutputJson,
                SessionId = streamResult.SessionId,
                TurnCount = streamResult.TurnCount,
                TokensIn = streamResult.TokensIn,
                TokensOut = streamResult.TokensOut,
            };
        }

        var error = lastStderr.Length > 0
            ? lastStderr.ToString().Trim()
            : $"Claude exited with code {exitCode} and no result.";

        return new RunResult
        {
            ExitCode = exitCode,
            ErrorMarkdown = error,
            SessionId = streamResult.SessionId,
            TurnCount = streamResult.TurnCount,
            TokensIn = streamResult.TokensIn,
            TokensOut = streamResult.TokensOut,
        };
    }
}
