"""Translate SQLAlchemy failures into safe, user-facing errors.

A raw SQLAlchemy exception stringifies to the statement that failed together
with its bound parameters::

    (psycopg2.errors.UndefinedColumn) column gov_users.emai does not exist
    [SQL: SELECT gov_users.uuid, gov_users.email, gov_users.password ...
     WHERE gov_users.emai = %(emai_1)s]
    [parameters: {'emai_1': 'john@example.com'}]

``global_exception_handler`` in ``abs-exception-core`` puts ``str(exc)`` straight
into the HTTP response body, so any driver error that escapes this package is
handed to the caller — schema, query shape and parameter values included.

Every database access in this package therefore runs inside :func:`db_guard`,
which logs the driver error with a full traceback and raises a generic HTTP
error in its place. Errors that were raised deliberately (``NotFoundError``,
``DuplicatedError``, ...) pass through untouched.
"""

from contextlib import contextmanager
from typing import Any, Iterator

from fastapi import HTTPException
from sqlalchemy.exc import (
    DataError,
    IntegrityError,
    InterfaceError,
    OperationalError,
    SQLAlchemyError,
    TimeoutError as SQLAlchemyTimeoutError,
)

from abs_exception_core.exceptions import (
    BadRequestError,
    ConflictError,
    InternalServerError,
    ServiceUnavailableError,
)
from abs_utils.logger import setup_logger

logger = setup_logger(__name__)

# Public messages. They describe the class of failure and nothing about the
# database behind it.
DB_UNAVAILABLE_MESSAGE = (
    "The service is temporarily unavailable. Please try again in a moment."
)
DB_CONFLICT_MESSAGE = "This request conflicts with data that already exists."
DB_INVALID_VALUE_MESSAGE = "One or more of the supplied values could not be processed."
DB_GENERIC_MESSAGE = "Something went wrong while processing the request."


def translate_db_error(exc: SQLAlchemyError, operation: str, **context: Any) -> HTTPException:
    """Log a database error in full and return the safe error to raise instead.

    Args:
        exc: The SQLAlchemy exception that was raised.
        operation: Short name of what was being done, e.g. "user lookup".
        **context: Extra identifiers for the log line. Pass column names and
            UUIDs, never user-supplied values such as an email address.

    Returns:
        HTTPException: The generic error to raise in place of `exc`.
    """
    suffix = ", ".join(f"{key}={value}" for key, value in context.items())
    logger.error(
        f"[DB] {operation} failed{f' ({suffix})' if suffix else ''}: "
        f"{exc.__class__.__name__}",
        exc_info=True,
    )

    # OperationalError/InterfaceError mean the database could not be reached or
    # dropped the connection — retryable, and not the caller's fault.
    if isinstance(exc, (OperationalError, InterfaceError, SQLAlchemyTimeoutError)):
        return ServiceUnavailableError(detail=DB_UNAVAILABLE_MESSAGE)
    if isinstance(exc, IntegrityError):
        return ConflictError(detail=DB_CONFLICT_MESSAGE)
    if isinstance(exc, DataError):
        return BadRequestError(detail=DB_INVALID_VALUE_MESSAGE)
    # ProgrammingError, InvalidRequestError and anything else is a bug on our
    # side: the caller gets nothing beyond "it broke".
    return InternalServerError(detail=DB_GENERIC_MESSAGE)


@contextmanager
def db_guard(operation: str, **context: Any) -> Iterator[None]:
    """Run a block of database work with driver errors translated and logged.

    Args:
        operation: Short name of what is being done, used in the log line.
        **context: Extra identifiers for the log line (no user-supplied values).

    Raises:
        HTTPException: A generic error whenever the block raises SQLAlchemyError.
            Deliberate HTTP errors raised inside the block are re-raised as-is.
    """
    try:
        yield
    except HTTPException:
        raise
    except SQLAlchemyError as exc:
        raise translate_db_error(exc, operation, **context) from exc
