"""SQLite schema definitions and initialization.

All DDL lives here so that ``init_schema()`` is the single entry point
for creating or upgrading the global database.
"""

from __future__ import annotations

import sqlite3
from pathlib import Path

__all__ = ["SCHEMA_VERSION", "init_schema"]

SCHEMA_VERSION = 7

# ---------------------------------------------------------------------------
# DDL statements
# ---------------------------------------------------------------------------

_PRAGMAS = """\
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
"""

_CREATE_SCHEMA_VERSION = """\
CREATE TABLE IF NOT EXISTS schema_version (
    version     INTEGER PRIMARY KEY,
    applied_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
"""

_CREATE_PROJECTS = """\
CREATE TABLE IF NOT EXISTS projects (
    id          TEXT PRIMARY KEY,
    name        TEXT NOT NULL,
    path        TEXT NOT NULL UNIQUE,
    created_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
    updated_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
"""

_CREATE_SKILL_DECISIONS = """\
CREATE TABLE IF NOT EXISTS skill_decisions (
    project_id  TEXT NOT NULL REFERENCES projects(id),
    skill_id    TEXT NOT NULL,
    decision    TEXT NOT NULL CHECK (decision IN ('promoted', 'rejected')),
    decided_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
    metadata    TEXT,  -- JSON blob for per-ID metadata (e.g. promoted_at timestamps)
    PRIMARY KEY (project_id, skill_id)
);
"""

_CREATE_REVIEW_LEARNINGS = """\
CREATE TABLE IF NOT EXISTS review_learnings (
    project_id          TEXT NOT NULL REFERENCES projects(id),
    id                  TEXT NOT NULL,
    rule                TEXT NOT NULL,
    category            TEXT NOT NULL,
    severity            TEXT NOT NULL,
    source              TEXT NOT NULL,
    first_seen          TEXT NOT NULL,
    last_reinforced     TEXT NOT NULL,
    reinforcement_count INTEGER NOT NULL DEFAULT 1,
    contradiction_count INTEGER NOT NULL DEFAULT 0,
    functional_principle TEXT,
    PRIMARY KEY (project_id, id)
);
"""

_CREATE_REVIEW_HISTORY = """\
CREATE TABLE IF NOT EXISTS review_history (
    rowid               INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id          TEXT NOT NULL REFERENCES projects(id),
    timestamp           TEXT NOT NULL,
    source              TEXT,
    issues_count        INTEGER NOT NULL DEFAULT 0,
    new_learning_ids    TEXT,  -- JSON array
    reinforced_learning_ids TEXT  -- JSON array
);
CREATE INDEX IF NOT EXISTS idx_review_history_project
    ON review_history(project_id, timestamp DESC);
"""

_CREATE_ACTIVE_SESSIONS = """\
CREATE TABLE IF NOT EXISTS active_sessions (
    project_id  TEXT PRIMARY KEY REFERENCES projects(id),
    data        TEXT NOT NULL,  -- JSON blob
    created_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
"""

_CREATE_REWARD_EVENTS = """\
CREATE TABLE IF NOT EXISTS reward_events (
    project_id      TEXT NOT NULL REFERENCES projects(id),
    id              TEXT NOT NULL,
    timestamp       TEXT NOT NULL,
    outcome         TEXT NOT NULL,
    reward_value    REAL NOT NULL,
    rules_active    TEXT,  -- JSON array
    revision_distance REAL,
    error_class     TEXT,
    notes           TEXT,
    source          TEXT,
    session_id      TEXT,
    PRIMARY KEY (project_id, id)
);
CREATE INDEX IF NOT EXISTS idx_reward_events_ts
    ON reward_events(project_id, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_reward_events_session
    ON reward_events(project_id, session_id);
"""

_CREATE_SESSIONS = """\
CREATE TABLE IF NOT EXISTS sessions (
    project_id      TEXT NOT NULL REFERENCES projects(id),
    id              TEXT NOT NULL,
    started_at      TEXT NOT NULL,
    ended_at        TEXT,
    entry_file      TEXT,
    rules_at_start  TEXT,  -- JSON array
    rules_at_end    TEXT,  -- JSON array
    selected_rules  TEXT,  -- JSON array
    error_class     TEXT,
    notes           TEXT,
    PRIMARY KEY (project_id, id)
);
CREATE INDEX IF NOT EXISTS idx_sessions_ts
    ON sessions(project_id, started_at DESC);
"""

_CREATE_MISTAKES = """\
CREATE TABLE IF NOT EXISTS mistakes (
    project_id      TEXT NOT NULL REFERENCES projects(id),
    id              TEXT NOT NULL,
    session_id      TEXT NOT NULL,
    timestamp       TEXT NOT NULL,
    error_class     TEXT NOT NULL,
    description     TEXT NOT NULL,
    semantic_hash   TEXT NOT NULL,
    was_repeat      INTEGER NOT NULL DEFAULT 0,
    corrected_by_rule TEXT,
    related_concepts TEXT,
    relation_to_prior TEXT,
    resolution_action TEXT,
    context         TEXT,
    severity        TEXT,
    PRIMARY KEY (project_id, id)
);
CREATE INDEX IF NOT EXISTS idx_mistakes_session
    ON mistakes(project_id, session_id);
CREATE INDEX IF NOT EXISTS idx_mistakes_hash
    ON mistakes(project_id, semantic_hash);
"""

