using System.Collections.ObjectModel;
using ClawdDotNet.App;
using ClawdDotNet.App.Services;
using ClawdDotNet.Core.Accounting;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace ClawdDotNet.Desktop.ViewModels;

/// <summary>Ein Balken des Wochendiagramms.</summary>
public sealed class TokenDayItem
{
    public required string DayLabel { get; init; }
    public required string TokensLabel { get; init; }
    public required int Tokens { get; init; }

    /// <summary>
    /// Höhe in Bildpunkten, nicht in Prozent: Ein Balken kann sich nicht auf die Höhe
    /// seines Elternteils beziehen, ohne dass die Zeile eine feste Höhe bekommt — und
    /// eine feste Höhe im Ansichtsmodell auszurechnen ist ehrlicher als eine, die in
    /// drei Bindungen verteilt steht.
    /// </summary>
    public required double BarHeight { get; init; }
}

/// <summary>Eine Zeile der Aufschlüsselung nach Agent und Modell.</summary>
public sealed class TokenAgentItem
{
    public required string AgentName { get; init; }
    public required string Model { get; init; }
    public required string TokensLabel { get; init; }
    public required string CostLabel { get; init; }
    public required int Runs { get; init; }
}

/// <summary>
/// Tokennutzung der letzten sieben Tage.
///
/// <para>Liest aus <see cref="SqliteUsageRepository"/> — dieselbe Quelle, aus der die
/// Tagesbudgets bedient werden. Kein Diagramm-Paket: Der Entwurf zeigt sieben Balken und
/// eine Tabelle, und dafür genügen Rechtecke mit ausgerechneter Höhe. Eine Bibliothek
/// dafür wäre 8 MB Abhängigkeit für sieben Rechtecke.</para>
/// </summary>
public sealed partial class TokenUsagePageViewModel : PageViewModel
{
    /// <summary>Höhe der Diagrammfläche in Bildpunkten (siehe Entwurf: 160 px inkl. Beschriftung).</summary>
    private const double ChartHeight = 130;

    /// <summary>Wie viele Läufe für die Aufschlüsselung herangezogen werden.</summary>
    private const int RecentRunLimit = 1000;

    private const int Days = 7;

    private readonly AppHost? _host;

    public ObservableCollection<TokenDayItem> Days7 { get; } = [];
    public ObservableCollection<TokenAgentItem> ByAgent { get; } = [];

    [ObservableProperty]
    private string _totalTokens = "—";

    [ObservableProperty]
    private string _totalCost = "—";

    [ObservableProperty]
    private string _totalRuns = "—";

    [ObservableProperty]
    private string _statusText = "";

    [ObservableProperty]
    private bool _isEmpty = true;

    public TokenUsagePageViewModel() : this(null) { }

    public TokenUsagePageViewModel(AppHost? host) : base("Tokennutzung")
    {
        _host = host;

        if (host?.Usage is null)
        {
            StatusText = host is null
                ? "Entwurfsmodus"
                : "Keine Verbrauchsdatenbank für diese Instanz.";
            return;
        }

        // Nicht abwarten: Der Aufbau des Fensters darf nicht auf eine Datenbankabfrage
        // warten. Die Seite füllt sich, sobald die Zahlen da sind.
        _ = RefreshAsync();
    }

    [RelayCommand]
    private async Task RefreshAsync()
    {
        if (_host?.Usage is not { } usage) return;

        try
        {
            var today = DateOnly.FromDateTime(DateTime.Now);
            var daily = new List<DailyUsage>(Days);

            for (var offset = Days - 1; offset >= 0; offset--)
                daily.Add(await usage.GetDailyAsync(today.AddDays(-offset), "", CancellationToken.None));

            var recent = await usage.GetRecentAsync(RecentRunLimit, CancellationToken.None);

            var since = DateTime.Now.Date.AddDays(-(Days - 1));
            var window = recent.Where(r => r.OccurredAt >= since).ToArray();

            BuildChart(daily);
            BuildBreakdown(window);
            BuildTotals(daily, window);
        }
        catch (Exception ex)
        {
            StatusText = $"Verbrauchsdaten konnten nicht gelesen werden: {ex.Message}";
        }
    }

    private void BuildChart(IReadOnlyList<DailyUsage> daily)
    {
        Days7.Clear();

        // Ohne Höchstwert gäbe es keinen Maßstab; ohne die Untergrenze 1 eine Division
        // durch null an dem Tag, an dem noch nichts gelaufen ist.
        var max = Math.Max(1, daily.Max(d => d.TotalTokens));

        foreach (var day in daily)
        {
            Days7.Add(new TokenDayItem
            {
                DayLabel = day.Date.ToString("ddd"),
                Tokens = day.TotalTokens,
                TokensLabel = day.TotalTokens >= 1000
                    ? $"{day.TotalTokens / 1000d:0.#}k"
                    : day.TotalTokens.ToString("N0"),

                // Mindestens ein Pixel, damit ein Tag mit wenigen Tokens nicht wie ein
                // Tag ohne Läufe aussieht.
                BarHeight = Math.Max(1, day.TotalTokens / (double)max * ChartHeight)
            });
        }
    }

    private void BuildBreakdown(IReadOnlyList<RunUsage> runs)
    {
        ByAgent.Clear();

        var names = _host?.Instance.Agents
            .ToDictionary(a => a.AgentId, a => a.DisplayName, StringComparer.OrdinalIgnoreCase)
            ?? [];

        var groups = runs
            .GroupBy(r => (r.AgentId, r.Model))
            .OrderByDescending(g => g.Sum(r => r.PromptTokens + r.CompletionTokens));

        foreach (var group in groups)
        {
            var (agentId, model) = group.Key;
            var tokens = group.Sum(r => r.PromptTokens + r.CompletionTokens);
            var cost = group.Sum(r => r.CostUsd);
            var incomplete = group.Any(r => !r.CostIsKnown);

            ByAgent.Add(new TokenAgentItem
            {
                AgentName = names.TryGetValue(agentId, out var name) ? name : agentId,
                Model = model,
                TokensLabel = tokens.ToString("N0"),
                CostLabel = FormatCost(cost, incomplete),
                Runs = group.Count()
            });
        }
    }

    private void BuildTotals(IReadOnlyList<DailyUsage> daily, IReadOnlyList<RunUsage> runs)
    {
        var tokens = daily.Sum(d => d.TotalTokens);
        var cost = daily.Sum(d => d.CostUsd);
        var incomplete = daily.Any(d => !d.CostIsComplete);

        TotalTokens = tokens.ToString("N0");
        TotalCost = FormatCost(cost, incomplete);
        TotalRuns = daily.Sum(d => d.RunCount).ToString("N0");

        IsEmpty = tokens == 0 && runs.Count == 0;
        StatusText = IsEmpty ? "In den letzten sieben Tagen ist kein Lauf verzeichnet." : "";
    }

    /// <summary>
    /// Kosten in Euro, wie in der Statusleiste — mit demselben Kurs aus
    /// <see cref="OpenRouterStatusService.UsdToEur"/>. Das Zeichen ⚠ steht für Läufe,
    /// deren Modellpreise nicht vorlagen: Die Summe ist dann zu niedrig, und das soll man
    /// ihr ansehen.
    /// </summary>
    private static string FormatCost(decimal usd, bool incomplete)
    {
        var eur = (double)usd * OpenRouterStatusService.UsdToEur;
        return incomplete ? $"{eur:F2} € ⚠" : $"{eur:F2} €";
    }
}
