"""SQLite schema definition, typed-column upgrades, and constraint migration.

Extracted from sqlite_store.py. Pure DDL/migration helpers operating on a
passed-in connection; no dependency on the store class. Behavior unchanged.
"""

from __future__ import annotations

import sqlite3

SCHEMA_VERSION = 2

CONSTRAINED_TABLE_SCHEMAS = {
    "submissions": """
        create table {table} (
            id text primary key,
            title text not null default '',
            risk_score integer not null default 0 check (risk_score >= 0 and risk_score <= 100),
            risk_band text not null default 'low' check (risk_band in ('low', 'medium', 'high', 'failed', 'pending')),
            decision_status text not null default 'unreviewed' check (decision_status in ('unreviewed', 'held', 'rejected', 'approved', 'corrected')),
            submitted_epoch integer not null default 0 check (submitted_epoch >= 0),
            queue_id text not null default '',
            payload text not null check (json_valid(payload))
        )
    """,
    "evidence": """
        create table {table} (
            id text primary key,
            submission_id text not null,
            source text not null default '',
            confidence real not null default 0 check (confidence >= 0 and confidence <= 1),
            status text not null default 'active' check (status in ('active', 'auto', 'manual', 'queued', 'rerun', 'weak', 'used_for_judgment', 'irrelevant', 'false_positive', 'pending')),
            contributed integer not null default 1 check (contributed in (0, 1)),
            payload text not null check (json_valid(payload)),
            foreign key (submission_id) references submissions(id) on delete cascade
        )
    """,
    "providers": """
        create table {table} (
            id text primary key,
            name text not null default '',
            enabled integer not null default 0 check (enabled in (0, 1)),
            usage integer not null default 0 check (usage >= 0),
            quota integer not null default 0 check (quota >= 0),
            payload text not null check (json_valid(payload))
        )
    """,
    "knowledge_entries": """
        create table {table} (
            id text primary key,
            name text not null default '',
            entry_type text not null default 'other',
            entry_status text not null default 'confirmed' check (entry_status in ('confirmed', 'watchlist', 'excluded')),
            active integer not null default 1 check (active in (0, 1)),
            source_submission_id text not null default '',
            payload text not null check (json_valid(payload))
        )
    """,
    "collection_candidates": """
        create table {table} (
            id text primary key,
            provider text not null default '',
            query text not null default '',
            status text not null default 'candidate' check (status in ('candidate', 'promoted')),
            collected_epoch integer not null default 0 check (collected_epoch >= 0),
            payload text not null check (json_valid(payload))
        )
    """,
    "corrections": """
        create table {table} (
            id text primary key,
            decision_id text not null default '',
            status text not null default '',
            payload text not null check (json_valid(payload))
        )
    """,
    "audit_events": """
        create table {table} (
            id integer primary key autoincrement,
            timestamp text not null default '',
            actor text not null default '',
            event text not null default '',
            object_ref text not null default '',
            payload text not null check (json_valid(payload))
        )
    """,
}
LEGACY_REBUILD_ORDER = (
    "submissions",
    "providers",
    "knowledge_entries",
    "collection_candidates",
    "corrections",
    "audit_events",
    "evidence",
)
TYPED_COLUMNS = {
    "submissions": [
        ("title", "text not null default ''"),
        ("risk_score", "integer not null default 0"),
        ("risk_band", "text not null default 'low'"),
        ("decision_status", "text not null default 'unreviewed'"),
        ("submitted_epoch", "integer not null default 0"),
        ("queue_id", "text not null default ''"),
    ],
    "evidence": [
        ("source", "text not null default ''"),
        ("confidence", "real not null default 0"),
        ("status", "text not null default 'active'"),
        ("contributed", "integer not null default 1"),
    ],
    "providers": [
        ("name", "text not null default ''"),
        ("enabled", "integer not null default 0"),
        ("usage", "integer not null default 0"),
        ("quota", "integer not null default 0"),
    ],
    "knowledge_entries": [
        ("name", "text not null default ''"),
        ("entry_type", "text not null default 'other'"),
        ("entry_status", "text not null default 'confirmed'"),
        ("active", "integer not null default 1"),
        ("source_submission_id", "text not null default ''"),
    ],
    "collection_candidates": [
        ("provider", "text not null default ''"),
        ("query", "text not null default ''"),
        ("status", "text not null default 'candidate'"),
        ("collected_epoch", "integer not null default 0"),
    ],
    "corrections": [
        ("decision_id", "text not null default ''"),
        ("status", "text not null default ''"),
    ],
    "audit_events": [
        ("timestamp", "text not null default ''"),
        ("actor", "text not null default ''"),
        ("event", "text not null default ''"),
        ("object_ref", "text not null default ''"),
    ],
}


