"""iurban-tenancy — shared multi-tenancy data layer (ADR-006).

The canonical bits every iUrban microapp needs: a Base with a stable Alembic naming convention,
column mixins (UUID PK, timestamps, tenant_id), the RLS session helper (SET LOCAL inside a txn),
and a helper to emit the RLS DDL for migrations. Apps don't reinvent or copy tenancy.py.
"""

import uuid
from datetime import datetime

from sqlalchemy import DateTime, MetaData, Uuid, func, text
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

__all__ = [
    "NAMING_CONVENTION",
    "metadata",
    "Base",
    "UuidPkMixin",
    "TimestampMixin",
    "TenantMixin",
    "apply_tenant",
    "rls_ddl",
]

# Stable constraint/index names -> reproducible Alembic migrations.
NAMING_CONVENTION = {
    "ix": "ix_%(column_0_label)s",
    "uq": "uq_%(table_name)s_%(column_0_name)s",
    "ck": "ck_%(table_name)s_%(constraint_name)s",
    "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
    "pk": "pk_%(table_name)s",
}

metadata = MetaData(naming_convention=NAMING_CONVENTION)


class Base(DeclarativeBase):
    metadata = metadata


class UuidPkMixin:
    id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)


class TimestampMixin:
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
    )


class TenantMixin:
    """Every business table carries tenant_id (RLS filters on it)."""

    tenant_id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), nullable=False, index=True)


async def apply_tenant(session: AsyncSession, tenant_id: object) -> None:
    """Set the tenant for the current transaction so RLS policies filter every query.
    MUST be called inside a transaction (local=true) — safe with PgBouncer transaction-pooling.

    Uses set_config(..., true) rather than `SET LOCAL ... = :tid`: Postgres `SET` does NOT accept
    bind parameters (`SET LOCAL x = $1` → syntax error), whereas set_config() does."""
    await session.execute(
        text("SELECT set_config('app.current_tenant', :tid, true)"), {"tid": str(tenant_id)}
    )


def rls_ddl(table: str) -> str:
    """SQL to enable + force RLS and add the tenant-isolation policy. Use it in an Alembic migration."""
    return (
        f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY;\n"
        f"ALTER TABLE {table} FORCE ROW LEVEL SECURITY;\n"
        f"CREATE POLICY tenant_isolation ON {table}\n"
        f"  USING (tenant_id = current_setting('app.current_tenant')::uuid)\n"
        f"  WITH CHECK (tenant_id = current_setting('app.current_tenant')::uuid);"
    )
