from collections.abc import AsyncGenerator

from fastapi import Depends
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase

import uuid

from app.core.config import settings
from app.core.security import AuthContext, PlatformAdminContext, get_auth_context, get_platform_admin_context

engine = create_async_engine(settings.database_url, pool_pre_ping=True)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False, autoflush=False)


class Base(DeclarativeBase):
    """
    Declarative base for models mapped onto tables already created by
    db/schema.sql. This app never calls Base.metadata.create_all() —
    the schema file is the single source of truth for DDL.
    """


async def limit_statement_time(session: AsyncSession) -> None:
    """One runaway query can't hold a connection and a CPU for long, however
    large the data: Postgres cancels it, and main.py says so as a 503. For
    this transaction only, like the session variables below."""
    if settings.request_statement_timeout_ms > 0:
        await session.execute(
            text("SELECT set_config('statement_timeout', :v, true)"), {"v": str(settings.request_statement_timeout_ms)}
        )


async def get_db(
    ctx: AuthContext = Depends(get_auth_context),
) -> AsyncGenerator[AsyncSession, None]:
    """
    Yields a session scoped to one request/transaction, with the Postgres
    Row-Level Security session variables set first (see db/schema.sql's
    tenant_isolation / vendor_isolation policies). SET LOCAL confines both
    to this transaction; the transaction commits on clean exit of the
    `session.begin()` block and rolls back if the request handler raises.
    """
    async with AsyncSessionLocal() as session:
        async with session.begin():
            await limit_statement_time(session)
            # Postgres's SET/SET LOCAL don't accept bound parameters ("SET LOCAL x = $1"
            # is a syntax error) — set_config() is a regular function and does, with its
            # third argument (is_local=true) giving the same transaction-local scoping.
            await session.execute(
                text("SELECT set_config('app.current_tenant_id', :v, true)"), {"v": str(ctx.tenant_id)}
            )
            if ctx.vendor_id is not None:
                await session.execute(
                    text("SELECT set_config('app.current_vendor_id', :v, true)"), {"v": str(ctx.vendor_id)}
                )
            # Outside its license dates a tenant can't be used, even by a
            # session that signed in before (app/crud/licensing.py).
            from app.crud import licensing  # imports the models, which import this module

            await licensing.ensure_session_allowed(session, ctx.tenant_id)
            # A session that was signed out ends here (app/crud/sessions.py).
            from app.crud import sessions

            await sessions.ensure_live(session, ctx)
            yield session


async def get_admin_scoped_db(
    tenant_id: uuid.UUID,
    _: PlatformAdminContext = Depends(get_platform_admin_context),
) -> AsyncGenerator[AsyncSession, None]:
    """Only ever wired behind get_platform_admin_context (see
    app/routers/platform_admin.py) — identical mechanism to get_db above,
    except the RLS session var comes from a Super Admin's chosen path
    param instead of the caller's own JWT. This is the entire cross-tenant
    mechanism: a verified admin "sudos" into one tenant's RLS context per
    request, reusing every existing tenant-scoped query/CRUD path
    unchanged. No RLS policy is bypassed or altered anywhere."""
    async with AsyncSessionLocal() as session:
        async with session.begin():
            await limit_statement_time(session)
            await session.execute(
                text("SELECT set_config('app.current_tenant_id', :v, true)"), {"v": str(tenant_id)}
            )
            yield session


async def get_unscoped_db(
    _: PlatformAdminContext = Depends(get_platform_admin_context),
) -> AsyncGenerator[AsyncSession, None]:
    """For the only two tables with no tenant_id/RLS policy at all —
    `tenants` and `platform_admins` (see db/schema.sql) — gated behind
    get_platform_admin_context so nothing but the Super Admin router can
    reach a session with no tenant scoping set."""
    async with AsyncSessionLocal() as session:
        async with session.begin():
            yield session
