from __future__ import annotations

import json
import sqlite3
from contextlib import contextmanager
from pathlib import Path


SCHEMA = """
PRAGMA foreign_keys=ON;
CREATE TABLE IF NOT EXISTS users (
 id INTEGER PRIMARY KEY, username TEXT NOT NULL UNIQUE COLLATE NOCASE,
 email TEXT UNIQUE COLLATE NOCASE, password_hash TEXT NOT NULL,
 created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS sessions (
 token_hash TEXT PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
 expires_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS api_tokens (
 token_hash TEXT PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
 label TEXT NOT NULL, last_used_at TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS projects (
 id INTEGER PRIMARY KEY, owner_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
 slug TEXT NOT NULL COLLATE NOCASE, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '',
 repository_url TEXT, spec_path TEXT, visibility TEXT NOT NULL DEFAULT 'public',
 created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
 UNIQUE(owner_user_id, slug)
);
CREATE TABLE IF NOT EXISTS releases (
 id INTEGER PRIMARY KEY, application TEXT NOT NULL, version TEXT NOT NULL,
 project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
 repository_url TEXT, source_commit TEXT, spec_path TEXT,
 artifact_url TEXT, artifact_name TEXT, artifact_sha256 TEXT, evidence_sha256 TEXT,
 signing_public_key TEXT, signature TEXT, channel TEXT NOT NULL DEFAULT 'stable',
 created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(application, version)
);
CREATE TABLE IF NOT EXISTS spec_functions (
 id INTEGER PRIMARY KEY, release_id INTEGER NOT NULL REFERENCES releases(id),
 name TEXT NOT NULL, UNIQUE(release_id, name)
);
CREATE TABLE IF NOT EXISTS implementations (
 pk INTEGER PRIMARY KEY, release_id INTEGER NOT NULL REFERENCES releases(id),
 id TEXT NOT NULL, spec_function_id INTEGER NOT NULL REFERENCES spec_functions(id),
 name TEXT NOT NULL, provenance TEXT NOT NULL DEFAULT '{}', status TEXT NOT NULL DEFAULT 'active',
 status_reason TEXT NOT NULL DEFAULT '', status_changed_at TEXT,
 UNIQUE(release_id, id)
);
CREATE TABLE IF NOT EXISTS clients (
 id TEXT PRIMARY KEY, installation_hash TEXT, release_id INTEGER NOT NULL REFERENCES releases(id),
 strategy TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
 last_seen_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(installation_hash, release_id)
);
CREATE TABLE IF NOT EXISTS compositions (
 id INTEGER PRIMARY KEY, client_id TEXT NOT NULL REFERENCES clients(id), generation INTEGER NOT NULL,
 created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, reason TEXT NOT NULL,
 replaced_at TEXT, UNIQUE(client_id, generation)
);
CREATE TABLE IF NOT EXISTS composition_items (
 composition_id INTEGER NOT NULL REFERENCES compositions(id), spec_function_id INTEGER NOT NULL REFERENCES spec_functions(id),
 implementation_pk INTEGER NOT NULL REFERENCES implementations(pk), PRIMARY KEY(composition_id, spec_function_id)
);
CREATE TABLE IF NOT EXISTS executions (
 id INTEGER PRIMARY KEY, client_id TEXT NOT NULL REFERENCES clients(id), composition_id INTEGER NOT NULL REFERENCES compositions(id),
 operation TEXT NOT NULL, success INTEGER NOT NULL, duration_ms REAL, error_kind TEXT,
 created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS calls (
 id INTEGER PRIMARY KEY, execution_id INTEGER NOT NULL REFERENCES executions(id), ordinal INTEGER NOT NULL,
 spec_function TEXT NOT NULL, implementation_id TEXT NOT NULL, outcome TEXT NOT NULL, duration_ms REAL
);
CREATE TABLE IF NOT EXISTS telemetry_events (
 id INTEGER PRIMARY KEY, client_id TEXT NOT NULL REFERENCES clients(id), composition_id INTEGER NOT NULL REFERENCES compositions(id),
 execution_id INTEGER REFERENCES executions(id), event_type TEXT NOT NULL, spec_function TEXT,
 implementation_id TEXT, message TEXT, recent_calls TEXT NOT NULL DEFAULT '[]',
 created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS status_changes (
 id INTEGER PRIMARY KEY, implementation_pk INTEGER NOT NULL REFERENCES implementations(pk), old_status TEXT NOT NULL,
 new_status TEXT NOT NULL, reason TEXT NOT NULL, affected_clients INTEGER NOT NULL DEFAULT 0,
 execution_cutoff INTEGER NOT NULL DEFAULT 0,
 changed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE INDEX IF NOT EXISTS ix_items_impl ON composition_items(implementation_pk);
CREATE INDEX IF NOT EXISTS ix_exec_client ON executions(client_id);
CREATE INDEX IF NOT EXISTS ix_events_client ON telemetry_events(client_id);
"""


