using Microsoft.Data.Sqlite;

namespace ClawdDotNet.Core.Storage;

/// <summary>
/// Zentrale Stelle für den Zugang zur Instanz-Datenbank.
///
/// Die frühere Fassung öffnete pro Aufruf eine Verbindung ohne weitere Einstellungen —
/// ohne WAL und ohne Wartezeit bei Sperren. Sobald mehrere Agenten gleichzeitig
/// schreiben, quittiert SQLite das mit "database is locked". Das sah nach einer Grenze
/// von SQLite aus, war aber nur fehlende Konfiguration.
///
/// - WAL erlaubt beliebig viele Leser parallel zu einem Schreiber.
/// - busy_timeout lässt einen Schreiber kurz warten, statt sofort zu scheitern.
/// - Connection-Pooling vermeidet den Aufbau je Aufruf.
/// </summary>
public sealed class SqliteStorage
{
    private readonly string _connectionString;
    private readonly SemaphoreSlim _writeGate = new(1, 1);

    public string DatabasePath { get; }

    public SqliteStorage(string databasePath)
    {
        DatabasePath = databasePath;

        var directory = Path.GetDirectoryName(Path.GetFullPath(databasePath));
        if (!string.IsNullOrEmpty(directory))
            Directory.CreateDirectory(directory);

        _connectionString = new SqliteConnectionStringBuilder
        {
            DataSource = databasePath,
            Mode = SqliteOpenMode.ReadWriteCreate,
            Cache = SqliteCacheMode.Shared,
            Pooling = true,
            DefaultTimeout = 30
        }.ToString();

        Initialize();
    }

    public SqliteConnection OpenConnection()
    {
        var connection = new SqliteConnection(_connectionString);
        connection.Open();
        ApplyPragmas(connection);
        return connection;
    }

    public async Task<SqliteConnection> OpenConnectionAsync(CancellationToken ct)
    {
        var connection = new SqliteConnection(_connectionString);
        await connection.OpenAsync(ct);
        ApplyPragmas(connection);
        return connection;
    }

    /// <summary>
    /// Serialisiert Schreibvorgänge im Prozess. SQLite lässt ohnehin nur einen Schreiber
    /// zu — die Warteschlange hier ist verlässlicher als das Zurückweisen durch die
    /// Datenbank und macht Fehlerbilder reproduzierbar.
    /// </summary>
    public async Task<T> WriteAsync<T>(Func<SqliteConnection, Task<T>> action, CancellationToken ct)
    {
        await _writeGate.WaitAsync(ct);
        try
        {
            await using var connection = await OpenConnectionAsync(ct);
            return await action(connection);
        }
        finally
        {
            _writeGate.Release();
        }
    }

    public async Task WriteAsync(Func<SqliteConnection, Task> action, CancellationToken ct)
        => await WriteAsync<object?>(async conn => { await action(conn); return null; }, ct);

    private static void ApplyPragmas(SqliteConnection connection)
    {
        using var cmd = connection.CreateCommand();
        cmd.CommandText = """
            PRAGMA journal_mode = WAL;
            PRAGMA busy_timeout = 5000;
            PRAGMA synchronous = NORMAL;
            PRAGMA foreign_keys = ON;
            """;
        cmd.ExecuteNonQuery();
    }

