"""SQLAlchemy engine and session factory for AI MusiMuse.

This module creates the database engine, a session factory, and an
initialization function.  SQLite is the only supported backend.

Tables are created automatically from the models defined in
:mod:`database.models` via ``Base.metadata.create_all()``.
"""

from __future__ import annotations

from collections.abc import Generator
from pathlib import Path

from sqlalchemy import create_engine
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session, sessionmaker

from database.base import Base


def create_database_engine(database_url: str) -> Engine:
    """Create a SQLAlchemy engine for the given database URL.

    Args:
        database_url: SQLAlchemy database URL.  Must be SQLite.

    Returns:
        A SQLAlchemy :class:`Engine` instance.
    """
    if not database_url.startswith("sqlite"):
        from core.exceptions import ConfigurationError

        raise ConfigurationError(f"Only SQLite is supported. Got URL: {database_url}")

    if database_url.startswith("sqlite:///"):
        db_path = database_url.replace("sqlite:///", "", 1)
        if db_path and db_path != ":memory:":
            Path(db_path).parent.mkdir(parents=True, exist_ok=True)

    return create_engine(
        database_url,
        echo=False,
        future=True,
    )


_engine: Engine | None = None
_SessionLocal: sessionmaker[Session] | None = None


def get_engine(database_url: str | None = None) -> Engine:
    """Return the global database engine, creating it if necessary.

    Args:
        database_url: Database URL.  If ``None``, the engine must have
            been previously initialized.

    Returns:
        A SQLAlchemy :class:`Engine` instance.

    Raises:
        RuntimeError: If no URL is provided and the engine has not been
            initialized.
    """
    global _engine
    if _engine is None:
        if database_url is None:
            raise RuntimeError(
                "Database engine has not been initialized. "
                "Provide a database_url or call init_db first."
            )
        _engine = create_database_engine(database_url)
    return _engine


def get_session_factory() -> sessionmaker[Session]:
    """Return the global session factory.

    Returns:
        A SQLAlchemy :class:`sessionmaker` bound to the global engine.

    Raises:
        RuntimeError: If the engine has not been initialized.
    """
    global _SessionLocal
    if _SessionLocal is None:
        engine = get_engine()
        _SessionLocal = sessionmaker(
            bind=engine,
            expire_on_commit=False,
            future=True,
        )
    return _SessionLocal


def init_db(database_url: str) -> Engine:
    """Initialize the database engine and create all defined tables.

    This function creates the SQLite database file (if needed) and
    runs ``Base.metadata.create_all()``.  At this stage no tables are
    defined, so only the database file is created.

    Args:
        database_url: SQLAlchemy database URL.  Must be SQLite.

    Returns:
        The initialized :class:`Engine` instance.
    """
    global _engine, _SessionLocal
    _engine = create_database_engine(database_url)
    _SessionLocal = sessionmaker(
        bind=_engine,
        expire_on_commit=False,
        future=True,
    )
    Base.metadata.create_all(_engine)
    return _engine


def get_session() -> Generator[Session, None, None]:
    """Yield a database session and ensure it is closed.

    Yields:
        A SQLAlchemy :class:`Session` instance.

    Raises:
        RuntimeError: If the session factory has not been initialized.
    """
    session_factory = get_session_factory()
    session = session_factory()
    try:
        yield session
    finally:
        session.close()
