using System.Collections.Concurrent;
using System.Net.Http.Headers;
using System.Text.Json;
using ClawdDotNet.Core.Api;
using ClawdDotNet.Core.Api.Models;

namespace ClawdDotNet.App.Services;

/// <summary>
/// Fragt regelmäßig Erreichbarkeit und Guthaben der OpenRouter-API ab.
///
/// Der Takt lief früher über einen <c>System.Windows.Forms.Timer</c> — der braucht eine
/// Nachrichtenschleife und damit ein Fenster. Jetzt <see cref="PeriodicTimer"/>: läuft
/// auch ohne Oberfläche, was der kopflose Betrieb voraussetzt.
///
/// <see cref="OnStatusUpdated"/> wird auf einem Hintergrundfaden ausgelöst. Wer daran
/// eine Oberfläche hängt, muss selbst auf den Oberflächenfaden wechseln — in Avalonia
/// über <c>Dispatcher.UIThread</c>.
/// </summary>
public sealed class OpenRouterStatusService : IAsyncDisposable
{
    private readonly HttpClient _http;
    private readonly TimeSpan _interval;
    private readonly CancellationTokenSource _cts = new();
    private Task? _loop;

    private readonly ConcurrentBag<UsageRecord> _usageRecords = new();

    /// <summary>
    /// Preise kommen vom /models-Endpunkt statt aus einer fest verdrahteten Tabelle.
    /// Die alte Tabelle war veraltet und enthielt ausgerechnet das Standardmodell der
    /// Agenten nicht — die Anzeige meldete dafür stillschweigend 0 €.
    /// </summary>
    private readonly ModelPricingCatalog _pricing = new();

    /// <summary>Modelle, für die keine Preise vorliegen — werden in der Anzeige benannt.</summary>
    private readonly ConcurrentDictionary<string, byte> _modelsWithoutPricing = new();

    /// <summary>
    /// Grober Umrechnungskurs fuer die Anzeige. Oeffentlich, damit die Tokennutzungs-
    /// Seite denselben Wert verwendet — zwei Kopien eines veraltenden Kurses waeren
    /// zwei Gelegenheiten, verschiedene Betraege fuer dieselben Laeufe zu zeigen.
    /// </summary>
    public const double UsdToEur = 0.92;

    public bool IsApiReachable { get; private set; }
    public string StatusText { get; private set; } = "Prüfe...";
    public string CreditsText { get; private set; } = "—";
    public string CreditsTooltip { get; private set; } = "";
    public double? CreditBalance { get; private set; }
    public double? CreditRemaining { get; private set; }

    public event Action? OnStatusUpdated;

    public OpenRouterStatusService(string apiKey, string baseUrl = "https://openrouter.ai/api/v1/", int checkIntervalSeconds = 60)
    {
        _http = new HttpClient { BaseAddress = new Uri(baseUrl) };
        _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
        _http.DefaultRequestHeaders.Add("HTTP-Referer", "ClawdDotNet");

        _interval = TimeSpan.FromSeconds(checkIntervalSeconds);
    }

    public void Start()
    {
        _loop ??= RunLoopAsync(_cts.Token);
        _ = CheckStatusAsync();
    }

    private async Task RunLoopAsync(CancellationToken ct)
    {
        using var timer = new PeriodicTimer(_interval);

        while (await timer.WaitForNextTickAsync(ct))
        {
            // CheckStatusAsync fängt bereits alles ab und setzt IsApiReachable — hier
            // muss nichts mehr behandelt werden.
            await CheckStatusAsync();
        }
    }

    public void RecordUsage(string model, int promptTokens, int completionTokens)
    {
        var estimate = _pricing.Estimate(model, promptTokens, completionTokens);

        if (!estimate.IsKnown)
            _modelsWithoutPricing.TryAdd(model, 0);

        _usageRecords.Add(new UsageRecord(
            DateTime.Now, model, promptTokens, completionTokens, (double)estimate.Usd, estimate.IsKnown));

        UpdateCreditsText();
        OnStatusUpdated?.Invoke();
    }

    /// <summary>
    /// Lädt die aktuellen Modellpreise. Ohne diesen Aufruf bleibt der Katalog leer und
    /// alle Kosten werden als unbekannt ausgewiesen.
    /// </summary>
    public async Task LoadPricingAsync(OpenRouterClient client, CancellationToken ct = default)
    {
        try
        {
            var models = await client.GetAvailableModelsAsync(ct);
            _pricing.Load(models);

            // Modelle, die bisher als unbekannt galten, sind jetzt vielleicht bekannt.
            foreach (var model in _modelsWithoutPricing.Keys)
            {
                if (_pricing.IsKnown(model))
                    _modelsWithoutPricing.TryRemove(model, out _);
            }

            UpdateCreditsText();
            OnStatusUpdated?.Invoke();
        }
        catch (Exception)
        {
            // Ohne Preise bleibt die Anzeige ehrlich unbekannt statt falsch null.
        }
    }

