"""Настройка асинхронного движка SQLAlchemy и сеанса."""

from collections.abc import AsyncGenerator
from typing import cast

from sqlalchemy import text
from sqlalchemy.ext.asyncio import (
    AsyncEngine,
    AsyncSession,
    async_sessionmaker,
    create_async_engine,
)
from sqlalchemy.pool import NullPool, QueuePool

from core.config import get_settings

settings = get_settings()

engine: AsyncEngine = create_async_engine(
    settings.database_url,
    pool_pre_ping=True,
    # Параметры пула — в настройках (`core/config.py`, там же расчёт бюджета
    # соединений на инстанс). Дефолт SQLAlchemy 5 + 10 под нагрузкой
    # выгребался за секунды.
    pool_size=settings.db_pool_size,
    max_overflow=settings.db_max_overflow,
    pool_timeout=settings.db_pool_timeout,
)

async_session_maker = async_sessionmaker(engine, expire_on_commit=False)


async def get_session() -> AsyncGenerator[AsyncSession, None]:
    """Зависимость FastAPI, возвращающая `AsyncSession`."""
    async with async_session_maker() as session:
        yield session


# --- Проверка доступности БД вне основного пула (сессия 33) ----------------
#
# Отдельный движок с `NullPool`: каждый вызов открывает новое соединение и
# закрывает его сразу после — бюджет соединений не пересекается с
# `engine.pool` (10 + 10 overflow × число воркеров uvicorn). Это единственный
# способ отличить «БД лежит» от «основной пул занят под нагрузкой»: проверка
# через `get_session()` в момент исчерпания пула сама встала бы в очередь на
# `db_pool_timeout` и не смогла бы ответить, пока не появится случайно
# освободившееся место — то есть не отличила бы два принципиально разных
# состояния. Короткий `timeout` на соединение (не путать с `db_pool_timeout`
# основного пула) — чтобы зависший, а не оборванный TCP (Postgres отвечает,
# но не может продвинуться) не держал проверку до дефолтных 60 секунд asyncpg.
_probe_engine: AsyncEngine = create_async_engine(
    settings.database_url,
    poolclass=NullPool,
    connect_args={"timeout": settings.db_probe_timeout_s},
)


async def check_db_up() -> bool:
    """`True`, если БД отвечает на `SELECT 1` по отдельному от основного пула соединению."""
    try:
        async with _probe_engine.connect() as connection:
            await connection.execute(text("SELECT 1"))
    except Exception:  # noqa: BLE001
        return False
    return True


# --- Сессия для метрик, читающих данные (не только «жив/мёртв»), вне основного
# пула (сессия 37, доделка session 32/33 — см. докстринг `api/metrics.py`) ----
#
# `check_db_up` выше обходится голым соединением ("SELECT 1"), но
# `vidconf_pipeline_sessions` нужна полноценная ORM-сессия (репозиторий,
# группировка по статусу) — `NullPool`, как и у `_probe_engine`: каждый вызов
# открывает новое соединение и сразу закрывает его, бюджет основного пула
# (`engine.pool`) не расходуется. `async_sessionmaker` — тот же паттерн, что
# `async_session_maker` выше, просто на другом движке.
_metrics_probe_engine: AsyncEngine = create_async_engine(
    settings.database_url,
    poolclass=NullPool,
    connect_args={"timeout": settings.db_probe_timeout_s},
)
_metrics_session_maker = async_sessionmaker(_metrics_probe_engine, expire_on_commit=False)


async def get_metrics_session() -> AsyncGenerator[AsyncSession, None]:
    """Зависимость FastAPI для метрик, которым нужна БД, но не основной пул.

    В отличие от `get_session()` (основной пул `engine.pool`, конкурирует за
    те же 10+10×воркеров соединений, что и API-запросы), сессия здесь открыта
    на `_metrics_probe_engine` — исчерпание основного пула эту метрику не
    заденет, как и `vidconf_db_up`/`vidconf_db_pool_*`. Тестовый харнесс
    (`tests/conftest.py`) подменяет и её на savepoint-сессию теста — так же,
    как `get_session` — иначе тест `vidconf_pipeline_sessions` не видел бы
    данные, ещё не закоммиченные за пределы savepoint.
    """
    async with _metrics_session_maker() as session:
        yield session


def db_pool_checked_out() -> int:
    """Число соединений основного пула, занятых прямо сейчас — без обращения к БД.

    SQLAlchemy держит счётчик в памяти самого объекта пула (`engine.pool`),
    поэтому его можно прочитать в любой момент, даже когда все соединения
    заняты или БД недоступна — именно это нужно алерту на исчерпание пула
    (метрика не должна зависеть от того, что измеряет). Размер и лимит
    overflow — конфигурация (`Settings.db_pool_size`/`db_max_overflow`),
    их не нужно снимать с объекта пула отдельно.
    """
    return cast(QueuePool, engine.pool).checkedout()