_CREATE_BANDIT_ARMS = """\
CREATE TABLE IF NOT EXISTS bandit_arms (
    project_id  TEXT NOT NULL REFERENCES projects(id),
    context     TEXT NOT NULL,
    rule_id     TEXT NOT NULL,
    alpha       REAL NOT NULL DEFAULT 1.0,
    beta        REAL NOT NULL DEFAULT 1.0,
    is_seed     INTEGER NOT NULL DEFAULT 0,
    updated_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
    PRIMARY KEY (project_id, context, rule_id)
);
CREATE INDEX IF NOT EXISTS idx_bandit_context
    ON bandit_arms(project_id, context);
"""

_CREATE_GAUNTLET_RULES = """\
CREATE TABLE IF NOT EXISTS gauntlet_rules (
    rule_id         TEXT PRIMARY KEY,
    persona         TEXT NOT NULL,
    rule            TEXT NOT NULL,
    category        TEXT NOT NULL,
    context         TEXT NOT NULL DEFAULT '',
    antipattern     TEXT NOT NULL DEFAULT '',
    rationale       TEXT NOT NULL DEFAULT '',
    tags            TEXT NOT NULL DEFAULT '[]',
    refs            TEXT DEFAULT '[]',
    provenance      TEXT,
    version         INTEGER NOT NULL DEFAULT 1,
    active          INTEGER NOT NULL DEFAULT 1,
    created_at      TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
    updated_at      TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
    seed_file_hash  TEXT,
    seed_filename   TEXT
);
CREATE INDEX IF NOT EXISTS idx_gauntlet_rules_persona
    ON gauntlet_rules(persona);
CREATE INDEX IF NOT EXISTS idx_gauntlet_rules_active
    ON gauntlet_rules(active);
"""

_CREATE_EMISSION_EDGES = """\
CREATE TABLE IF NOT EXISTS emission_edges (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    source_id       TEXT NOT NULL,
    target_id       TEXT NOT NULL,
    relation_type   TEXT NOT NULL,
    confidence      REAL,
    artifact_type   TEXT NOT NULL,
    project_id      TEXT NOT NULL,
    emitted_at      TEXT NOT NULL,
    consumed_at     TEXT NOT NULL,
    properties      TEXT,
    UNIQUE(source_id, target_id, relation_type, emitted_at)
);
CREATE INDEX IF NOT EXISTS idx_emission_edges_relation
    ON emission_edges(relation_type);
CREATE INDEX IF NOT EXISTS idx_emission_edges_target
    ON emission_edges(target_id);
CREATE INDEX IF NOT EXISTS idx_emission_edges_project
    ON emission_edges(project_id);
"""

# Ordered list of all DDL blocks for schema v1.
_DDL_V1: list[str] = [
    _CREATE_SCHEMA_VERSION,
    _CREATE_PROJECTS,
    _CREATE_SKILL_DECISIONS,
    _CREATE_REVIEW_LEARNINGS,
    _CREATE_REVIEW_HISTORY,
    _CREATE_ACTIVE_SESSIONS,
    _CREATE_REWARD_EVENTS,
    _CREATE_SESSIONS,
    _CREATE_MISTAKES,
    _CREATE_BANDIT_ARMS,
    _CREATE_GAUNTLET_RULES,
]

# ---------------------------------------------------------------------------
# v2 migration: enrich mistakes table with graph-ready metadata
# ---------------------------------------------------------------------------

_MIGRATE_V2: list[str] = [
    "ALTER TABLE mistakes ADD COLUMN related_concepts TEXT;",
    "ALTER TABLE mistakes ADD COLUMN relation_to_prior TEXT;",
    "ALTER TABLE mistakes ADD COLUMN resolution_action TEXT;",
    "ALTER TABLE mistakes ADD COLUMN context TEXT;",
    "ALTER TABLE mistakes ADD COLUMN severity TEXT;",
]

# ---------------------------------------------------------------------------
# v3 migration: add session_id to reward_events for session linking
# ---------------------------------------------------------------------------

_MIGRATE_V3: list[str] = [
    "ALTER TABLE reward_events ADD COLUMN session_id TEXT;",
    "CREATE INDEX IF NOT EXISTS idx_reward_events_session ON reward_events(project_id, session_id);",
]

# ---------------------------------------------------------------------------
# v4 migration: add gauntlet_rules table (global, no project_id)
# ---------------------------------------------------------------------------

_MIGRATE_V4: str = _CREATE_GAUNTLET_RULES

# ---------------------------------------------------------------------------
# v5 migration: add emission_edges table for consumed emission data
# ---------------------------------------------------------------------------

_MIGRATE_V5: str = _CREATE_EMISSION_EDGES

# ---------------------------------------------------------------------------
# v6 migration: gauntlet_credits table for feedback loop attribution
# ---------------------------------------------------------------------------

