"""SQLite connection and migration runner.

The schema lives in backend/migrations/ as numbered .sql files; this module
runs them in order against the configured database file and exposes a
connection factory that the rest of the app uses. WAL is enabled because
the SSE handlers and the reconciler can read while a webhook writes; FK
enforcement is on because §5's cascade rules depend on it.

Per §4.2, single SQLite file colocated with the FastAPI process. If we
outgrow this, the spec calls for a planned migration to Postgres on a
separate host — not for a clever sharding scheme bolted on here.
"""
from __future__ import annotations

import sqlite3
from contextlib import contextmanager
from pathlib import Path
from typing import Iterator

from .config import Config

MIGRATIONS_DIR = Path(__file__).resolve().parent.parent / "migrations"


def connect(path: Path) -> sqlite3.Connection:
    conn = sqlite3.connect(path, isolation_level=None, check_same_thread=False)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode = WAL")
    conn.execute("PRAGMA foreign_keys = ON")
    conn.execute("PRAGMA synchronous = NORMAL")
    return conn


def run_migrations(config: Config) -> None:
    conn = connect(config.database_path)
    try:
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS schema_migrations (
                version TEXT PRIMARY KEY,
                applied_at TEXT NOT NULL DEFAULT (datetime('now'))
            )
            """
        )
        applied = {row["version"] for row in conn.execute("SELECT version FROM schema_migrations")}
        for path in sorted(MIGRATIONS_DIR.glob("*.sql")):
            version = path.stem
            if version in applied:
                continue
            sql = path.read_text()
            if "-- migrate:no-foreign-keys" in sql:
                # SQLite table rebuilds (changing a PRIMARY KEY / UNIQUE, e.g.
                # folding project_id into a composite key) follow the official
                # 12-step ALTER procedure, which requires FK enforcement OFF —
                # and `PRAGMA foreign_keys` is a no-op *inside* a transaction, so
                # it must be toggled here, around the script. The connection is
                # in autocommit mode (isolation_level=None), so the PRAGMA takes
                # effect immediately. We re-enable and run foreign_key_check
                # after, failing the migration loudly if the rebuild left any
                # dangling reference.
                conn.execute("PRAGMA foreign_keys = OFF")
                try:
                    conn.executescript("BEGIN; " + sql + "; COMMIT;")
                    violations = conn.execute("PRAGMA foreign_key_check").fetchall()
                    if violations:
                        raise RuntimeError(
                            f"migration {version} left foreign-key violations: "
                            f"{[tuple(v) for v in violations]}"
                        )
                finally:
                    conn.execute("PRAGMA foreign_keys = ON")
            else:
                conn.executescript("BEGIN; " + sql + "; COMMIT;")
            conn.execute("INSERT INTO schema_migrations (version) VALUES (?)", (version,))
    finally:
        conn.close()


_CONN: sqlite3.Connection | None = None


def init(config: Config) -> None:
    """Open the long-lived connection. Call once at startup, after migrations."""
    global _CONN
    if _CONN is not None:
        return
    _CONN = connect(config.database_path)


def conn() -> sqlite3.Connection:
    if _CONN is None:
        raise RuntimeError("db.init() has not been called")
    return _CONN


@contextmanager
def tx() -> Iterator[sqlite3.Connection]:
    """Wrap a block in a transaction. Rolls back on exception."""
    c = conn()
    c.execute("BEGIN")
    try:
        yield c
        c.execute("COMMIT")
    except Exception:
        c.execute("ROLLBACK")
        raise
