from __future__ import annotations

import sqlite3
import sys
from pathlib import Path

from alembic import command
from alembic.config import Config
from alembic.script import ScriptDirectory
from alembic.util.exc import CommandError

PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from app.config import get_settings

APP_BASELINE_REVISION = "20260625_14_meter_uuid"


class AppDatabaseAdoptionError(RuntimeError):
    """Raised when the app database is missing or not managed as expected."""


def _database_path_from_url(database_url: str) -> Path:
    prefix = "sqlite:///"
    if not database_url.startswith(prefix):
        raise AppDatabaseAdoptionError(
            f"Only sqlite URLs are supported for app DB initialization, got: {database_url}"
        )
    return Path(database_url[len(prefix) :])


def _make_alembic_config(database_url: str) -> Config:
    config = Config("alembic_app.ini")
    config.set_main_option("sqlalchemy.url", database_url)
    return config


def _expected_head_revision(alembic_config: Config) -> str:
    script = ScriptDirectory.from_config(alembic_config)
    heads = script.get_heads()
    if len(heads) != 1:
        raise AppDatabaseAdoptionError(
            f"Expected exactly one Alembic head for app DB, got {len(heads)}"
        )
    return heads[0]


def _is_known_revision(alembic_config: Config, revision: str) -> bool:
    script = ScriptDirectory.from_config(alembic_config)
    try:
        return script.get_revision(revision) is not None
    except CommandError:
        return False


def _alembic_version_table_exists(database_path: Path) -> bool:
    conn = sqlite3.connect(database_path)
    try:
        row = conn.execute(
            "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'alembic_version'"
        ).fetchone()
        return row is not None
    finally:
        conn.close()


def _fetch_alembic_revision(database_path: Path) -> str:
    conn = sqlite3.connect(database_path)
    try:
        row = conn.execute("SELECT version_num FROM alembic_version").fetchone()
        if row is None:
            raise AppDatabaseAdoptionError("Alembic version table exists but contains no revision")
        return row[0]
    finally:
        conn.close()


def _list_user_tables(database_path: Path) -> list[str]:
    conn = sqlite3.connect(database_path)
    try:
        rows = conn.execute(
            """
            SELECT name
            FROM sqlite_master
            WHERE type = 'table'
              AND name NOT LIKE 'sqlite_%'
            """
        ).fetchall()
        return sorted(row[0] for row in rows)
    finally:
        conn.close()


def validate_app_runtime_db(database_url: str) -> None:
    database_path = _database_path_from_url(database_url)
    alembic_config = _make_alembic_config(database_url)
    expected_revision = _expected_head_revision(alembic_config)
    if not database_path.exists():
        raise AppDatabaseAdoptionError(
            "App DB file was not found. Run 'python scripts/app_db_adopt.py' first to "
            "initialize the app DB before starting the app."
        )

    if not _alembic_version_table_exists(database_path):
        raise AppDatabaseAdoptionError(
            "App DB exists but is not yet Alembic-managed. Run "
            "'python scripts/app_db_adopt.py' first before starting the app."
        )

    current_revision = _fetch_alembic_revision(database_path)
    if current_revision != expected_revision:
        raise AppDatabaseAdoptionError(
            "App DB revision mismatch. Refusing to start the app: "
            f"expected {expected_revision}, got {current_revision}"
        )


def adopt_or_initialize_app_db(database_url: str) -> str:
    database_path = _database_path_from_url(database_url)
    alembic_config = _make_alembic_config(database_url)
    expected_revision = _expected_head_revision(alembic_config)

    if database_path.exists():
        if _alembic_version_table_exists(database_path):
            current_revision = _fetch_alembic_revision(database_path)
            if current_revision == expected_revision:
                return "already_managed"
            if not _is_known_revision(alembic_config, current_revision):
                raise AppDatabaseAdoptionError(
                    "App DB is already Alembic-managed but revision does not match "
                    f"a known migration revision: got {current_revision}"
                )
            command.upgrade(alembic_config, "head")
            return "upgraded"

        existing_tables = _list_user_tables(database_path)
        if existing_tables:
            raise AppDatabaseAdoptionError(
                "App DB exists with unmanaged tables. Refusing to continue because there is "
                "no legacy app DB adoption path in this revision."
            )

    database_path.parent.mkdir(parents=True, exist_ok=True)
    command.upgrade(alembic_config, "head")
    return "initialized"


def main() -> None:
    settings = get_settings()
    result = adopt_or_initialize_app_db(settings.app_database_url)
    if result == "initialized":
        print("Initialized a new app DB via Alembic upgrade head.")
    elif result == "upgraded":
        print("Upgraded existing app DB to the expected Alembic head revision.")
    else:
        print("App DB is already Alembic-managed at the expected baseline revision.")


if __name__ == "__main__":
    main()
