"""
数据库引擎的构建与连接额度核算。

同步引擎与未池化的全局异步引擎都在此按需创建（首次访问时，不在 import 期）；
按事件循环池化的异步引擎由 session 模块创建。三者的构建参数在这里收口。
"""
import threading
from typing import Dict, Optional, cast

from sqlalchemy import NullPool, QueuePool, create_engine, event, text
from sqlalchemy.engine import Engine as SyncEngine
from sqlalchemy.ext.asyncio import AsyncEngine as SaAsyncEngine, create_async_engine
from sqlalchemy.pool import Pool

from app.runtime.config import settings
from app.db.diagnostics import _register_database_error_logging
from app.db.worker import DATABASE_WORKER_MAX_WORKERS
from app.runtime.log import logger
from app.runtime.observability import record_metric


def _database_backend_label() -> str:
    """把数据库类型收敛为有限的观测标签。"""
    return "postgresql" if settings.DB_TYPE.lower() == "postgresql" else "sqlite"


def _register_database_pool_metrics(engine: SyncEngine) -> None:
    """在 SQLAlchemy 池 checkout/checkin 边界维护当前借出连接数。"""
    if not isinstance(engine.pool, Pool):
        # 引擎构建单测允许注入不具备 PoolEvents 的轻量替身。
        return
    backend = _database_backend_label()

    def record_checkout(*_args: object) -> None:
        """连接借出后增加当前使用量。"""
        record_metric("db.pool.checked_out", 1, backend=backend)

    def record_checkin(*_args: object) -> None:
        """连接归还后减少当前使用量。"""
        record_metric("db.pool.checked_out", -1, backend=backend)

    event.listen(engine.pool, "checkout", record_checkout)
    event.listen(engine.pool, "checkin", record_checkin)


def _async_pool_kwargs(pooled: bool) -> dict:
    """
    异步引擎的连接池参数。

    池化时不指定 poolclass：SQLAlchemy 会自动选用异步适配的
    AsyncAdaptedQueuePool，显式传入同步的 QueuePool 反而会出错。
    :param pooled: 是否启用连接池
    :return: 传给 create_async_engine 的池参数
    """
    if not pooled:
        return {"poolclass": NullPool}
    return {
        "pool_size": settings.DB_ASYNC_POOL_SIZE,
        "max_overflow": settings.DB_ASYNC_MAX_OVERFLOW,
        "pool_timeout": settings.DB_POOL_TIMEOUT,
    }


def _get_database_engine(is_async: bool = False, pooled: bool = False):
    """
    获取数据库连接参数并设置WAL模式
    :param is_async: 是否创建异步引擎，True - 异步引擎, False - 同步引擎
    :param pooled: 异步引擎是否启用连接池，仅对常驻事件循环使用
    :return: 返回对应的数据库引擎
    """
    # 根据数据库类型选择连接方式
    if settings.DB_TYPE.lower() == "postgresql":
        return _get_postgresql_engine(is_async, pooled=pooled)
    else:
        return _get_sqlite_engine(is_async, pooled=pooled)