    private void Initialize()
    {
        using var connection = OpenConnection();
        using var cmd = connection.CreateCommand();

        cmd.CommandText = """
            CREATE TABLE IF NOT EXISTS ToolState (
                Key   TEXT PRIMARY KEY,
                Value TEXT
            );

            CREATE TABLE IF NOT EXISTS Memories (
                Id          INTEGER PRIMARY KEY AUTOINCREMENT,
                Scope       TEXT    NOT NULL,
                OwnerId     TEXT    NOT NULL,
                Category    TEXT    NOT NULL,
                Subject     TEXT    NOT NULL,
                MemoryKey   TEXT    NULL,
                Content     TEXT    NOT NULL,
                Tags        TEXT    NOT NULL DEFAULT '',
                Importance  INTEGER NOT NULL DEFAULT 3,
                CreatedAt   TEXT    NOT NULL,
                UpdatedAt   TEXT    NOT NULL,
                CreatedBy   TEXT    NOT NULL DEFAULT ''
            );

            CREATE INDEX IF NOT EXISTS IX_Memories_Lookup
                ON Memories (Scope, OwnerId, Subject);

            CREATE INDEX IF NOT EXISTS IX_Memories_Recent
                ON Memories (Scope, OwnerId, UpdatedAt DESC);

            -- Ein Schluessel identifiziert eine Erinnerung eindeutig; erneutes Merken
            -- unter demselben Schluessel aktualisiert sie, statt eine zweite anzulegen.
            CREATE UNIQUE INDEX IF NOT EXISTS UX_Memories_Key
                ON Memories (Scope, OwnerId, MemoryKey)
                WHERE MemoryKey IS NOT NULL;

            CREATE TABLE IF NOT EXISTS RunUsage (
                Id               INTEGER PRIMARY KEY AUTOINCREMENT,
                AgentId          TEXT    NOT NULL,
                Model            TEXT    NOT NULL,
                PromptTokens     INTEGER NOT NULL,
                CompletionTokens INTEGER NOT NULL,
                CachedTokens     INTEGER NOT NULL DEFAULT 0,
                CostUsd          TEXT    NOT NULL DEFAULT '0',
                CostIsKnown      INTEGER NOT NULL DEFAULT 0,
                Status           TEXT    NOT NULL DEFAULT '',
                StepCount        INTEGER NOT NULL DEFAULT 0,
                DurationMs       INTEGER NOT NULL DEFAULT 0,
                OccurredAt       TEXT    NOT NULL,
                -- Ortsdatum, damit ein Tagesbudget der Wahrnehmung des Benutzers folgt
                -- und die Abfrage ohne Zeitzonenrechnerei auskommt.
                UsageDate        TEXT    NOT NULL
            );

            CREATE INDEX IF NOT EXISTS IX_RunUsage_Day
                ON RunUsage (UsageDate, AgentId);

            CREATE INDEX IF NOT EXISTS IX_RunUsage_Recent
                ON RunUsage (OccurredAt DESC);

            -- Taskboard (A1): Ausführungszustand der Aufgaben. Die Definition lebt in
            -- Markdown-Dateien; diese Tabelle spiegelt sie und macht das Claiming atomar.
            CREATE TABLE IF NOT EXISTS Tasks (
                Id                 TEXT    PRIMARY KEY,
                Title              TEXT    NOT NULL DEFAULT '',
                Status             TEXT    NOT NULL DEFAULT 'todo',
                Type               TEXT    NOT NULL DEFAULT 'work',
                Priority           INTEGER NOT NULL DEFAULT 3,
                Assignee           TEXT    NOT NULL DEFAULT '@human',
                WhenKind           TEXT    NULL,
                WhenValue          TEXT    NULL,
                WhenTz             TEXT    NULL,
                RequireApproval    INTEGER NOT NULL DEFAULT 0,
                Acceptance         TEXT    NOT NULL DEFAULT '',
                -- Blocker als "|a|b|"; die Begrenzer verhindern Teiltreffer bei der Suche.
                BlockedBy          TEXT    NOT NULL DEFAULT '',
                OnlyWhenMarketOpen INTEGER NOT NULL DEFAULT 0,
                Body               TEXT    NOT NULL DEFAULT '',
                FileName           TEXT    NOT NULL DEFAULT '',
                -- Nur für Typ tool_job: welches Tool mit welcher Job-Art getickt wird.
                ToolName           TEXT    NOT NULL DEFAULT '',
                JobTypeId          TEXT    NOT NULL DEFAULT '',
                -- Ausführungszustand: nur das Board schreibt hier.
                LastOccurrence     TEXT    NULL,
                ClaimToken         TEXT    NULL,
                ClaimedAt          TEXT    NULL,
                CreatedAt          TEXT    NOT NULL,
                UpdatedAt          TEXT    NOT NULL
            );

            CREATE INDEX IF NOT EXISTS IX_Tasks_Status
                ON Tasks (Status);

            CREATE INDEX IF NOT EXISTS IX_Tasks_Assignee
                ON Tasks (Assignee);

            -- Audit-Log (A3): ein Eintrag je Tool-Aufruf. Append-only — die Herkunft
            -- stempelt die Engine, Korrekturen sind neue Zeilen.
            CREATE TABLE IF NOT EXISTS AuditLog (
                Id         INTEGER PRIMARY KEY AUTOINCREMENT,
                RunId      TEXT    NOT NULL,
                AgentId    TEXT    NOT NULL,
                Model      TEXT    NOT NULL DEFAULT '',
                Source     TEXT    NOT NULL DEFAULT 'unknown',
                Tool       TEXT    NOT NULL,
                Arguments  TEXT    NOT NULL DEFAULT '',
                Status     TEXT    NOT NULL,
                Summary    TEXT    NOT NULL DEFAULT '',
                DurationMs INTEGER NOT NULL DEFAULT 0,
                OccurredAt TEXT    NOT NULL
            );

            CREATE INDEX IF NOT EXISTS IX_AuditLog_Run
                ON AuditLog (RunId, Id);

            CREATE INDEX IF NOT EXISTS IX_AuditLog_Recent
                ON AuditLog (Id DESC);

            -- Abschluss-Belege (Receipts): ein Beleg je Lauf, verknüpft mit einem Task.
            CREATE TABLE IF NOT EXISTS RunReceipts (
                Id               INTEGER PRIMARY KEY AUTOINCREMENT,
                RunId            TEXT    NOT NULL,
                AgentId          TEXT    NOT NULL,
                Model            TEXT    NOT NULL DEFAULT '',
                Source           TEXT    NOT NULL DEFAULT 'unknown',
                TaskId           TEXT    NULL,
                Status           TEXT    NOT NULL DEFAULT '',
                StepCount        INTEGER NOT NULL DEFAULT 0,
                PromptTokens     INTEGER NOT NULL DEFAULT 0,
                CompletionTokens INTEGER NOT NULL DEFAULT 0,
                CachedTokens     INTEGER NOT NULL DEFAULT 0,
                CostUsd          TEXT    NOT NULL DEFAULT '0',
                CostIsKnown      INTEGER NOT NULL DEFAULT 0,
                DurationMs       INTEGER NOT NULL DEFAULT 0,
                ResultRef        TEXT    NOT NULL DEFAULT '',
                OccurredAt       TEXT    NOT NULL
            );

            CREATE INDEX IF NOT EXISTS IX_RunReceipts_Task
                ON RunReceipts (TaskId);

            CREATE INDEX IF NOT EXISTS IX_RunReceipts_Run
                ON RunReceipts (RunId);

            -- Staging (A2): eingefrorene, freigabepflichtige Tool-Aufrufe. Ausgeführt wird
            -- genau der gespeicherte Argument-JSON (Plan-Freeze).
            CREATE TABLE IF NOT EXISTS StagedCalls (
                Id              INTEGER PRIMARY KEY AUTOINCREMENT,
                RunId           TEXT    NOT NULL,
                AgentId         TEXT    NOT NULL,
                InstanceId      TEXT    NOT NULL DEFAULT '',
                Tool            TEXT    NOT NULL,
                Action          TEXT    NULL,
                ArgumentsJson   TEXT    NOT NULL DEFAULT '',
                Proposal        TEXT    NOT NULL DEFAULT '',
                Status          TEXT    NOT NULL DEFAULT 'Pending',
                CreatedAt       TEXT    NOT NULL,
                DecidedAt       TEXT    NULL,
                DecidedBy       TEXT    NULL,
                ResultRef       TEXT    NULL,
                RejectionReason TEXT    NULL
            );

            CREATE INDEX IF NOT EXISTS IX_StagedCalls_Pending
                ON StagedCalls (Status, Id);
            """;

        cmd.ExecuteNonQuery();
    }
}
