"""异步数据库连接与会话。

连接按当前配置惰性创建，测试可以安全地切换到临时 SQLite 数据库。
"""
from __future__ import annotations

import asyncio
from contextlib import asynccontextmanager
from typing import AsyncIterator, Optional
from weakref import WeakKeyDictionary

from sqlalchemy.ext.asyncio import (
    AsyncEngine,
    AsyncSession,
    async_sessionmaker,
    create_async_engine,
)

from .config import get_settings
from .models import Base

_engine: Optional[AsyncEngine] = None
_engine_url: Optional[str] = None
_session_factory: Optional[async_sessionmaker[AsyncSession]] = None
_sqlite_session_locks: WeakKeyDictionary[asyncio.AbstractEventLoop, asyncio.Lock] = (
    WeakKeyDictionary()
)


def _sqlite_session_lock() -> asyncio.Lock:
    """Return one transaction lock per event loop for the SQLite prototype.

    SQLite permits only one writer at a time. Serializing the complete
    session_scope transaction prevents the simulator and human control routes
    from reading the same event sequence and then racing on commit. PostgreSQL
    deployments skip this application-level lock.
    """

    loop = asyncio.get_running_loop()
    lock = _sqlite_session_locks.get(loop)
    if lock is None:
        lock = asyncio.Lock()
        _sqlite_session_locks[loop] = lock
    return lock


def get_engine() -> AsyncEngine:
    global _engine, _engine_url, _session_factory
    url = get_settings().database_url
    if _engine is None or _engine_url != url:
        _engine = create_async_engine(url, future=True)
        _engine_url = url
        _session_factory = async_sessionmaker(_engine, expire_on_commit=False)
    return _engine


def get_session_factory() -> async_sessionmaker[AsyncSession]:
    get_engine()
    assert _session_factory is not None
    return _session_factory


@asynccontextmanager
async def _transaction_scope() -> AsyncIterator[AsyncSession]:
    session = get_session_factory()()
    try:
        yield session
        await session.commit()
    except Exception:
        await session.rollback()
        raise
    finally:
        await session.close()


@asynccontextmanager
async def session_scope() -> AsyncIterator[AsyncSession]:
    if get_settings().is_sqlite:
        async with _sqlite_session_lock():
            async with _transaction_scope() as session:
                yield session
        return

    async with _transaction_scope() as session:
        yield session


async def init_db() -> None:
    engine = get_engine()
    async with engine.begin() as connection:
        await connection.run_sync(Base.metadata.create_all)


async def drop_db() -> None:
    engine = get_engine()
    async with engine.begin() as connection:
        await connection.run_sync(Base.metadata.drop_all)


async def dispose_db() -> None:
    global _engine, _engine_url, _session_factory
    if _engine is not None:
        await _engine.dispose()
    _engine = None
    _engine_url = None
    _session_factory = None