def _get_sqlite_engine(is_async: bool = False, pooled: bool = False):
    """
    获取SQLite数据库引擎
    """
    # 连接参数
    _connect_args = {
        "timeout": settings.DB_TIMEOUT,
    }
    # 允许部署侧注入驱动级参数（如 PgBouncer 事务模式下的 statement_cache_size）
    _connect_args.update(settings.DB_CONNECT_ARGS or {})
    # 启用 WAL 模式时的额外配置
    if settings.DB_WAL_ENABLE:
        _connect_args["check_same_thread"] = False

    # 创建同步引擎
    if not is_async:
        # 根据池类型设置 poolclass 和相关参数
        _pool_class = NullPool if settings.DB_POOL_TYPE == "NullPool" else QueuePool

        # 数据库参数
        _db_kwargs = {
            "url": settings.DB_SQLITE_URL(),
            "pool_pre_ping": settings.DB_POOL_PRE_PING,
            "echo": settings.DB_ECHO,
            "poolclass": _pool_class,
            "pool_recycle": settings.DB_POOL_RECYCLE,
            "connect_args": _connect_args
        }

        # 当使用 QueuePool 时，添加 QueuePool 特有的参数
        if _pool_class == QueuePool:
            _db_kwargs.update({
                "pool_size": settings.DB_SQLITE_POOL_SIZE,
                "pool_timeout": settings.DB_POOL_TIMEOUT,
                "max_overflow": settings.DB_SQLITE_MAX_OVERFLOW
            })

        # 创建数据库引擎
        engine = create_engine(**_db_kwargs)
        _register_database_error_logging(engine)
        _register_database_pool_metrics(engine)

        # 设置WAL模式。
        # 这是引擎构建里唯一的阻塞 I/O，且发生在 get_engine() 的创建锁内——异步侧因此
        # 移除了对称的那一段（见下方 else 分支）。同步侧保留是因为 journal_mode 必须有人
        # 设置一次，而同步引擎的首次创建由 lifespan 数据库准备组件中的 init_db() 完成，
        # 不存在一群线程
        # 等在锁上的场面；即便退化到运行期首次访问，阻塞的也只是本地 SQLite 的一次 PRAGMA。
        _journal_mode = "WAL" if settings.DB_WAL_ENABLE else "DELETE"
        with engine.connect() as connection:
            current_mode = connection.execute(text(f"PRAGMA journal_mode={_journal_mode};")).scalar()
            print(f"SQLite database journal mode set to: {current_mode}")

        return engine
    else:
        # 数据库参数，只能使用 NullPool
        _db_kwargs = {
            "url": settings.DB_SQLITE_URL("aiosqlite"),
            "pool_pre_ping": settings.DB_POOL_PRE_PING,
            "echo": settings.DB_ECHO,
            "pool_recycle": settings.DB_POOL_RECYCLE,
            "connect_args": _connect_args,
            **_async_pool_kwargs(pooled),
        }
        # 创建异步数据库引擎
        async_engine = create_async_engine(**_db_kwargs)
        _register_database_error_logging(async_engine.sync_engine)
        _register_database_pool_metrics(async_engine.sync_engine)

        # 异步侧不再设置 WAL。journal_mode 是数据库文件级的持久属性，同步引擎已经设置过，
        # 这里重复设置本就是冗余的；而它原本用 asyncio.run() 完成，是异步引擎构建里唯一的
        # 阻塞 I/O。引擎改为惰性创建之后，构建可能发生在任意线程——包括在运行中的事件循环
        # 内部（async_session_scope 首次取全局引擎时），那里调 asyncio.run() 会直接抛
        # RuntimeError；即便不抛，它也是在持有创建锁的状态下阻塞，会把所有等锁的线程拖死。
        return async_engine


def _get_postgresql_engine(is_async: bool = False, pooled: bool = False):
    """
    获取PostgreSQL数据库引擎
    """
    db_url = settings.DB_POSTGRESQL_URL()

    # PostgreSQL连接参数。允许部署侧注入驱动级参数，
    # 例如经 PgBouncer 事务模式接入时 asyncpg 需要 statement_cache_size=0
    _connect_args = dict(settings.DB_CONNECT_ARGS or {})

    # 创建同步引擎
    if not is_async:
        # 根据池类型设置 poolclass 和相关参数
        _pool_class = NullPool if settings.DB_POOL_TYPE == "NullPool" else QueuePool

        # 数据库参数
        _db_kwargs = {
            "url": db_url,
            "pool_pre_ping": settings.DB_POOL_PRE_PING,
            "echo": settings.DB_ECHO,
            "poolclass": _pool_class,
            "pool_recycle": settings.DB_POOL_RECYCLE,
            "connect_args": _connect_args
        }

        # 当使用 QueuePool 时，添加 QueuePool 特有的参数
        if _pool_class == QueuePool:
            _db_kwargs.update({
                "pool_size": settings.DB_POSTGRESQL_POOL_SIZE,
                "pool_timeout": settings.DB_POOL_TIMEOUT,
                "max_overflow": settings.DB_POSTGRESQL_MAX_OVERFLOW
            })

        # 创建数据库引擎
        engine = create_engine(**_db_kwargs)
        _register_database_error_logging(engine)
        _register_database_pool_metrics(engine)
        print(f"PostgreSQL database connected to {settings.DB_POSTGRESQL_TARGET}/{settings.DB_POSTGRESQL_DATABASE}")

        return engine
    else:
        async_db_url = settings.DB_POSTGRESQL_URL("asyncpg")

        # 数据库参数，只能使用 NullPool
        _db_kwargs = {
            "url": async_db_url,
            "pool_pre_ping": settings.DB_POOL_PRE_PING,
            "echo": settings.DB_ECHO,
            "pool_recycle": settings.DB_POOL_RECYCLE,
            "connect_args": _connect_args,
            **_async_pool_kwargs(pooled),
        }
        # 创建异步数据库引擎
        async_engine = create_async_engine(**_db_kwargs)
        _register_database_error_logging(async_engine.sync_engine)
        _register_database_pool_metrics(async_engine.sync_engine)
        print(f"Async PostgreSQL database connected to {settings.DB_POSTGRESQL_TARGET}/{settings.DB_POSTGRESQL_DATABASE}")

        return async_engine


