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

namespace ClawdDotNet.Core.Staging;

/// <summary>
/// Die Staging-Warteschlange in der Instanz-Datenbank. Der bedingte Statuswechsel
/// (<see cref="TryTransitionAsync"/>) ist dieselbe atomare Claim-Technik wie beim
/// Taskboard: Er verhindert, dass zwei Reviewer denselben Vorschlag doppelt entscheiden.
/// </summary>
public sealed class SqliteStagingRepository : IStagingRepository
{
    private readonly SqliteStorage _storage;

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

    public Task<long> AppendAsync(StagedCall call, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            using var cmd = conn.CreateCommand();
            cmd.CommandText = """
                INSERT INTO StagedCalls
                    (RunId, AgentId, InstanceId, Tool, Action, ArgumentsJson, Proposal, Status, CreatedAt)
                VALUES
                    (@runId, @agentId, @instanceId, @tool, @action, @args, @proposal, 'Pending', @createdAt);
                SELECT last_insert_rowid();
                """;
            cmd.Parameters.AddWithValue("@runId", call.RunId);
            cmd.Parameters.AddWithValue("@agentId", call.AgentId);
            cmd.Parameters.AddWithValue("@instanceId", call.InstanceId);
            cmd.Parameters.AddWithValue("@tool", call.Tool);
            cmd.Parameters.AddWithValue("@action", (object?)call.Action ?? DBNull.Value);
            cmd.Parameters.AddWithValue("@args", call.ArgumentsJson);
            cmd.Parameters.AddWithValue("@proposal", call.Proposal);
            cmd.Parameters.AddWithValue("@createdAt", Format(DateTime.UtcNow));
            return Convert.ToInt64(await cmd.ExecuteScalarAsync(ct));
        }, ct);

    public async Task<StagedCall?> GetAsync(long id, CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT " + Columns + " FROM StagedCalls WHERE Id = @id LIMIT 1";
        cmd.Parameters.AddWithValue("@id", id);

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

    public async Task<IReadOnlyList<StagedCall>> ListPendingAsync(CancellationToken ct)
    {
        await using var conn = await _storage.OpenConnectionAsync(ct);
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT " + Columns + " FROM StagedCalls WHERE Status = 'Pending' ORDER BY Id";

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

    public Task<bool> TryTransitionAsync(
        long id, StagingStatus from, StagingStatus to,
        string? decidedBy, string? rejectionReason, DateTime now, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            using var cmd = conn.CreateCommand();
            cmd.CommandText = """
                UPDATE StagedCalls
                   SET Status = @to, DecidedBy = @decidedBy, DecidedAt = @now,
                       RejectionReason = @reason
                 WHERE Id = @id AND Status = @from
                """;
            cmd.Parameters.AddWithValue("@to", to.ToString());
            cmd.Parameters.AddWithValue("@decidedBy", (object?)decidedBy ?? DBNull.Value);
            cmd.Parameters.AddWithValue("@now", Format(now));
            cmd.Parameters.AddWithValue("@reason", (object?)rejectionReason ?? DBNull.Value);
            cmd.Parameters.AddWithValue("@id", id);
            cmd.Parameters.AddWithValue("@from", from.ToString());
            return await cmd.ExecuteNonQueryAsync(ct) == 1;
        }, ct);

    public Task FinalizeAsync(long id, StagingStatus status, string? resultRef, DateTime now, CancellationToken ct)
        => _storage.WriteAsync(async conn =>
        {
            using var cmd = conn.CreateCommand();
            cmd.CommandText = """
                UPDATE StagedCalls
                   SET Status = @status, ResultRef = @resultRef, DecidedAt = @now
                 WHERE Id = @id
                """;
            cmd.Parameters.AddWithValue("@status", status.ToString());
            cmd.Parameters.AddWithValue("@resultRef", (object?)resultRef ?? DBNull.Value);
            cmd.Parameters.AddWithValue("@now", Format(now));
            cmd.Parameters.AddWithValue("@id", id);
            await cmd.ExecuteNonQueryAsync(ct);
        }, ct);

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

    // ─── Hilfsfunktionen ───

    private const string Columns =
        "Id, RunId, AgentId, InstanceId, Tool, Action, ArgumentsJson, Proposal, Status, " +
        "CreatedAt, DecidedAt, DecidedBy, ResultRef, RejectionReason";

    private static StagedCall Read(SqliteDataReader r) => new()
    {
        Id = r.GetInt64(0),
        RunId = r.GetString(1),
        AgentId = r.GetString(2),
        InstanceId = r.GetString(3),
        Tool = r.GetString(4),
        Action = r.IsDBNull(5) ? null : r.GetString(5),
        ArgumentsJson = r.GetString(6),
        Proposal = r.GetString(7),
        Status = Enum.TryParse<StagingStatus>(r.GetString(8), out var s) ? s : StagingStatus.Pending,
        CreatedAt = Parse(r.GetString(9)),
        DecidedAt = r.IsDBNull(10) ? null : Parse(r.GetString(10)),
        DecidedBy = r.IsDBNull(11) ? null : r.GetString(11),
        ResultRef = r.IsDBNull(12) ? null : r.GetString(12),
        RejectionReason = r.IsDBNull(13) ? null : r.GetString(13)
    };

    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;
}
