"""
Programmatic Alembic migration runner for per-user encrypted databases.

This module provides functions to run Alembic migrations against SQLCipher
encrypted databases without using the Alembic CLI. Each user database
tracks its own migration version via the alembic_version table.
"""

import os
from pathlib import Path
from typing import Optional

from alembic import command
from alembic.config import Config
from alembic.runtime.migration import MigrationContext
from alembic.script import ScriptDirectory
from loguru import logger
from sqlalchemy import Engine, inspect


def get_migrations_dir() -> Path:
    """
    Get the path to the migrations directory with security validation.

    Validates that the migrations directory is within the expected package
    boundary to prevent symlink attacks that could redirect migration loading
    to arbitrary locations.

    Returns:
        Path to the migrations directory

    Raises:
        ValueError: If the migrations path is outside expected boundaries
    """
    migrations_dir = Path(__file__).parent / "migrations"
    real_path = migrations_dir.resolve()
    expected_parent = Path(__file__).parent.resolve()

    # Security: Ensure migrations directory is within expected package boundary
    # This prevents symlink attacks that could load arbitrary Python code
    if not real_path.is_relative_to(expected_parent):
        raise ValueError(
            "Invalid migrations path (possible symlink attack): "
            "migrations dir resolves outside expected package boundary"
        )

    return migrations_dir


def _validate_migrations_permissions(migrations_dir: Path) -> None:
    """
    Validate migration files are not world-writable.

    World-writable migration files could be replaced with malicious code
    that would execute during database migrations with the application's
    privileges.

    Args:
        migrations_dir: Path to the migrations directory

    Raises:
        ValueError: If any migration file is world-writable

    Note:
        This check is skipped on Windows where file permissions work differently.
    """
    if os.name == "nt":  # Skip permission checks on Windows
        return

    versions_dir = migrations_dir / "versions"
    if not versions_dir.exists():
        return

    # Check the versions directory itself
    st = versions_dir.stat()
    if st.st_mode & 0o002:
        raise ValueError(
            f"Migrations directory has insecure permissions (world-writable): "
            f"{versions_dir}. Fix with: chmod o-w {versions_dir}"
        )

    for migration_file in versions_dir.glob("*.py"):
        st = migration_file.stat()
        if st.st_mode & 0o002:  # World-writable bit
            raise ValueError(
                f"Migration file has insecure permissions (world-writable): "
                f"{migration_file.name}. "
                f"Fix with: chmod o-w {migration_file}"
            )


def get_alembic_config(engine: Engine) -> Config:
    """
    Create an Alembic Config object for programmatic usage.

    Args:
        engine: SQLAlchemy engine to run migrations against

    Returns:
        Configured Alembic Config object
    """
    migrations_dir = get_migrations_dir()

    # Create config object without ini file
    config = Config()

    # Set script location
    config.set_main_option("script_location", str(migrations_dir))

    # Set SQLAlchemy URL (not actually used since we pass connection directly)
    # But Alembic requires it to be set
    config.set_main_option("sqlalchemy.url", "sqlite:///:memory:")

    return config


def get_current_revision(engine: Engine) -> Optional[str]:
    """
    Get the current migration revision for a database.

    Args:
        engine: SQLAlchemy engine

    Returns:
        Current revision string or None if no migrations have run
    """
    with engine.connect() as conn:
        context = MigrationContext.configure(conn)
        return context.get_current_revision()


def get_head_revision() -> Optional[str]:
    """
    Get the latest migration revision.

    Returns:
        Head revision string, or None if no migrations exist
    """
    migrations_dir = get_migrations_dir()
    config = Config()
    config.set_main_option("script_location", str(migrations_dir))

    script = ScriptDirectory.from_config(config)
    return script.get_current_head()


def needs_migration(engine: Engine) -> bool:
    """
    Check if a database needs migrations.

    Args:
        engine: SQLAlchemy engine

    Returns:
        True if migrations are pending
    """
    head = get_head_revision()

    if head is None:
        # No migrations exist yet
        return False

    current = get_current_revision(engine)

    if current is None:
        # Check if this is a fresh database or existing without migrations
        inspector = inspect(engine)
        tables = inspector.get_table_names()

        if not tables:
            # Fresh database, needs initial migration
            return True
        if "alembic_version" not in tables:
            # Existing database without Alembic - needs stamping then check
            return True

    return current != head


def stamp_database(engine: Engine, revision: str = "head") -> None:
    """
    Stamp a database with a revision without running migrations.
    Used for baselining existing databases.

    Args:
        engine: SQLAlchemy engine
        revision: Revision to stamp (default "head")
    """
    config = get_alembic_config(engine)

    with engine.begin() as conn:
        config.attributes["connection"] = conn
        command.stamp(config, revision)

    logger.info(f"Stamped database at revision: {revision}")


def run_migrations(engine: Engine, target: str = "head") -> None:
    """
    Run pending migrations on a database.

    The initial migration is idempotent (only creates tables that don't exist),
    so this function always runs migrations rather than just stamping existing
    databases. This ensures any missing tables are created.

    Security validations performed before running migrations:
    - Migration directory path is within expected package boundary
    - Migration files are not world-writable

    On failure, the transaction is automatically rolled back by
    ``engine.begin()``'s context manager — the database stays at its
    previous revision.  The original exception is re-raised so callers
    can decide how to handle it.

    Args:
        engine: SQLAlchemy engine to migrate
        target: Target revision (default "head" for latest)

    Raises:
        Exception: If migration fails (database is safely rolled back)
    """
    # Security: Validate migrations directory and file permissions
    migrations_dir = get_migrations_dir()
    _validate_migrations_permissions(migrations_dir)

    head = get_head_revision()

    if head is None:
        # No migrations exist yet - nothing to do
        logger.debug("No migrations found, skipping")
        return

    current = get_current_revision(engine)

    if current is None:
        logger.warning(
            "Database has no migration history — applying migrations "
            f"(target={target})"
        )
    elif current != head and target == "head":
        logger.warning(
            f"Database schema outdated (revision {current}, "
            f"head is {head}) — applying migrations"
        )

    config = get_alembic_config(engine)

    try:
        with engine.begin() as conn:
            config.attributes["connection"] = conn
            command.upgrade(config, target)
    except Exception:
        logger.exception(
            "Database migration failed — database remains at previous "
            "revision (auto-rollback by transaction manager)"
        )
        raise

    new_revision = get_current_revision(engine)
    if current != new_revision:
        logger.warning(f"Database migrated: {current} -> {new_revision}")
    else:
        logger.info(f"Database already at revision {new_revision}")