class Database:
    def __init__(self, path: str):
        self.path = path

    def initialize(self) -> None:
        if self.path != ":memory:":
            Path(self.path).parent.mkdir(parents=True, exist_ok=True)
        with sqlite3.connect(self.path, timeout=10) as con:
            con.row_factory = sqlite3.Row
            release_columns = {row[1] for row in con.execute("PRAGMA table_info(releases)")}
            if release_columns and "project_id" not in release_columns:
                con.execute("ALTER TABLE releases ADD COLUMN project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE")
            columns = {row[1] for row in con.execute("PRAGMA table_info(implementations)")}
            if columns and "pk" not in columns:
                release_columns = {row[1] for row in con.execute("PRAGMA table_info(releases)")}
                if "project_id" not in release_columns:
                    con.execute("ALTER TABLE releases ADD COLUMN project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE")
                status_columns = {row[1] for row in con.execute("PRAGMA table_info(status_changes)")}
                if "execution_cutoff" not in status_columns:
                    con.execute("ALTER TABLE status_changes ADD COLUMN execution_cutoff INTEGER NOT NULL DEFAULT 0")
                    con.execute("""UPDATE status_changes SET execution_cutoff=(
                      SELECT coalesce(max(e.id),0) FROM executions e WHERE e.created_at < status_changes.changed_at)""")
                self._migrate_release_scoped_implementations(con)
            con.executescript(SCHEMA)
            release_columns = {row[1] for row in con.execute("PRAGMA table_info(releases)")}
            release_additions = {
                "project_id": "INTEGER REFERENCES projects(id) ON DELETE CASCADE",
                "repository_url": "TEXT", "source_commit": "TEXT", "spec_path": "TEXT",
                "artifact_url": "TEXT", "artifact_name": "TEXT", "artifact_sha256": "TEXT",
                "evidence_sha256": "TEXT", "signing_public_key": "TEXT", "signature": "TEXT",
                "channel": "TEXT NOT NULL DEFAULT 'stable'",
            }
            for name, definition in release_additions.items():
                if name not in release_columns:
                    con.execute(f"ALTER TABLE releases ADD COLUMN {name} {definition}")
            con.execute("CREATE INDEX IF NOT EXISTS ix_releases_project ON releases(project_id)")
            columns = {row[1] for row in con.execute("PRAGMA table_info(status_changes)")}
            if "execution_cutoff" not in columns:
                con.execute("ALTER TABLE status_changes ADD COLUMN execution_cutoff INTEGER NOT NULL DEFAULT 0")
                con.execute("""UPDATE status_changes SET execution_cutoff=(
                  SELECT coalesce(max(e.id),0) FROM executions e WHERE e.created_at < status_changes.changed_at)""")

    def _migrate_release_scoped_implementations(self, con: sqlite3.Connection) -> None:
        """Upgrade the original globally keyed implementation tables in place."""
        con.execute("PRAGMA foreign_keys=OFF")
        con.executescript(
            """
            BEGIN;
            ALTER TABLE composition_items RENAME TO composition_items_legacy;
            ALTER TABLE status_changes RENAME TO status_changes_legacy;
            ALTER TABLE implementations RENAME TO implementations_legacy;
            """
            + SCHEMA
            + """
            INSERT INTO implementations(
              release_id,id,spec_function_id,name,provenance,status,status_reason,status_changed_at
            )
            SELECT sf.release_id,i.id,i.spec_function_id,i.name,i.provenance,i.status,i.status_reason,i.status_changed_at
            FROM implementations_legacy i JOIN spec_functions sf ON sf.id=i.spec_function_id;

            INSERT INTO composition_items(composition_id,spec_function_id,implementation_pk)
            SELECT old.composition_id,old.spec_function_id,i.pk
            FROM composition_items_legacy old
            JOIN implementations i ON i.spec_function_id=old.spec_function_id AND i.id=old.implementation_id;

            INSERT INTO status_changes(
              id,implementation_pk,old_status,new_status,reason,affected_clients,execution_cutoff,changed_at
            )
            SELECT old.id,i.pk,old.old_status,old.new_status,old.reason,old.affected_clients,
                   old.execution_cutoff,old.changed_at
            FROM status_changes_legacy old JOIN implementations i ON i.id=old.implementation_id;

            DROP TABLE composition_items_legacy;
            DROP TABLE status_changes_legacy;
            DROP TABLE implementations_legacy;
            COMMIT;
            """
        )
        con.execute("PRAGMA foreign_keys=ON")

    @contextmanager
    def connect(self):
        con = sqlite3.connect(self.path, timeout=10, isolation_level=None)
        con.row_factory = sqlite3.Row
        con.execute("PRAGMA foreign_keys=ON")
        try:
            con.execute("BEGIN")
            yield con
            con.commit()
        except Exception:
            con.rollback()
            raise
        finally:
            con.close()


def as_dict(row):
    return dict(row) if row is not None else None


def decode_json(value: str):
    return json.loads(value)
