using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Nodes;
using ClaudeDo.Data;
using ClaudeDo.Worker.Usage.Interfaces;

namespace ClaudeDo.Worker.Usage;

/// <summary>
/// Fetches the current OAuth usage/limit snapshot from Anthropic using the access token
/// Claude Code keeps fresh in <c>~/.claude/.credentials.json</c>. Never refreshes the token
/// itself, and never logs or surfaces the token value.
/// </summary>
public sealed class ClaudeOAuthUsageClient : IUsageClient
{
    private const string UsageUrl = "https://api.anthropic.com/api/oauth/usage";

    private readonly HttpClient _http;
    private readonly string _credentialsPath;

    public ClaudeOAuthUsageClient(HttpClient http)
        : this(http, Path.Combine(Paths.Expand("~/.claude"), ".credentials.json")) { }

    internal ClaudeOAuthUsageClient(HttpClient http, string credentialsPath)
    {
        _http = http;
        _credentialsPath = credentialsPath;
    }

    public async Task<UsageSnapshot> GetUsageAsync(CancellationToken ct = default)
    {
        var token = ReadAccessToken();

        using var request = new HttpRequestMessage(HttpMethod.Get, UsageUrl);
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        request.Headers.Add("anthropic-beta", "oauth-2025-04-20");

        using var response = await _http.SendAsync(request, ct);
        if (response.StatusCode == HttpStatusCode.TooManyRequests)
            throw new UsageRateLimitedException(ReadRetryAfter(response));
        if (!response.IsSuccessStatusCode)
            throw new InvalidOperationException($"Usage request failed with status {(int)response.StatusCode}.");

        var body = await response.Content.ReadAsStringAsync(ct);
        return Parse(body);
    }

    /// <summary>
    /// Reads the <c>Retry-After</c> header in either form (delta-seconds or HTTP-date).
    /// A missing/past value returns null — the caller then uses its own backoff.
    /// </summary>
    internal static TimeSpan? ReadRetryAfter(HttpResponseMessage response)
    {
        var header = response.Headers.RetryAfter;
        if (header is null) return null;

        if (header.Delta is { } delta)
            return delta > TimeSpan.Zero ? delta : null;

        if (header.Date is { } date)
        {
            var remaining = date - DateTimeOffset.UtcNow;
            return remaining > TimeSpan.Zero ? remaining : null;
        }

        return null;
    }

    private string ReadAccessToken()
    {
        if (!File.Exists(_credentialsPath))
            throw new InvalidOperationException($"Claude credentials file not found at '{_credentialsPath}'.");

        string json;
        try
        {
            json = File.ReadAllText(_credentialsPath);
        }
        catch (IOException ex)
        {
            throw new InvalidOperationException($"Failed to read Claude credentials file: {ex.Message}", ex);
        }

        JsonNode? root;
        try
        {
            root = JsonNode.Parse(json);
        }
        catch (JsonException ex)
        {
            throw new InvalidOperationException($"Failed to parse Claude credentials file: {ex.Message}", ex);
        }

        var token = root?["claudeAiOauth"]?["accessToken"]?.GetValue<string>();
        if (string.IsNullOrWhiteSpace(token))
            throw new InvalidOperationException("Claude credentials file has no accessToken.");

        return token;
    }

    /// <summary>
    /// Defensive parse of the usage response: missing/null buckets become null, missing
    /// <c>limits</c> becomes an empty list, and unknown fields are ignored. Only an
    /// unparsable body throws.
    /// </summary>
    internal static UsageSnapshot Parse(string json)
    {
        JsonNode? root;
        try
        {
            root = JsonNode.Parse(json);
        }
        catch (JsonException ex)
        {
            throw new InvalidOperationException($"Failed to parse usage response: {ex.Message}", ex);
        }

        if (root is not JsonObject obj)
            throw new InvalidOperationException("Usage response was not a JSON object.");

        try
        {
            var fiveHour = ParseBucket(obj["five_hour"]);
            var sevenDay = ParseBucket(obj["seven_day"]);
            var limits = ParseLimits(obj["limits"] as JsonArray);

            return new UsageSnapshot(fiveHour, sevenDay, limits, DateTime.UtcNow);
        }
        catch (InvalidOperationException)
        {
            throw;
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException($"Failed to parse usage response: {ex.Message}", ex);
        }
    }

    private static UsageBucket? ParseBucket(JsonNode? node)
    {
        if (node is not JsonObject obj)
            return null;

        var utilization = TryGetDouble(obj["utilization"]) ?? 0;
        var resetsAt = TryGetDateTimeOffset(obj["resets_at"]);
        return new UsageBucket(utilization, resetsAt);
    }

    private static List<UsageLimitRow> ParseLimits(JsonArray? array)
    {
        var result = new List<UsageLimitRow>();
        if (array is null)
            return result;

        foreach (var item in array)
        {
            if (item is not JsonObject row)
                continue;

            var kind = TryGetString(row["kind"]) ?? "";
            var group = TryGetString(row["group"]) ?? "";
            var percent = TryGetDouble(row["percent"]) ?? 0;
            var severity = TryGetString(row["severity"]) ?? "";
            var resetsAt = TryGetDateTimeOffset(row["resets_at"]);
            var scopeModelDisplayName = TryGetString((row["scope"] as JsonObject)?["model"]?["display_name"]);
            var isActive = TryGetBool(row["is_active"]) ?? false;

            result.Add(new UsageLimitRow(kind, group, percent, severity, resetsAt, scopeModelDisplayName, isActive));
        }

        return result;
    }

    private static string? TryGetString(JsonNode? node) =>
        node is JsonValue value && value.TryGetValue(out string? s) ? s : null;

    private static double? TryGetDouble(JsonNode? node) =>
        node is JsonValue value && value.TryGetValue(out double d) ? d : null;

    private static bool? TryGetBool(JsonNode? node) =>
        node is JsonValue value && value.TryGetValue(out bool b) ? b : null;

    private static DateTimeOffset? TryGetDateTimeOffset(JsonNode? node) =>
        node is JsonValue value && value.TryGetValue(out string? s) && DateTimeOffset.TryParse(s, out var dto)
            ? dto
            : null;
}

/// <summary>
/// The usage endpoint answered 429. Carries the server's <c>Retry-After</c> when it sent one so
/// <see cref="UsageMonitorService"/> can honour it instead of guessing a backoff.
/// </summary>
public sealed class UsageRateLimitedException : InvalidOperationException
{
    public UsageRateLimitedException(TimeSpan? retryAfter)
        : base(retryAfter is { } r
            ? $"Usage request was rate-limited (429); retry after {(int)r.TotalSeconds}s."
            : "Usage request was rate-limited (429).")
    {
        RetryAfter = retryAfter;
    }

    public TimeSpan? RetryAfter { get; }
}
