using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Usage.Interfaces;

namespace ClaudeDo.Worker.Usage;

/// <summary>
/// Polls <see cref="IUsageClient"/> and keeps <see cref="UsageState"/> current. Polls once
/// immediately at startup, then on an **activity-dependent** interval: while any task is
/// Running it uses <see cref="WorkerConfig.UsagePollIntervalActiveSeconds"/>, otherwise the
/// slower <see cref="WorkerConfig.UsagePollIntervalIdleSeconds"/>. A 429 adds exponential
/// backoff on top (honouring <c>Retry-After</c> when the server sends one) — the endpoint is
/// undocumented and rate-limits aggressively. <see cref="RefreshNowAsync"/> gives the UI a
/// manual refresh that also resets the schedule, so the slow idle interval never leaves the
/// user staring at a stale number.
///
/// A failure is logged as a warning at most once per distinct error message, to avoid log spam
/// on a persistent outage. Broadcasts <see cref="HubBroadcaster.UsageUpdated"/> after every
/// poll cycle, success or failure, so the UI can reflect a stale/blocked state as soon as it
/// happens.
/// </summary>
public sealed class UsageMonitorService : BackgroundService
{
    /// <summary>A manual refresh inside this window of the last poll reuses the last result.</summary>
    internal static readonly TimeSpan ManualRefreshCooldown = TimeSpan.FromSeconds(10);

    private readonly IUsageClient _client;
    private readonly UsageState _state;
    private readonly WorkerConfig _config;
    private readonly ILogger<UsageMonitorService> _logger;
    private readonly UsageSnapshotBuilder _snapshotBuilder;
    private readonly HubBroadcaster _broadcaster;
    private readonly IRunningTaskProbe _runningProbe;

    // Serializes the background loop against a manual refresh so two polls never overlap.
    private readonly SemaphoreSlim _pollLock = new(1, 1);
    private string? _lastLoggedError;
    private int _rateLimitStrikes;
    private DateTime _lastPollUtc = DateTime.MinValue;
    private DateTime _nextPollDueUtc = DateTime.MinValue;

    public UsageMonitorService(
        IUsageClient client, UsageState state, WorkerConfig config, ILogger<UsageMonitorService> logger,
        UsageSnapshotBuilder snapshotBuilder, HubBroadcaster broadcaster, IRunningTaskProbe runningProbe)
    {
        _client = client;
        _state = state;
        _config = config;
        _logger = logger;
        _snapshotBuilder = snapshotBuilder;
        _broadcaster = broadcaster;
        _runningProbe = runningProbe;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Re-read the due time every iteration: a manual refresh pushes it out, which is
            // how the loop avoids polling again right after the user hit refresh.
            var wait = _nextPollDueUtc - DateTime.UtcNow;
            if (wait > TimeSpan.Zero)
            {
                try
                {
                    await Task.Delay(wait, stoppingToken);
                }
                catch (OperationCanceledException)
                {
                    return;
                }
                continue;
            }

            await TickAsync(stoppingToken);
        }
    }

    /// <summary>
    /// Forces a poll now and returns the resulting snapshot DTO. Within
    /// <see cref="ManualRefreshCooldown"/> of the last poll the API call is skipped and the
    /// current state is returned instead, so click-spamming the refresh button can't earn a 429.
    /// </summary>
    public async Task<UsageSnapshotDto> RefreshNowAsync(CancellationToken ct = default)
    {
        await PollAsync(ct, ManualRefreshCooldown);
        return await _snapshotBuilder.BuildAsync(ct);
    }

    internal Task TickAsync(CancellationToken ct) => PollAsync(ct, null);

    private async Task PollAsync(CancellationToken ct, TimeSpan? skipIfPolledWithin)
    {
        await _pollLock.WaitAsync(ct);
        TimeSpan? retryAfter = null;
        try
        {
            // Checked under the lock so a manual refresh that queued behind a background poll
            // sees that poll's timestamp and reuses its result instead of firing a second call.
            if (skipIfPolledWithin is { } window && DateTime.UtcNow - _lastPollUtc < window)
                return;

            try
            {
                var snapshot = await _client.GetUsageAsync(ct);
                _state.ReportSuccess(snapshot);
                _lastLoggedError = null;
                _rateLimitStrikes = 0;
            }
            catch (OperationCanceledException) when (ct.IsCancellationRequested)
            {
                throw;
            }
            catch (UsageRateLimitedException ex)
            {
                _rateLimitStrikes = Math.Min(_rateLimitStrikes + 1, UsagePollSchedule.MaxStrikes);
                retryAfter = ex.RetryAfter;
                RecordFailure(ex);
            }
            catch (Exception ex)
            {
                RecordFailure(ex);
            }

            _lastPollUtc = DateTime.UtcNow;
            _nextPollDueUtc = _lastPollUtc + await NextDelayAsync(retryAfter, ct);
        }
        finally
        {
            _pollLock.Release();
        }

        var dto = await _snapshotBuilder.BuildAsync(ct);
        await _broadcaster.UsageUpdated(dto);
    }

    private void RecordFailure(Exception ex)
    {
        _state.ReportFailure(ex.Message, DateTime.UtcNow);

        if (_lastLoggedError == ex.Message) return;
        _logger.LogWarning(ex, "UsageMonitorService: failed to fetch usage: {Message}", ex.Message);
        _lastLoggedError = ex.Message;
    }

    private async Task<TimeSpan> NextDelayAsync(TimeSpan? retryAfter, CancellationToken ct)
    {
        var anyRunning = await _runningProbe.AnyRunningAsync(ct);
        return UsagePollSchedule.NextDelay(
            anyRunning,
            _config.UsagePollIntervalActiveSeconds,
            _config.UsagePollIntervalIdleSeconds,
            _rateLimitStrikes,
            retryAfter);
    }
}

/// <summary>
/// Pure poll-interval arithmetic for <see cref="UsageMonitorService"/>: active-vs-idle base
/// interval plus 429 backoff. Kept static and side-effect-free so the schedule is testable
/// without a running background service.
/// </summary>
internal static class UsagePollSchedule
{
    /// <summary>Strike count is capped so the exponent can't run away on a long outage.</summary>
    internal const int MaxStrikes = 4;

    /// <summary>Nothing ever waits longer than this, not even an absurd <c>Retry-After</c>.</summary>
    internal static readonly TimeSpan MaxDelay = TimeSpan.FromMinutes(30);

    internal static TimeSpan NextDelay(
        bool anyTaskRunning, int activeSeconds, int idleSeconds, int rateLimitStrikes, TimeSpan? retryAfter)
    {
        var baseDelay = TimeSpan.FromSeconds(Math.Max(1, anyTaskRunning ? activeSeconds : idleSeconds));
        if (rateLimitStrikes <= 0)
            return baseDelay;

        var backoff = retryAfter ?? baseDelay * Math.Pow(2, Math.Min(rateLimitStrikes, MaxStrikes));
        // Never poll *sooner* than the normal cadence just because Retry-After was small.
        if (backoff < baseDelay) backoff = baseDelay;
        return backoff > MaxDelay ? MaxDelay : backoff;
    }
}
