"""Schema migration runner with version tracking.

Migrations are registered as numbered functions and applied in order.
Version state is tracked in the _schema_metadata table.
"""
from __future__ import annotations

import importlib
import pkgutil
from typing import Any, Callable, Protocol


class MigrationDB(Protocol):
    """Minimal DB interface needed by migrations."""
    def execute(self, query: str, params: tuple = ()) -> list[dict[str, Any]]: ...
    def execute_one(self, query: str, params: tuple = ()) -> dict[str, Any] | None: ...


_MIGRATIONS: dict[int, tuple[str, Callable[[MigrationDB], None]]] = {}


def register(version: int, description: str):
    """Decorator to register a migration function."""
    def decorator(fn: Callable[[MigrationDB], None]):
        _MIGRATIONS[version] = (description, fn)
        return fn
    return decorator


def get_current_version(db: MigrationDB) -> int:
    """Read current schema version from metadata table."""
    try:
        row = db.execute_one(
            "SELECT value FROM _schema_metadata WHERE key = ?",
            ("schema_version",),
        )
        return int(row["value"]) if row else 0
    except Exception:
        return 0


def _discover_migrations() -> None:
    """Auto-import all migration modules so @register decorators fire."""
    if getattr(_discover_migrations, "_done", False):
        return
    for importer, modname, ispkg in pkgutil.iter_modules(__path__):
        if modname[0].isdigit():
            importlib.import_module(f".{modname}", __package__)
    _discover_migrations._done = True  # type: ignore[attr-defined]


def get_pending(db: MigrationDB) -> list[tuple[int, str, Callable]]:
    """Return migrations that haven't been applied yet."""
    _discover_migrations()
    current = get_current_version(db)
    pending = []
    for version in sorted(_MIGRATIONS.keys()):
        if version > current:
            desc, fn = _MIGRATIONS[version]
            pending.append((version, desc, fn))
    return pending


def _ensure_metadata_table(db: MigrationDB) -> None:
    """Create the metadata table if it doesn't exist."""
    db.execute(
        "CREATE TABLE IF NOT EXISTS _schema_metadata "
        "(key TEXT PRIMARY KEY, value TEXT, updated_at TEXT DEFAULT (datetime('now')))"
    )


def apply_all(db: MigrationDB) -> list[tuple[int, str]]:
    """Apply all pending migrations. Returns list of (version, description) applied."""
    _ensure_metadata_table(db)
    applied = []
    for version, desc, fn in get_pending(db):
        fn(db)
        db.execute(
            "INSERT INTO _schema_metadata (key, value) VALUES (?, ?) "
            "ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')",
            ("schema_version", str(version), str(version)),
        )
        applied.append((version, desc))
    return applied
