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

namespace ClaudeDo.Worker.Runner;

public sealed class ClaudeProcess : IClaudeProcess
{
    public const string NoResultPrefix = "Claude exited with code";

    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(
        IReadOnlyList<string> arguments,
        string prompt,
        string workingDirectory,
        Func<string, Task> onStdoutLine,
        CancellationToken ct)
    {
        var resolved = ExecutableResolver.Resolve(_cfg.ClaudeBin)
            ?? throw new InvalidOperationException($"'{_cfg.ClaudeBin}' not found on PATH.");

        var psi = new ProcessStartInfo
        {
            WorkingDirectory = workingDirectory,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true,
            StandardOutputEncoding = Encoding.UTF8,
            StandardErrorEncoding = Encoding.UTF8,
        };

        // A shim (.cmd/.bat) can't be launched directly with UseShellExecute=false;
        // route it through cmd.exe /c so the real interpreter starts the target.
        if (resolved.IsShim)
        {
            psi.FileName = "cmd.exe";
            psi.ArgumentList.Add("/c");
            psi.ArgumentList.Add(resolved.Path);
        }
        else
        {
            psi.FileName = resolved.Path;
        }

        foreach (var arg in arguments)
            psi.ArgumentList.Add(arg);

        // Claude Code caps HTTP MCP tool calls at 60 s unless MCP_TOOL_TIMEOUT is raised.
        // wait_for_task_change blocks up to TaskWaitMcpTools.MaxTimeoutSeconds (900 s) and the
        // in-task AskUser tool blocks up to 3 min waiting for the user, so lift the cap well
        // past the longer of the two (with margin) or that wait would be killed early. Harmless
        // for every other tool. Shares the value with InteractiveLaunchSpecService.McpToolTimeoutMs.
        psi.Environment["MCP_TOOL_TIMEOUT"] = InteractiveLaunchSpecService.McpToolTimeoutMs;

        using var process = new Process { StartInfo = psi };
        process.Start();
        ProcessJobObject.Assign(process, _logger);

        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,
                Blocks = streamResult.Blocks,
                ResultSubtype = streamResult.ResultSubtype,
                TerminalReason = streamResult.TerminalReason,
                Errors = streamResult.Errors,
                PermissionDenials = streamResult.PermissionDenials,
            };
        }

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

        return new RunResult
        {
            ExitCode = exitCode,
            // Kept even on failure: a terminal reason like api_error often carries the
            // provider's own message (e.g. session-limit + reset time) in this field,
            // with nothing useful on stderr.
            ResultMarkdown = streamResult.ResultMarkdown,
            ErrorMarkdown = error,
            SessionId = streamResult.SessionId,
            TurnCount = streamResult.TurnCount,
            TokensIn = streamResult.TokensIn,
            TokensOut = streamResult.TokensOut,
            Blocks = streamResult.Blocks,
            ResultSubtype = streamResult.ResultSubtype,
            TerminalReason = streamResult.TerminalReason,
            Errors = streamResult.Errors,
            PermissionDenials = streamResult.PermissionDenials,
        };
    }
}
