using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Usage;
using Microsoft.EntityFrameworkCore;

namespace ClaudeDo.Worker.Queue;

/// <summary>
/// The "Continue on session limit reset" toggle (<see cref="AppSettingsEntity.AutoContinueOnUsageLimit"/>).
/// Off by default and fails open on every missing signal — no snapshot, no reset time — never a
/// timer, never a crash, never a change from today's behaviour. Called once per
/// <see cref="QueueService"/> tick (both from its wake and its 30s backstop):
/// <see cref="GetScheduledWakeAtAsync"/> tells the caller when to schedule an exact wake so a
/// gate-blocked queue doesn't wait out the backstop, and <see cref="RunAsync"/> fires at most one
/// <c>ContinueTask</c> per eligible task once the window has actually reset.
/// </summary>
public sealed class UsageLimitAutoContinueCoordinator
{
    public const string ContinuePrompt =
        "The Anthropic usage limit window that stopped this run has since reset. Continue the task from where the previous run left off.";

    private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
    private readonly UsageState _usageState;
    private readonly OverrideSlotService _override;
    private readonly HubBroadcaster _broadcaster;
    private readonly ILogger<UsageLimitAutoContinueCoordinator> _logger;

    public UsageLimitAutoContinueCoordinator(
        IDbContextFactory<ClaudeDoDbContext> dbFactory, UsageState usageState, OverrideSlotService overrideSlot,
        HubBroadcaster broadcaster, ILogger<UsageLimitAutoContinueCoordinator> logger)
    {
        _dbFactory = dbFactory;
        _usageState = usageState;
        _override = overrideSlot;
        _broadcaster = broadcaster;
        _logger = logger;
    }

    /// <summary>The reset time <see cref="QueueService"/> should schedule an exact wake for, or
    /// null when the toggle is off or no reset time is known yet — fail open, no timer.</summary>
    public async Task<DateTimeOffset?> GetScheduledWakeAtAsync(CancellationToken ct)
    {
        var settings = await TryReadSettingsAsync(ct);
        if (settings is null || !settings.AutoContinueOnUsageLimit) return null;
        return _usageState.Snapshot?.FiveHour?.ResetsAt;
    }

    /// <summary>Fires at most one <c>ContinueTask</c> per task failed on the usage limit, once the
    /// 5h window has actually reset — and at most one per tick, because <see cref="OverrideSlotService"/>
    /// has a single slot. Remaining candidates are picked up by later ticks. No-ops (never throws)
    /// when the toggle is off, no reset time is known, or the window hasn't reset yet.</summary>
    public async Task RunAsync(CancellationToken ct)
    {
        var settings = await TryReadSettingsAsync(ct);
        if (settings is null || !settings.AutoContinueOnUsageLimit) return;

        var resetsAt = _usageState.Snapshot?.FiveHour?.ResetsAt;
        if (resetsAt is null || DateTimeOffset.UtcNow < resetsAt.Value) return;

        List<TaskEntity> candidates;
        using (var context = _dbFactory.CreateDbContext())
            candidates = await new TaskRepository(context).GetUsageLimitAutoContinueCandidatesAsync(ct);

        foreach (var task in candidates)
        {
            // OverrideSlotService holds exactly ONE slot, and a usage limit typically stops every
            // running task at once — so most ticks have more candidates than capacity. Stop at the
            // first occupied slot instead of burning the rest; the queue's backstop retries.
            if (_override.CurrentSlot is not null) return;

            bool claimed;
            using (var context = _dbFactory.CreateDbContext())
                claimed = await new TaskRepository(context).TryClaimUsageLimitAutoContinueAsync(task.Id, DateTime.UtcNow, ct);
            if (!claimed) continue; // already fired for this task — never twice.

            try
            {
                await _override.ContinueTask(task.Id, ContinuePrompt);
                await _broadcaster.WorkerLog(
                    $"Auto-continuing #{task.Number} \"{task.Title}\" after usage-limit reset",
                    WorkerLogLevel.Info, DateTime.UtcNow);
            }
            catch (Exception ex)
            {
                // The claim is the dedupe guard, so it has to be taken *before* the run starts —
                // which means a run that never started has to give it back, or the task is
                // permanently excluded from the candidate query for a continue that never ran.
                await ReleaseClaimAsync(task.Id, ct);
                _logger.LogError(ex, "Failed to auto-continue task {TaskId} after usage-limit reset", task.Id);
                return; // slot busy or the run was rejected — leave the rest for the next tick.
            }
        }
    }

    private async Task ReleaseClaimAsync(string taskId, CancellationToken ct)
    {
        try
        {
            using var context = _dbFactory.CreateDbContext();
            await new TaskRepository(context).ReleaseUsageLimitAutoContinueClaimAsync(taskId, ct);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to release the usage-limit auto-continue claim for task {TaskId}", taskId);
        }
    }

    private async Task<AppSettingsEntity?> TryReadSettingsAsync(CancellationToken ct)
    {
        try
        {
            using var context = _dbFactory.CreateDbContext();
            return await new AppSettingsRepository(context).GetAsync(ct);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "UsageLimitAutoContinueCoordinator: failed to read settings; skipping tick");
            return null;
        }
    }
}
