from collections.abc import Generator
from contextlib import contextmanager

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

from app.core.config import get_settings


Base = declarative_base()

_engine_cache: dict[str, Engine] = {}
_session_factory_cache: dict[str, sessionmaker[Session]] = {}
_prepared_urls: set[str] = set()
SQLITE_BUSY_TIMEOUT_MS = 30000


def _connect_args(database_url: str) -> dict[str, object]:
    if database_url.startswith("sqlite"):
        return {"check_same_thread": False, "timeout": SQLITE_BUSY_TIMEOUT_MS / 1000}
    return {}


def _configure_sqlite_engine(engine: Engine, database_url: str) -> None:
    if not database_url.startswith("sqlite"):
        return

    @event.listens_for(engine, "connect")
    def _set_sqlite_pragmas(dbapi_connection, _connection_record) -> None:
        cursor = dbapi_connection.cursor()
        try:
            cursor.execute(f"PRAGMA busy_timeout = {SQLITE_BUSY_TIMEOUT_MS};")
            cursor.execute("PRAGMA foreign_keys = ON;")
            cursor.execute("PRAGMA journal_mode = WAL;")
            cursor.execute("PRAGMA synchronous = NORMAL;")
        finally:
            cursor.close()


def get_engine(database_url: str | None = None) -> Engine:
    target_url = database_url or get_settings().database_url
    engine = _engine_cache.get(target_url)
    if engine is None:
        engine = create_engine(
            target_url,
            future=True,
            pool_pre_ping=not target_url.startswith("sqlite"),
            connect_args=_connect_args(target_url),
        )
        _configure_sqlite_engine(engine, target_url)
        _engine_cache[target_url] = engine

    return engine


def prepare_database(database_url: str | None = None) -> Engine:
    target_url = database_url or get_settings().database_url
    engine = get_engine(target_url)
    if target_url not in _prepared_urls:
        Base.metadata.create_all(bind=engine)
        _prepared_urls.add(target_url)
    return engine


def _get_session_factory(database_url: str | None = None) -> sessionmaker[Session]:
    target_url = database_url or get_settings().database_url
    session_factory = _session_factory_cache.get(target_url)
    if session_factory is None:
        session_factory = sessionmaker(bind=prepare_database(target_url), autoflush=False, autocommit=False, future=True)
        _session_factory_cache[target_url] = session_factory
    return session_factory


def open_db_session(database_url: str | None = None) -> Generator[Session, None, None]:
    session_factory = _get_session_factory(database_url)
    db = session_factory()
    try:
        yield db
    finally:
        db.close()


@contextmanager
def db_session_context(database_url: str | None = None) -> Generator[Session, None, None]:
    session_factory = _get_session_factory(database_url)
    db = session_factory()
    try:
        yield db
    finally:
        db.close()
