"""
Alembic environment configuration for Dazzle.

Reads DATABASE_URL from the environment and uses the EntitySpec → SQLAlchemy
MetaData bridge as ``target_metadata`` so that ``--autogenerate`` can diff
the DSL schema against the live database.
"""

import logging
import os
import sys
from logging.config import fileConfig
from pathlib import Path
from typing import Any

import sqlalchemy
from alembic import context
from alembic.operations import ops as alembic_ops

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Alembic Config object (provides access to alembic.ini values)
# ---------------------------------------------------------------------------
config = context.config

# Interpret the config file for Python logging (if present).
if config.config_file_name and os.path.exists(config.config_file_name):
    fileConfig(config.config_file_name)

# ---------------------------------------------------------------------------
# Target metadata — built from DSL EntitySpecs
# ---------------------------------------------------------------------------

# Ensure the project src/ is on sys.path so imports work.
_src = str(Path(__file__).resolve().parents[2])  # src/
if _src not in sys.path:
    sys.path.insert(0, _src)


def _load_target_metadata() -> sqlalchemy.MetaData:
    """Lazily load target metadata from the active Dazzle project.

    Falls back to an empty MetaData if no project entities are available
    (e.g. when running ``alembic current`` without a project context).
    """
    try:
        # Try to load entities from the project in CWD
        from dazzle.core.dsl_parser import parse_modules
        from dazzle.core.fileset import discover_dsl_files
        from dazzle.core.linker import build_appspec
        from dazzle.core.manifest import load_manifest
        from dazzle_back.runtime.sa_schema import build_metadata

        project_root = Path.cwd()
        manifest_path = project_root / "dazzle.toml"
        if manifest_path.exists():
            manifest = load_manifest(manifest_path)
            dsl_files = discover_dsl_files(project_root, manifest)
            if dsl_files:
                modules = parse_modules(dsl_files)
                appspec = build_appspec(modules, str(project_root))
                from dazzle_back.converters.entity_converter import convert_entities

                entities = convert_entities(appspec.domain.entities)
                return build_metadata(entities)
    except Exception:
        logger.debug("Failed to load target metadata from DSL", exc_info=True)

    # Fallback: empty metadata
    import sqlalchemy

    return sqlalchemy.MetaData()


target_metadata = _load_target_metadata()

# ---------------------------------------------------------------------------
# Database URL
# ---------------------------------------------------------------------------


def _get_url() -> str:
    """Get database URL from env or alembic config.

    Ensures the URL uses the psycopg (v3) driver for SQLAlchemy.
    """
    url = os.environ.get("DATABASE_URL") or config.get_main_option("sqlalchemy.url", "")
    # Ensure SQLAlchemy uses psycopg v3, not psycopg2
    if url.startswith("postgresql://"):
        url = url.replace("postgresql://", "postgresql+psycopg://", 1)
    return url


# ---------------------------------------------------------------------------
# Revision directive hook
# ---------------------------------------------------------------------------


def _process_revision_directives(context: Any, revision: Any, directives: list[Any]) -> None:
    """Post-process autogenerated migration directives.

    1. Suppress empty revisions (no-op when DSL hasn't changed).
    2. Inject postgresql_using clauses for safe type casts.
    """
    if not directives:
        return

    script = directives[0]
    if script.upgrade_ops is None:
        return

    # Suppress empty migrations
    if script.upgrade_ops.is_empty():
        directives[:] = []
        return

    # Inject USING clauses for safe type casts
    try:
        from dazzle_back.runtime.safe_casts import get_using_clause
    except ImportError:
        return

    for op in script.upgrade_ops.ops:
        if not isinstance(op, alembic_ops.ModifyTableOps):
            continue
        for sub_op in op.ops:
            if not isinstance(sub_op, alembic_ops.AlterColumnOp):
                continue
            if sub_op.modify_type is None or sub_op.existing_type is None:
                continue
            # Get Postgres type names from SA types
            from_name = sub_op.existing_type.__class__.__name__.upper()
            to_name = sub_op.modify_type.__class__.__name__.upper()
            using = get_using_clause(from_name, to_name, sub_op.column_name)
            if using:
                sub_op.kw["postgresql_using"] = using


# ---------------------------------------------------------------------------
# Migration runners
# ---------------------------------------------------------------------------


def run_migrations_offline() -> None:
    """Run migrations in 'offline' mode (emit SQL without connecting)."""
    url = _get_url()
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
        compare_type=True,
        process_revision_directives=_process_revision_directives,
    )

    with context.begin_transaction():
        context.run_migrations()


def run_migrations_online() -> None:
    """Run migrations against a live database connection."""
    from sqlalchemy import create_engine

    url = _get_url()
    if not url:
        raise RuntimeError("DATABASE_URL environment variable is required for online migrations.")

    connectable = create_engine(url)

    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata,
            compare_type=True,
            process_revision_directives=_process_revision_directives,
        )

        with context.begin_transaction():
            tenant_schema = config.attributes.get("tenant_schema")
            if tenant_schema:
                import re

                # Validate: slug_to_schema_name() guarantees alphanumeric +
                # underscore only, but we enforce it here too so this file is
                # safe regardless of call-site discipline.
                if not re.fullmatch(r"[a-zA-Z0-9_]+", tenant_schema):
                    raise ValueError(
                        f"Invalid tenant_schema {tenant_schema!r}: "
                        "must be alphanumeric and underscores only"
                    )
                # Use the raw DBAPI cursor to SET search_path so that SQLAlchemy's
                # text() pathway is not involved (avoids taint-analysis false positives).
                # The schema name is validated as a safe identifier above.
                dbapi_conn: Any = connection.connection
                cur = dbapi_conn.cursor()
                try:
                    cur.execute("SET search_path TO %s, public", (tenant_schema,))
                finally:
                    cur.close()
            context.run_migrations()

    connectable.dispose()


if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()