def _ensure_typed_columns(conn: sqlite3.Connection) -> None:
    for table, columns in TYPED_COLUMNS.items():
        existing = {
            str(row["name"])
            for row in conn.execute(f"pragma table_info({table})").fetchall()
        }
        for name, definition in columns:
            if name not in existing:
                conn.execute(f"alter table {table} add column {name} {definition}")


def _ensure_schema_version(conn: sqlite3.Connection) -> None:
    current = int(conn.execute("pragma user_version").fetchone()[0])
    if current < SCHEMA_VERSION:
        conn.execute(f"pragma user_version = {SCHEMA_VERSION}")


def _ensure_queue_schema(conn: sqlite3.Connection) -> None:
    conn.execute(
        """
        create table if not exists submission_queues (
            id text primary key,
            folder_path text not null unique,
            label text not null default '',
            is_active integer not null default 0 check (is_active in (0, 1)),
            created_at text not null default '',
            created_epoch integer not null default 0 check (created_epoch >= 0),
            last_imported_epoch integer not null default 0 check (last_imported_epoch >= 0),
            last_imported_at text not null default ''
        )
        """
    )
    conn.execute(
        "create index if not exists idx_submission_queues_active on submission_queues(is_active, id)"
    )


def _drop_stale_rebuild_temp_tables(conn: sqlite3.Connection) -> None:
    rows = conn.execute(
        "select name from sqlite_master where type = 'table' and name like '\\_\\_copyrighter\\_%\\_new' escape '\\'"
    ).fetchall()
    for row in rows:
        conn.execute(f"drop table if exists {row['name']}")


def _ensure_constrained_schema(conn: sqlite3.Connection) -> None:
    pending = [
        table for table in LEGACY_REBUILD_ORDER if _table_needs_constraint_rebuild(conn, table)
    ]
    if not pending:
        return
    # Disable foreign-key enforcement for the duration of the rebuilds. The
    # rebuild drops each parent table before its children are recreated, and a
    # `drop table submissions` while foreign keys are ON would fire evidence's
    # `on delete cascade` and silently wipe every evidence row. Toggling FKs off
    # (the procedure SQLite documents for constraint-changing migrations) must
    # happen with no transaction pending, so commit first and restore on exit.
    conn.commit()
    # Clean up any half-built temp table left by a prior crashed/failed run so we
    # retry cleanly instead of tripping on an orphan.
    _drop_stale_rebuild_temp_tables(conn)
    conn.execute("pragma foreign_keys = off")
    try:
        for table in pending:
            _rebuild_constrained_table(conn, table)
        conn.commit()
    except Exception:
        # The copy step raises before the original table is dropped (e.g. legacy
        # rows violate the new CHECK), so the source data is intact; remove the
        # orphaned temp table so the next startup can retry.
        _drop_stale_rebuild_temp_tables(conn)
        conn.commit()
        raise
    finally:
        conn.execute("pragma foreign_keys = on")


def _table_needs_constraint_rebuild(conn: sqlite3.Connection, table: str) -> bool:
    row = conn.execute(
        "select sql from sqlite_master where type = 'table' and name = ?",
        (table,),
    ).fetchone()
    if row is None:
        return False
    sql = str(row["sql"] or "").lower()
    if "json_valid(payload)" not in sql:
        return True
    if table == "evidence" and "foreign key" not in sql:
        return True
    if table == "submissions" and "risk_band in" not in sql:
        return True
    return False


def _rebuild_constrained_table(conn: sqlite3.Connection, table: str) -> None:
    schema = CONSTRAINED_TABLE_SCHEMAS[table]
    temp_table = f"__copyrighter_{table}_new"
    conn.execute(f"drop table if exists {temp_table}")
    conn.execute(schema.format(table=temp_table))

    source_columns = _table_columns(conn, table)
    target_columns = _table_columns(conn, temp_table)
    copied_columns = [column for column in target_columns if column in source_columns]
    if copied_columns:
        column_sql = ", ".join(copied_columns)
        conn.execute(
            f"insert into {temp_table} ({column_sql}) select {column_sql} from {table}"
        )
    conn.execute(f"drop table {table}")
    conn.execute(f"alter table {temp_table} rename to {table}")


def _table_columns(conn: sqlite3.Connection, table: str) -> list[str]:
    return [str(row["name"]) for row in conn.execute(f"pragma table_info({table})").fetchall()]
