using System.Globalization;
using ClawdDotNet.Core.Storage;
using Microsoft.Data.Sqlite;

namespace ClawdDotNet.Core.Accounting;

/// <summary>
/// Ablage der Verbrauchsdaten in der Instanz-Datenbank.
///
/// Vorher lag das in TokenUsage.json: Bei JEDEM Agenten-Lauf wurde die gesamte Datei
/// geladen, ergänzt und neu geschrieben — unter einem globalen Lock. Das wächst
/// quadratisch und ist der eigentliche Engpass bei vielen Agenten, unabhängig davon,
/// welche Datenbank darunter liegt.
///
/// Beträge werden als Text abgelegt und als decimal gelesen. SQLite kennt keinen
/// exakten Dezimaltyp; über REAL zu gehen würde bei Cent-Betraegen Rundungsfehler
/// einsammeln, die sich über tausende Läufe summieren.
/// </summary>
public sealed class SqliteUsageRepository : IUsageRepository
{
    private readonly SqliteStorage _storage;

    public SqliteUsageRepository(SqliteStorage storage) => _storage = storage;

    public Task RecordAsync(RunUsage usage, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            var occurredAt = usage.OccurredAt == default ? DateTime.Now : usage.OccurredAt;

            using var cmd = conn.CreateCommand();
            cmd.CommandText = """
                INSERT INTO RunUsage
                    (AgentId, Model, PromptTokens, CompletionTokens, CachedTokens,
                     CostUsd, CostIsKnown, Status, StepCount, DurationMs, OccurredAt, UsageDate)
                VALUES
                    (@agentId, @model, @prompt, @completion, @cached,
                     @cost, @costKnown, @status, @steps, @duration, @occurredAt, @usageDate)
                """;

            cmd.Parameters.AddWithValue("@agentId", usage.AgentId);
            cmd.Parameters.AddWithValue("@model", usage.Model);
            cmd.Parameters.AddWithValue("@prompt", usage.PromptTokens);
            cmd.Parameters.AddWithValue("@completion", usage.CompletionTokens);
            cmd.Parameters.AddWithValue("@cached", usage.CachedTokens);
            cmd.Parameters.AddWithValue("@cost", FormatDecimal(usage.CostUsd));
            cmd.Parameters.AddWithValue("@costKnown", usage.CostIsKnown ? 1 : 0);
            cmd.Parameters.AddWithValue("@status", usage.Status);
            cmd.Parameters.AddWithValue("@steps", usage.StepCount);
            cmd.Parameters.AddWithValue("@duration", usage.DurationMs);
            cmd.Parameters.AddWithValue("@occurredAt", occurredAt.ToString("O"));
            cmd.Parameters.AddWithValue("@usageDate", FormatDate(DateOnly.FromDateTime(occurredAt)));

            await cmd.ExecuteNonQueryAsync(ct);
        }, ct);

    public async Task<DailyUsage> GetDailyAsync(DateOnly date, string agentId, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();

        var filterAgent = !string.IsNullOrWhiteSpace(agentId);

        cmd.CommandText = $"""
            SELECT COALESCE(SUM(PromptTokens), 0),
                   COALESCE(SUM(CompletionTokens), 0),
                   COUNT(*),
                   COALESCE(SUM(CASE WHEN CostIsKnown = 0 THEN 1 ELSE 0 END), 0)
              FROM RunUsage
             WHERE UsageDate = @date {(filterAgent ? "AND AgentId = @agentId" : "")}
            """;
        cmd.Parameters.AddWithValue("@date", FormatDate(date));
        if (filterAgent)
            cmd.Parameters.AddWithValue("@agentId", agentId);

        int prompt = 0, completion = 0, runs = 0, unpriced = 0;
        await using (var reader = await cmd.ExecuteReaderAsync(ct))
        {
            if (await reader.ReadAsync(ct))
            {
                prompt = reader.GetInt32(0);
                completion = reader.GetInt32(1);
                runs = reader.GetInt32(2);
                unpriced = reader.GetInt32(3);
            }
        }

        // Beträge einzeln summieren, weil sie als Text abgelegt sind.
        using var costCmd = conn.CreateCommand();
        costCmd.CommandText = $"""
            SELECT CostUsd FROM RunUsage
             WHERE UsageDate = @date {(filterAgent ? "AND AgentId = @agentId" : "")}
            """;
        costCmd.Parameters.AddWithValue("@date", FormatDate(date));
        if (filterAgent)
            costCmd.Parameters.AddWithValue("@agentId", agentId);

        decimal cost = 0m;
        await using (var reader = await costCmd.ExecuteReaderAsync(ct))
        {
            while (await reader.ReadAsync(ct))
                cost += ParseDecimal(reader.GetString(0));
        }

        return new DailyUsage(date, prompt, completion, cost, unpriced == 0, runs);
    }

    public async Task<IReadOnlyList<RunUsage>> GetRecentAsync(int limit, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = """
            SELECT Id, AgentId, Model, PromptTokens, CompletionTokens, CachedTokens,
                   CostUsd, CostIsKnown, Status, StepCount, DurationMs, OccurredAt
              FROM RunUsage
             ORDER BY OccurredAt DESC
             LIMIT @limit
            """;
        cmd.Parameters.AddWithValue("@limit", Math.Clamp(limit, 1, 1000));

        var results = new List<RunUsage>();
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
            results.Add(Read(reader));

        return results;
    }

    public Task<int> PurgeBeforeAsync(DateOnly date, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            using var cmd = conn.CreateCommand();
            cmd.CommandText = "DELETE FROM RunUsage WHERE UsageDate < @date";
            cmd.Parameters.AddWithValue("@date", FormatDate(date));
            return await cmd.ExecuteNonQueryAsync(ct);
        }, ct);

    // ─── Hilfsfunktionen ───

    private static RunUsage Read(SqliteDataReader reader) => new()
    {
        Id = reader.GetInt64(0),
        AgentId = reader.GetString(1),
        Model = reader.GetString(2),
        PromptTokens = reader.GetInt32(3),
        CompletionTokens = reader.GetInt32(4),
        CachedTokens = reader.GetInt32(5),
        CostUsd = ParseDecimal(reader.GetString(6)),
        CostIsKnown = reader.GetInt32(7) != 0,
        Status = reader.GetString(8),
        StepCount = reader.GetInt32(9),
        DurationMs = reader.GetInt64(10),
        OccurredAt = DateTime.TryParse(reader.GetString(11), null,
            DateTimeStyles.RoundtripKind, out var dt) ? dt : DateTime.MinValue
    };

    private static string FormatDate(DateOnly date) => date.ToString("yyyy-MM-dd");

    private static string FormatDecimal(decimal value)
        => value.ToString(CultureInfo.InvariantCulture);

    private static decimal ParseDecimal(string value)
        => decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) ? d : 0m;
}
