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

namespace ClawdDotNet.Core.Audit;

/// <summary>
/// Das Audit-Log in der Instanz-Datenbank. Bewusst nur Einfügen und Lesen — es gibt keine
/// Update-/Delete-Methoden, weil die Unveränderlichkeit die eigentliche Zusage ist.
/// </summary>
public sealed class SqliteAuditRepository : IAuditRepository
{
    private readonly SqliteStorage _storage;

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

    public Task AppendAsync(AuditEntry entry, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            using var cmd = conn.CreateCommand();
            cmd.CommandText = """
                INSERT INTO AuditLog
                    (RunId, AgentId, Model, Source, Tool, Arguments, Status, Summary, DurationMs, OccurredAt)
                VALUES
                    (@runId, @agentId, @model, @source, @tool, @arguments, @status, @summary, @durationMs, @occurredAt)
                """;
            cmd.Parameters.AddWithValue("@runId", entry.RunId);
            cmd.Parameters.AddWithValue("@agentId", entry.AgentId);
            cmd.Parameters.AddWithValue("@model", entry.Model);
            cmd.Parameters.AddWithValue("@source", AuditSource.Normalize(entry.Source));
            cmd.Parameters.AddWithValue("@tool", entry.Tool);
            cmd.Parameters.AddWithValue("@arguments", entry.Arguments);
            cmd.Parameters.AddWithValue("@status", entry.Status.ToString());
            cmd.Parameters.AddWithValue("@summary", entry.Summary);
            cmd.Parameters.AddWithValue("@durationMs", entry.DurationMs);
            cmd.Parameters.AddWithValue("@occurredAt", Format(entry.OccurredAt));
            await cmd.ExecuteNonQueryAsync(ct);
        }, ct);

    public Task RecordReceiptAsync(RunReceipt receipt, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            using var cmd = conn.CreateCommand();
            cmd.CommandText = """
                INSERT INTO RunReceipts
                    (RunId, AgentId, Model, Source, TaskId, Status, StepCount,
                     PromptTokens, CompletionTokens, CachedTokens, CostUsd, CostIsKnown,
                     DurationMs, ResultRef, OccurredAt)
                VALUES
                    (@runId, @agentId, @model, @source, @taskId, @status, @stepCount,
                     @prompt, @completion, @cached, @cost, @costKnown,
                     @durationMs, @resultRef, @occurredAt)
                """;
            cmd.Parameters.AddWithValue("@runId", receipt.RunId);
            cmd.Parameters.AddWithValue("@agentId", receipt.AgentId);
            cmd.Parameters.AddWithValue("@model", receipt.Model);
            cmd.Parameters.AddWithValue("@source", AuditSource.Normalize(receipt.Source));
            cmd.Parameters.AddWithValue("@taskId", (object?)receipt.TaskId ?? DBNull.Value);
            cmd.Parameters.AddWithValue("@status", receipt.Status);
            cmd.Parameters.AddWithValue("@stepCount", receipt.StepCount);
            cmd.Parameters.AddWithValue("@prompt", receipt.PromptTokens);
            cmd.Parameters.AddWithValue("@completion", receipt.CompletionTokens);
            cmd.Parameters.AddWithValue("@cached", receipt.CachedTokens);
            cmd.Parameters.AddWithValue("@cost", receipt.CostUsd.ToString(CultureInfo.InvariantCulture));
            cmd.Parameters.AddWithValue("@costKnown", receipt.CostIsKnown ? 1 : 0);
            cmd.Parameters.AddWithValue("@durationMs", receipt.DurationMs);
            cmd.Parameters.AddWithValue("@resultRef", receipt.ResultRef);
            cmd.Parameters.AddWithValue("@occurredAt", Format(receipt.OccurredAt));
            await cmd.ExecuteNonQueryAsync(ct);
        }, ct);

    public async Task<IReadOnlyList<AuditEntry>> ListRecentAsync(int limit, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT " + AuditColumns + " FROM AuditLog ORDER BY Id DESC LIMIT @limit";
        cmd.Parameters.AddWithValue("@limit", Math.Clamp(limit, 1, 1000));
        return await ReadEntriesAsync(cmd, ct);
    }

    public async Task<IReadOnlyList<AuditEntry>> ListForRunAsync(string runId, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT " + AuditColumns + " FROM AuditLog WHERE RunId = @runId ORDER BY Id";
        cmd.Parameters.AddWithValue("@runId", runId);
        return await ReadEntriesAsync(cmd, ct);
    }

    public async Task<RunReceipt?> GetReceiptForRunAsync(string runId, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT " + ReceiptColumns + " FROM RunReceipts WHERE RunId = @runId ORDER BY Id DESC LIMIT 1";
        cmd.Parameters.AddWithValue("@runId", runId);

        await using var reader = await cmd.ExecuteReaderAsync(ct);
        return await reader.ReadAsync(ct) ? ReadReceipt(reader) : null;
    }

    public async Task<IReadOnlyList<RunReceipt>> ListReceiptsForTaskAsync(string taskId, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT " + ReceiptColumns + " FROM RunReceipts WHERE TaskId = @taskId ORDER BY Id";
        cmd.Parameters.AddWithValue("@taskId", taskId);

        var results = new List<RunReceipt>();
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
            results.Add(ReadReceipt(reader));
        return results;
    }

    public async Task<int> CountAsync(CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT COUNT(*) FROM AuditLog";
        return Convert.ToInt32(await cmd.ExecuteScalarAsync(ct));
    }

    // ─── Hilfsfunktionen ───

    private const string AuditColumns =
        "Id, RunId, AgentId, Model, Source, Tool, Arguments, Status, Summary, DurationMs, OccurredAt";

    private const string ReceiptColumns =
        "Id, RunId, AgentId, Model, Source, TaskId, Status, StepCount, PromptTokens, " +
        "CompletionTokens, CachedTokens, CostUsd, CostIsKnown, DurationMs, ResultRef, OccurredAt";

    private static async Task<IReadOnlyList<AuditEntry>> ReadEntriesAsync(SqliteCommand cmd, CancellationToken ct)
    {
        var results = new List<AuditEntry>();
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
            results.Add(ReadEntry(reader));
        return results;
    }

    private static AuditEntry ReadEntry(SqliteDataReader r) => new()
    {
        Id = r.GetInt64(0),
        RunId = r.GetString(1),
        AgentId = r.GetString(2),
        Model = r.GetString(3),
        Source = r.GetString(4),
        Tool = r.GetString(5),
        Arguments = r.GetString(6),
        Status = Enum.TryParse<AuditStatus>(r.GetString(7), out var s) ? s : AuditStatus.Ok,
        Summary = r.GetString(8),
        DurationMs = r.GetInt64(9),
        OccurredAt = Parse(r.GetString(10))
    };

    private static RunReceipt ReadReceipt(SqliteDataReader r) => new()
    {
        Id = r.GetInt64(0),
        RunId = r.GetString(1),
        AgentId = r.GetString(2),
        Model = r.GetString(3),
        Source = r.GetString(4),
        TaskId = r.IsDBNull(5) ? null : r.GetString(5),
        Status = r.GetString(6),
        StepCount = r.GetInt32(7),
        PromptTokens = r.GetInt32(8),
        CompletionTokens = r.GetInt32(9),
        CachedTokens = r.GetInt32(10),
        CostUsd = decimal.TryParse(r.GetString(11), NumberStyles.Any, CultureInfo.InvariantCulture, out var c) ? c : 0m,
        CostIsKnown = r.GetInt32(12) != 0,
        DurationMs = r.GetInt64(13),
        ResultRef = r.GetString(14),
        OccurredAt = Parse(r.GetString(15))
    };

    private static string Format(DateTime value) => value.ToUniversalTime().ToString("O");

    private static DateTime Parse(string value)
        => DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dt)
            ? dt
            : DateTime.MinValue;
}