    private async Task CheckStatusAsync()
    {
        try
        {
            using var response = await _http.GetAsync("auth/key");

            if (response.IsSuccessStatusCode)
            {
                var body = await response.Content.ReadAsStringAsync();
                var doc = JsonDocument.Parse(body);

                IsApiReachable = true;

                if (doc.RootElement.TryGetProperty("data", out var data))
                {
                    if (data.TryGetProperty("limit", out var limit))
                        CreditBalance = limit.GetDouble();

                    if (data.TryGetProperty("usage", out var usage))
                    {
                        var used = usage.GetDouble();
                        var remaining = (CreditBalance ?? 0) - used;
                        CreditRemaining = remaining;
                        StatusText = $"✓ API OK | Credits: ${remaining:F4} von ${CreditBalance:F2}";
                    }
                    else
                    {
                        StatusText = "✓ API erreichbar";
                    }
                }
                else
                {
                    StatusText = "✓ API erreichbar";
                }
            }
            else
            {
                IsApiReachable = false;
                StatusText = $"✗ API Fehler: {(int)response.StatusCode}";
            }
        }
        catch (HttpRequestException ex)
        {
            IsApiReachable = false;
            StatusText = $"✗ Nicht erreichbar: {ex.Message}";
        }
        catch (TaskCanceledException)
        {
            IsApiReachable = false;
            StatusText = "✗ Timeout";
        }
        catch (Exception ex)
        {
            IsApiReachable = false;
            StatusText = $"✗ Fehler: {ex.Message}";
        }

        UpdateCreditsText();
        OnStatusUpdated?.Invoke();
    }

    private void UpdateCreditsText()
    {
        var now = DateTime.Now;
        var oneHourAgo = now.AddHours(-1);
        var oneDayAgo = now.AddHours(-24);

        var records = _usageRecords.ToArray();

        var lastHour = records.Where(r => r.Timestamp >= oneHourAgo).ToArray();
        var last24h = records.Where(r => r.Timestamp >= oneDayAgo).ToArray();

        var tokensLastHour = lastHour.Sum(r => r.PromptTokens + r.CompletionTokens);
        var tokensLast24h = last24h.Sum(r => r.PromptTokens + r.CompletionTokens);
        var costLastHour = lastHour.Sum(r => r.CostUsd);
        var costLast24h = last24h.Sum(r => r.CostUsd);

        // Ein Hinweis, sobald Läufe dabei sind, deren Kosten nicht bezifferbar sind —
        // sonst liest sich eine zu niedrige Summe wie eine vollständige.
        var unpriced = last24h.Count(r => !r.CostIsKnown);
        var warning = unpriced > 0 ? " ⚠" : "";

        CreditsText = $"1h: {tokensLastHour:N0} Tok (~{costLastHour * UsdToEur:F4}€) | " +
                      $"24h: {tokensLast24h:N0} Tok (~{costLast24h * UsdToEur:F4}€){warning}";

        // Detaillierter Tooltip: Pro-Model-Aufschlüsselung (letzte 24h)
        var modelGroups = last24h
            .GroupBy(r => r.Model)
            .OrderByDescending(g => g.Sum(r => r.CostUsd))
            .ToList();

        if (modelGroups.Count == 0)
        {
            CreditsTooltip = "Keine Token-Nutzung in den letzten 24h";
            return;
        }

        var sb = new System.Text.StringBuilder();
        sb.AppendLine("═══ Token-Verbrauch (24h) ═══");
        sb.AppendLine();

        foreach (var group in modelGroups)
        {
            var modelName = group.Key;
            var shortName = modelName.Contains('/') ? modelName[(modelName.IndexOf('/') + 1)..] : modelName;
            var prompt = group.Sum(r => r.PromptTokens);
            var completion = group.Sum(r => r.CompletionTokens);
            var total = prompt + completion;
            var cost = group.Sum(r => r.CostUsd);
            var runs = group.Count();

            sb.AppendLine($"▸ {shortName}");
            sb.AppendLine($"  {runs}x Runs | {total:N0} Tokens ({prompt:N0} in / {completion:N0} out)");

            if (_pricing.Get(modelName) is { } pricing)
            {
                sb.AppendLine($"  Preis: ${pricing.InputPer1M:0.####}/1M in, ${pricing.OutputPer1M:0.####}/1M out");
                sb.AppendLine($"  Kosten: ${cost:F4} (~{cost * UsdToEur:F4}€)");
            }
            else
            {
                sb.AppendLine("  Kosten: unbekannt — für dieses Modell liegen keine Preise vor");
            }

            sb.AppendLine();
        }

        var totalCost = last24h.Sum(r => r.CostUsd);
        sb.AppendLine($"═══ Gesamt: ${totalCost:F4} (~{totalCost * UsdToEur:F4}€) ═══");

        if (unpriced > 0)
        {
            sb.AppendLine();
            sb.AppendLine($"⚠ {unpriced} Lauf/Läufe ohne Preisangabe — die Summe ist unvollständig.");
            sb.AppendLine($"  Betroffene Modelle: {string.Join(", ", _modelsWithoutPricing.Keys.Order())}");
        }

        if (_pricing.LastUpdated is { } updated)
            sb.AppendLine($"\nPreise abgerufen: {updated:g} ({_pricing.Count} Modelle)");
        else
            sb.AppendLine("\n⚠ Preise noch nicht geladen.");

        CreditsTooltip = sb.ToString().TrimEnd();
    }

    public async ValueTask DisposeAsync()
    {
        await _cts.CancelAsync();

        if (_loop is not null)
        {
            try { await _loop; }
            catch (OperationCanceledException) { /* erwartet */ }
        }

        _cts.Dispose();
        _http.Dispose();
    }

    private sealed record UsageRecord(
        DateTime Timestamp,
        string Model,
        int PromptTokens,
        int CompletionTokens,
        double CostUsd,
        bool CostIsKnown);
}
