using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Runner;
using Microsoft.EntityFrameworkCore;

namespace ClaudeDo.Worker.Prime;

public sealed class PrimeRunner : IPrimeRunner
{
    private static readonly TimeSpan FireTimeout = TimeSpan.FromMinutes(5);
    private const int MaxTurns = 30;

    private readonly IClaudeProcess _claude;
    private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
    private readonly IPrimeClock _clock;
    private readonly ILogger<PrimeRunner> _logger;
    private readonly IPrimeBroadcaster _broadcaster;
    private readonly SemaphoreSlim _gate = new(1, 1);

    public PrimeRunner(
        IClaudeProcess claude,
        IDbContextFactory<ClaudeDoDbContext> dbFactory,
        IPrimeClock clock,
        ILogger<PrimeRunner> logger,
        IPrimeBroadcaster broadcaster)
    {
        _claude = claude;
        _dbFactory = dbFactory;
        _clock = clock;
        _logger = logger;
        _broadcaster = broadcaster;
    }

    public async Task<PrimeRunOutcome> FireAsync(PrimeScheduleDto schedule, CancellationToken ct)
    {
        // One gate for all kinds — two schedules must never run at the same time.
        if (!await _gate.WaitAsync(0, ct))
            return new PrimeRunOutcome(false, "Prime run already running");

        try
        {
            return schedule.Kind switch
            {
                PrimeActionKind.FillMyDay => await RunFillMyDayAsync(schedule, ct),
                PrimeActionKind.Custom => await RunCustomAsync(schedule, ct),
                _ => await RunPingAsync(ct),
            };
        }
        finally
        {
            _gate.Release();
        }
    }

    private async Task<PrimeRunOutcome> RunFillMyDayAsync(PrimeScheduleDto schedule, CancellationToken ct)
    {
        var success = false;
        try
        {
            var logPath = DailyPrepPrompt.LogPath();
            try { if (File.Exists(logPath)) File.Delete(logPath); } catch { /* best effort */ }
            await using var logWriter = new LogWriter(logPath);

            await _broadcaster.PrepStartedAsync();

            var cwd = Cwd();

            int maxTasks;
            await using (var dbCtx = await _dbFactory.CreateDbContextAsync(ct))
            {
                var settings = await new AppSettingsRepository(dbCtx).GetAsync(ct);
                maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
            }

            var today = DateOnly.FromDateTime(_clock.Now.LocalDateTime);
            var prompt = DailyPrepPrompt.BuildPrompt(maxTasks, today, schedule.PromptOverride);

            using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            timeoutCts.CancelAfter(FireTimeout);

            var result = await _claude.RunAsync(
                arguments: DailyPrepPrompt.BuildArgs(MaxTurns),
                prompt: prompt,
                workingDirectory: cwd,
                onStdoutLine: async line =>
                {
                    await logWriter.WriteLineAsync(line);
                    await _broadcaster.PrepLineAsync(line);
                },
                ct: timeoutCts.Token);

            success = result.IsSuccess;
            return success
                ? new PrimeRunOutcome(true, "Daily prep complete")
                : new PrimeRunOutcome(false, $"exit code {result.ExitCode}");
        }
        catch (OperationCanceledException) when (!ct.IsCancellationRequested)
        {
            return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalMinutes:0} min");
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Daily prep run failed");
            return new PrimeRunOutcome(false, ex.Message);
        }
        finally
        {
            await _broadcaster.PrepFinishedAsync(success);
        }
    }

    private Task<PrimeRunOutcome> RunPingAsync(CancellationToken ct) =>
        RunQuietAsync(PrimePrompts.BuildPingArgs(), PrimePrompts.PingPrompt, "Ping complete", "Ping", ct);

    private Task<PrimeRunOutcome> RunCustomAsync(PrimeScheduleDto schedule, CancellationToken ct)
    {
        var prompt = schedule.PromptOverride;
        if (string.IsNullOrWhiteSpace(prompt))
            return Task.FromResult(new PrimeRunOutcome(false, "Custom schedule has no prompt"));

        return RunQuietAsync(PrimePrompts.BuildCustomArgs(MaxTurns), prompt, "Custom run complete", "Custom", ct);
    }

    /// <summary>Ping and Custom deliberately write no log file and raise no Prep* events — the
    /// prep log belongs to the MyDay selection and would otherwise be overwritten with noise.</summary>
    private async Task<PrimeRunOutcome> RunQuietAsync(
        IReadOnlyList<string> args, string prompt, string successMessage, string label, CancellationToken ct)
    {
        try
        {
            using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            timeoutCts.CancelAfter(FireTimeout);

            var result = await _claude.RunAsync(
                arguments: args,
                prompt: prompt,
                workingDirectory: Cwd(),
                onStdoutLine: _ => Task.CompletedTask,
                ct: timeoutCts.Token);

            return result.IsSuccess
                ? new PrimeRunOutcome(true, successMessage)
                : new PrimeRunOutcome(false, $"exit code {result.ExitCode}");
        }
        catch (OperationCanceledException) when (!ct.IsCancellationRequested)
        {
            return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalMinutes:0} min");
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Prime {Label} run failed", label);
            return new PrimeRunOutcome(false, ex.Message);
        }
    }

    private static string Cwd()
    {
        var cwd = Paths.AppDataRoot();
        Directory.CreateDirectory(cwd);
        return cwd;
    }
}