# 引擎按需创建，不在 import 期建立连接。
#
# 此前这两个是模块级常量，`import app.db` 就会按 settings 连库、建出 user.db、SQLite 还要
# 去设一次 WAL——仅仅把这个包 import 进来（工具脚本、子进程探测、文档生成）就有了副作用。
#
# 注意惰性化并没有让「隔离 CONFIG_DIR 必须早于 import」这条约束消失：settings 是在
# import app.runtime.config 时构造的，那一刻 CONFIG_DIR 就定型了，晚建的引擎连的仍是真实库。
# 它消掉的是「import 本身即产生副作用」，以及由此带来的「测试想换库就必须重新起进程」。
#
# 惰性化引入的唯一新风险是首次访问的并发：这个项目有上百个调度线程，创建出多个引擎
# 意味着各自持一份连接池，实际连接数是额度核算的数倍。因此用双重检查加锁收口。
_sync_engine_lock = threading.RLock()
_async_engine_lock = threading.RLock()
_sync_engine: Optional[SyncEngine] = None
_async_engine: Optional[SaAsyncEngine] = None


def get_engine() -> SyncEngine:
    """
    获取同步数据库引擎，首次调用时创建。
    :return: 同步引擎
    """
    global _sync_engine
    if _sync_engine is None:
        with _sync_engine_lock:
            # 锁内复查：等锁期间可能已被其它线程创建
            if _sync_engine is None:
                _sync_engine = cast(SyncEngine, _get_database_engine(is_async=False))
    return _sync_engine


def get_global_async_engine() -> SaAsyncEngine:
    """
    获取未池化的全局异步引擎，供非常驻事件循环回退使用，首次调用时创建。
    :return: 异步引擎
    """
    global _async_engine
    if _async_engine is None:
        with _async_engine_lock:
            if _async_engine is None:
                _async_engine = cast(SaAsyncEngine, _get_database_engine(is_async=True))
    return _async_engine


def peek_sync_engine() -> Optional[SyncEngine]:
    """
    取已创建的同步引擎，未创建时返回 None——不触发创建。

    关停路径（close_database、测试引导的 atexit）需要「有就释放、没有就算了」：
    走 get_engine() 会为了 dispose 而先连一次库，在从未用过数据库的进程里尤其荒谬。
    取锁而不是裸读槽位：另一个线程可能正卡在创建里，裸读会看到 None、把那个引擎漏掉，
    取锁则会等它建完。注意这只是把竞态窗口**缩小**，并没有消除——在 peek 返回之后才
    开始创建的引擎照样漏。真要杜绝得让关停之后的创建直接失败，那是另一层面的改动。
    :return: 同步引擎或 None
    """
    with _sync_engine_lock:
        return _sync_engine


def peek_async_engine() -> Optional[SaAsyncEngine]:
    """
    取已创建的全局异步引擎，未创建时返回 None——不触发创建。
    :return: 异步引擎或 None
    """
    with _async_engine_lock:
        return _async_engine


def _async_pool_enabled() -> bool:
    """
    是否启用异步连接池。设为 NullPool 可回退到池化前的行为。
    """
    return str(settings.DB_ASYNC_POOL_TYPE or "").strip().lower() != "nullpool"


