"""Async SQLAlchemy engine factory.

Wraps :func:`sqlalchemy.ext.asyncio.create_async_engine` with
production-leaning defaults:

  - ``pool_pre_ping=True`` — every checkout pings the connection.
    Costs one round-trip per checkout but catches stale connections
    after DB restart / firewall reset / etc.
  - ``pool_recycle=3600`` — recycle connections older than an hour.
    Avoids the MySQL "wait_timeout" surprise + Postgres connection
    leaks.
  - ``pool_size`` / ``max_overflow`` set to conservative defaults
    that work for SQLite + scale to Postgres / MySQL.

Consumers override any default via kwargs.
"""

from __future__ import annotations

from typing import Any

from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import ArgumentError
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine


def make_safe_url(url: str) -> str:
    """Return ``url`` with any embedded password masked, for safe logging.

    Use whenever a connection URL would land in a log record, exception
    message, error report, or anywhere else operators or downstream
    systems might persist it. Uses SQLAlchemy's :func:`make_url` to
    parse + re-render with ``hide_password=True``; a malformed URL
    returns the literal ``"<malformed-url>"`` rather than raising,
    so this helper is safe to call from error-handling paths where
    a parse failure would mask the real failure.

    SH-02. Pairs with the federation-wide pattern (mgf-alembic ships
    a sibling `_safe_url` helper for the same reason).
    """
    try:
        return make_url(url).render_as_string(hide_password=True)
    except ArgumentError:  # EH-07: typed catch — make_url raises ArgumentError on
        # any malformed/None URL; narrowed from `except Exception` so an unexpected
        # error surfaces instead of being silently masked.
        return "<malformed-url>"


# Production-leaning defaults. Override per-consumer as needed.
_DEFAULT_POOL_PRE_PING: bool = True
_DEFAULT_POOL_RECYCLE_SECONDS: int = 3600  # 1 hour
_DEFAULT_POOL_SIZE: int = 5
_DEFAULT_MAX_OVERFLOW: int = 10

# RA-05: URL prefixes for SQLite-shaped drivers — single-file DBs
# that use StaticPool / NullPool and reject ``pool_size`` /
# ``max_overflow``. Plain ``sqlite://`` (legacy/sync; SQLAlchemy
# default driver), ``sqlite+<driver>`` (e.g., ``sqlite+aiosqlite``,
# ``sqlite+pysqlite``), and the SQLCipher variants
# (``sqlcipher+pysqlcipher`` / ``sqlcipher+aiosqlcipher``) — same
# pool semantics, same kwarg rejection. Extend when a new SQLite-
# shaped driver enters the consumer set; a driver-name-introspection
# alternative is tracked in RELIABILITY_AND_ACCURACY.md RA-05 §5.
_SQLITE_SHAPED_URL_PREFIXES: tuple[str, ...] = (
    "sqlite://",
    "sqlite+",
    "sqlcipher://",
    "sqlcipher+",
)


def create_engine(
    database_url: str,
    *,
    echo: bool = False,
    pool_pre_ping: bool = _DEFAULT_POOL_PRE_PING,
    pool_recycle: int = _DEFAULT_POOL_RECYCLE_SECONDS,
    pool_size: int = _DEFAULT_POOL_SIZE,
    max_overflow: int = _DEFAULT_MAX_OVERFLOW,
    **extra: Any,
) -> AsyncEngine:
    """Build an async SQLAlchemy engine with mgf defaults.

    ``database_url`` follows the SQLAlchemy URL grammar — must use
    an async driver:

      - ``postgresql+asyncpg://user:pass@host/db``
      - ``mysql+aiomysql://...``
      - ``sqlite+aiosqlite://`` (in-memory) or
        ``sqlite+aiosqlite:///path/to/db.sqlite``

    ``echo=True`` logs every statement (debug only — never in prod).

    Pool settings are passed through to SQLAlchemy. SQLite ignores
    ``pool_size`` / ``max_overflow`` (it uses ``StaticPool`` /
    ``NullPool``) — the values are still accepted for shape
    consistency but have no effect.

    ``**extra`` is forwarded to :func:`create_async_engine` for
    consumers needing rare options (``connect_args``, ``isolation_level``,
    ``poolclass``, etc.). RA-02: keys in ``**extra`` MUST NOT
    collide with named parameters (``echo`` / ``pool_pre_ping`` /
    ``pool_recycle`` / ``pool_size`` / ``max_overflow``) — a
    collision raises ``ValueError`` at construction. Use the
    explicit parameter for documented kwargs; reserve ``**extra``
    for options not named in this function's signature.

    Returns the engine. The engine is **not yet connected** — the
    first connection attempt happens lazily on the first
    ``await engine.connect()`` or ``async_sessionmaker()`` call.

    Notes
    -----
    **Performance — ``pool_pre_ping=True`` is the default.** Every
    connection checkout sends ``SELECT 1`` to verify liveness; this
    costs **one DB roundtrip per checkout**. On a 1 ms-RTT DB at
    100 req/s with checkout-per-request, that's ~100 ms/sec of DB
    time spent on pings. The default is the right choice for
    production reliability — it catches stale connections after a
    DB restart, firewall reset, or pool-recycle event cleanly, and
    the retry happens transparently. Disable
    (``pool_pre_ping=False``) only for apps that are (a) known
    stable enough to tolerate occasional stale-connection errors
    AND (b) have documented mitigations (per-request retry, short
    connection timeout, app-level health checks). The PC-02
    tracker has the measurement template if you want to quantify
    the tradeoff for your stack. (PC-02.)
    """
    kwargs: dict[str, Any] = {
        "echo": echo,
        "pool_pre_ping": pool_pre_ping,
        "pool_recycle": pool_recycle,
    }

    # Pool sizing only applies to drivers that use a real pool.
    # SQLite (and SQLite-shaped drivers like SQLCipher) uses
    # StaticPool / NullPool; passing pool_size to those raises.
    # Detect the driver and skip pool kwargs accordingly.
    #
    # RA-05: the URL-prefix tuple covers the SQLite-shaped drivers
    # currently in scope: plain ``sqlite://``, the async/sync
    # variants ``sqlite+<driver>``, and the encrypted-SQLite drivers
    # ``sqlcipher://`` / ``sqlcipher+<driver>`` (same pool semantics
    # as SQLite — single-file DB, single-writer). If a future
    # consumer brings a SQLite-shaped driver under a new URL scheme,
    # add the prefix here; alternative shapes (driver-name
    # introspection via ``make_url(...).get_driver_name()``) are
    # tracked in the RA tracker for v1.1+ if the prefix list grows
    # unwieldy.
    is_sqlite = database_url.startswith(_SQLITE_SHAPED_URL_PREFIXES)
    if not is_sqlite:
        kwargs["pool_size"] = pool_size
        kwargs["max_overflow"] = max_overflow

    # RA-02: collision check on `**extra`. Without this, a key
    # that's already named in the explicit signature would
    # SILENTLY override the explicit kwarg — a consumer who passes
    # `pool_size=20, **{"pool_size": 5}` ends up with 5 and never
    # notices. Raise loudly so the misuse surfaces at construction.
    collisions = set(extra) & set(kwargs)
    if collisions:
        raise ValueError(
            f"create_engine: `**extra` collides with explicit "
            f"parameter(s) {sorted(collisions)!r}. Use the explicit "
            f"parameter for documented kwargs; `**extra` is for "
            f"options that aren't named in this function's signature."
        )

    kwargs.update(extra)
    return create_async_engine(database_url, **kwargs)


__all__ = ["create_engine", "make_safe_url"]