_MIGRATE_V6: str = """\
CREATE TABLE IF NOT EXISTS gauntlet_credits (
    project_id  TEXT NOT NULL REFERENCES projects(id),
    timestamp   TEXT NOT NULL,
    iteration   INTEGER NOT NULL,
    rules       TEXT NOT NULL,  -- JSON array of credited rule IDs
    PRIMARY KEY (project_id, timestamp)
);
CREATE INDEX IF NOT EXISTS idx_gauntlet_credits_ts
    ON gauntlet_credits(project_id, timestamp DESC);
"""


# v7 migration: rules_consulted on mistakes + posterior_snapshots table
# ---------------------------------------------------------------------------

_MIGRATE_V7_STMTS: list[str] = [
    "ALTER TABLE mistakes ADD COLUMN rules_consulted TEXT;",  # JSON array
]

_MIGRATE_V7_DDL: str = """\
CREATE TABLE IF NOT EXISTS posterior_snapshots (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    project_id  TEXT NOT NULL REFERENCES projects(id),
    timestamp   TEXT NOT NULL,
    rule_id     TEXT NOT NULL,
    alpha       REAL NOT NULL,
    beta        REAL NOT NULL,
    mean        REAL NOT NULL,
    trigger     TEXT NOT NULL,
    iteration   INTEGER,
    batch_id    TEXT
);
CREATE INDEX IF NOT EXISTS idx_posterior_snapshots_rule
    ON posterior_snapshots(project_id, rule_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_posterior_snapshots_ts
    ON posterior_snapshots(project_id, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_posterior_snapshots_batch
    ON posterior_snapshots(project_id, batch_id);
"""


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def init_schema(conn: sqlite3.Connection) -> int:
    """Create or upgrade the schema to the current version.

    Args:
        conn: Open SQLite connection.

    Returns:
        The schema version after initialization.
    """
    # Apply pragmas (must be outside a transaction for WAL)
    for line in _PRAGMAS.strip().splitlines():
        conn.execute(line)

    # Check current version
    try:
        row = conn.execute("SELECT MAX(version) FROM schema_version").fetchone()
        current_version = row[0] if row and row[0] is not None else 0
    except sqlite3.OperationalError:
        current_version = 0

    if current_version >= SCHEMA_VERSION:
        return current_version

    # Apply v1 schema
    if current_version < 1:
        for ddl in _DDL_V1:
            conn.executescript(ddl)
        conn.execute(
            "INSERT OR REPLACE INTO schema_version (version) VALUES (?)",
            (1,),
        )
        conn.commit()
        current_version = 1

    # Apply v2 migration: enrich mistakes table
    if current_version < 2:
        for stmt in _MIGRATE_V2:
            try:
                conn.execute(stmt)
            except sqlite3.OperationalError:
                # Column already exists (e.g. from a partial migration)
                pass
        conn.execute(
            "INSERT OR REPLACE INTO schema_version (version) VALUES (?)",
            (2,),
        )
        conn.commit()
        current_version = 2

    # Apply v3 migration: add session_id to reward_events
    if current_version < 3:
        for stmt in _MIGRATE_V3:
            try:
                conn.execute(stmt)
            except sqlite3.OperationalError:
                # Column/index already exists (e.g. from a partial migration)
                pass
        conn.execute(
            "INSERT OR REPLACE INTO schema_version (version) VALUES (?)",
            (3,),
        )
        conn.commit()
        current_version = 3

    # Apply v4 migration: add gauntlet_rules table
    if current_version < 4:
        conn.executescript(_MIGRATE_V4)
        conn.execute(
            "INSERT OR REPLACE INTO schema_version (version) VALUES (?)",
            (4,),
        )
        conn.commit()
        current_version = 4

    # Apply v5 migration: add emission_edges table
    if current_version < 5:
        conn.executescript(_MIGRATE_V5)
        conn.execute(
            "INSERT OR REPLACE INTO schema_version (version) VALUES (?)",
            (5,),
        )
        conn.commit()
        current_version = 5

    # Apply v6 migration: gauntlet_credits table
    if current_version < 6:
        conn.executescript(_MIGRATE_V6)
        conn.execute(
            "INSERT OR REPLACE INTO schema_version (version) VALUES (?)",
            (6,),
        )
        conn.commit()
        current_version = 6

    # Apply v7 migration: rules_consulted on mistakes + posterior_snapshots
    if current_version < 7:
        for stmt in _MIGRATE_V7_STMTS:
            try:
                conn.execute(stmt)
            except sqlite3.OperationalError:
                pass  # Column already exists
        conn.executescript(_MIGRATE_V7_DDL)
        conn.execute(
            "INSERT OR REPLACE INTO schema_version (version) VALUES (?)",
            (7,),
        )
        conn.commit()

    return SCHEMA_VERSION


def open_db(db_path: Path) -> sqlite3.Connection:
    """Open (or create) the global database and ensure the schema is current.

    Args:
        db_path: Path to the SQLite database file.

    Returns:
        An open ``sqlite3.Connection`` with schema initialized.
    """
    db_path.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(str(db_path), check_same_thread=False)
    conn.row_factory = sqlite3.Row
    init_schema(conn)
    return conn