def connection_budget() -> Dict[str, int]:
    """
    核算数据库连接的理论峰值。

    各连接池此前是彼此独立配置的，没有任何地方核算总和——异步侧从无界收敛到有界
    之后，真正决定安全与否的就变成了「同步池 + 异步池 + 回退配额」这个总数是否
    还在数据库的额度之内。这里把它显式算出来，供启动校验与排障使用。
    连接池是进程级的：多 worker 部署时每个进程各持一份，因此合计要乘上 worker 数。
    只报单进程用量会让多 worker 在启动校验里一路绿灯，实际第一个 worker 还没起完
    就顶穿了 max_connections。
    :return: 单进程各项上限、worker 数与合计
    """
    if settings.DB_TYPE.lower() == "postgresql":
        sync_max = settings.DB_POSTGRESQL_POOL_SIZE + settings.DB_POSTGRESQL_MAX_OVERFLOW
    else:
        sync_max = settings.DB_SQLITE_POOL_SIZE + settings.DB_SQLITE_MAX_OVERFLOW
    if settings.DB_POOL_TYPE == "NullPool":
        # 未池化连接由通用线程池和专属数据库 worker 共同创建，二者都要计入上限估计。
        sync_max = settings.CONF.threadpool + DATABASE_WORKER_MAX_WORKERS
    async_max = (settings.DB_ASYNC_POOL_SIZE + settings.DB_ASYNC_MAX_OVERFLOW
                 if _async_pool_enabled() else 0)
    fallback = settings.DB_ASYNC_FALLBACK_LIMIT if _async_pool_enabled() else settings.CONF.scheduler
    per_worker = sync_max + async_max + fallback
    # worker 数非法时按 1 计：退化成 0 会让合计归零、反而误判「额度充足」
    workers = getattr(settings, "API_WORKERS", 1) or 1
    workers = workers if isinstance(workers, int) and workers > 0 else 1
    return {
        "sync": sync_max,
        "async_pooled": async_max,
        "async_fallback": fallback,
        "per_worker": per_worker,
        "workers": workers,
        "total": per_worker * workers,
    }


def check_connection_budget() -> bool:
    """
    对照数据库的真实连接上限校验理论峰值，超限时告警。

    只对 PostgreSQL 生效：SQLite 没有服务端连接上限，其压力体现为 WAL 写争用而非
    连接耗尽。用真实的 max_connections 而不是猜测值——部署方可能已经调过它。
    :return: 是否在额度之内
    """
    budget = connection_budget()
    if settings.DB_TYPE.lower() != "postgresql":
        logger.info(f"数据库连接理论峰值: {budget['total']} "
                    f"(单进程 {budget['per_worker']} = 同步 {budget['sync']} + 异步池 "
                    f"{budget['async_pooled']} + 回退 {budget['async_fallback']}"
                    f"，worker {budget['workers']})")
        return True
    try:
        with get_engine().connect() as conn:
            max_conn = int(conn.execute(text("SHOW max_connections")).scalar() or 0)
            reserved = int(
                conn.execute(text("SHOW superuser_reserved_connections")).scalar() or 0
            )
    except Exception as err:
        logger.warn(f"无法读取 PostgreSQL 连接上限，跳过额度校验: {err}")
        return True
    available = max_conn - reserved
    total = budget["total"]
    detail = (f"理论峰值 {total} = 单进程 {budget['per_worker']} (同步 {budget['sync']} "
              f"+ 异步池 {budget['async_pooled']} + 回退 {budget['async_fallback']}) "
              f"x worker {budget['workers']}，数据库可用 {available} "
              f"(max_connections {max_conn} - 保留 {reserved})")
    if total > available:
        logger.error(
            f"数据库连接额度不足：{detail}。"
            f"突发并发时可能出现 TooManyConnectionsError。"
            f"请调大 max_connections，或调小 API_WORKERS / DB_POSTGRESQL_MAX_OVERFLOW / "
            f"DB_ASYNC_MAX_OVERFLOW / DB_ASYNC_FALLBACK_LIMIT"
        )
        return False
    logger.info(f"数据库连接额度校验通过：{detail}")
    return True
