using ClaudeDo.Worker.Usage.TokenTracker.Interfaces;

namespace ClaudeDo.Worker.Usage.TokenTracker;

/// <summary>
/// Owns the cached export. One fetch covers <see cref="WindowDays"/> days because
/// <c>buildSessionAnalytics</c> walks the whole history regardless of <c>--from</c>/<c>--to</c>
/// (v0.88.4 returns the same rows either way), so narrowing the request would buy nothing while
/// making every range switch pay the cost again.
/// </summary>
public sealed class TokenTrackerService
{
    public const int WindowDays = 90;

    private readonly ITokenTrackerClient _client;
    private readonly Func<DateTime> _utcNow;
    private readonly SemaphoreSlim _refreshLock = new(1, 1);
    private TokenTrackerProbe? _probe;

    public TokenTrackerService(ITokenTrackerClient client, TokenTrackerState state, Func<DateTime>? utcNow = null)
    {
        _client = client;
        State = state;
        _utcNow = utcNow ?? (() => DateTime.UtcNow);
    }

    public TokenTrackerState State { get; }

    public async Task<TokenTrackerProbe> ProbeAsync(bool force = false, CancellationToken ct = default)
    {
        if (!force && _probe is { } cached) return cached;

        var probe = await _client.ProbeAsync(ct);
        _probe = probe;
        return probe;
    }

    /// <summary>Fetches only when the cached export is missing or older than
    /// <paramref name="maxAge"/>. Safe to call on every modal open.</summary>
    public Task EnsureFreshAsync(TimeSpan maxAge, CancellationToken ct = default) =>
        State.IsOlderThan(maxAge, _utcNow()) ? RefreshAsync(ct) : Task.CompletedTask;

    public async Task RefreshAsync(CancellationToken ct = default)
    {
        // A refresh already in flight is the refresh this caller wanted — waiting for it and
        // returning is correct, and it stops a burst of range switches from spawning processes.
        if (!await _refreshLock.WaitAsync(0, ct))
        {
            await _refreshLock.WaitAsync(ct);
            _refreshLock.Release();
            return;
        }

        try
        {
            var now = _utcNow();
            var to = DateOnly.FromDateTime(now.ToLocalTime());
            var from = to.AddDays(-WindowDays);

            var run = await _client.ExportAsync(from, to, ct);
            if (!run.Ok)
            {
                State.ReportFailure(run.Error ?? "TokenTracker export failed.", now);
                return;
            }

            var export = TokenTrackerExportParser.Parse(run.StdOut, now);
            if (export is null)
            {
                State.ReportFailure("TokenTracker returned output we could not parse.", now);
                return;
            }

            if (export.FormatVersion != TokenTrackerExportParser.SupportedFormatVersion)
            {
                State.ReportFailure(
                    $"Unexpected TokenTracker export format v{export.FormatVersion} " +
                    $"(supported: v{TokenTrackerExportParser.SupportedFormatVersion}).",
                    now);
                return;
            }

            State.ReportSuccess(export);
        }
        finally
        {
            _refreshLock.Release();
        }
    }

    /// <summary>Starts (or reuses) the local dashboard server. Probes first so a missing CLI comes
    /// back as a message instead of a failed process spawn.</summary>
    public async Task<TokenTrackerDashboard> OpenDashboardAsync(CancellationToken ct = default)
    {
        var probe = await ProbeAsync(ct: ct);
        if (!probe.Installed)
            return new TokenTrackerDashboard(false, null,
                probe.Error ?? "TokenTracker CLI is not installed.");

        return await _client.StartDashboardAsync(ct);
    }

    public async Task<TokenTrackerRunResult> InstallAsync(
        IProgress<string>? output = null, CancellationToken ct = default)
    {
        var result = await _client.InstallAsync(output, ct);
        await ProbeAsync(force: true, ct);
        return result;
    }
}
