"""
FastAPI portal API router — tenant CRUD, agent CRUD, and auth endpoints.

Mounted at /api/portal in the gateway or a dedicated portal-api service.
The Auth.js v5 Credentials provider calls /auth/verify to validate operator
email/password against the portal_users table.
"""

from __future__ import annotations

import re
import uuid
from datetime import datetime
from typing import Any

import bcrypt
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession

from shared.api.rbac import PortalCaller, get_portal_caller, require_platform_admin, require_tenant_admin, require_tenant_member
from shared.db import get_session
from shared.models.audit import AuditEvent
from shared.models.auth import PortalInvitation, PortalUser, UserTenantRole
from shared.models.tenant import Agent, Tenant
from shared.rls import current_tenant_id

portal_router = APIRouter(prefix="/api/portal", tags=["portal"])

# ---------------------------------------------------------------------------
# Pydantic schemas
# ---------------------------------------------------------------------------

_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,48}[a-z0-9]$|^[a-z0-9]{1,2}$")


class AuthVerifyRequest(BaseModel):
    email: str
    password: str


class AuthVerifyResponse(BaseModel):
    id: str
    email: str
    name: str
    role: str
    tenant_ids: list[str]
    active_tenant_id: str | None
    language: str = "en"


class AuthRegisterRequest(BaseModel):
    email: str
    password: str = Field(min_length=8)
    name: str = Field(min_length=1, max_length=255)


class AuthRegisterResponse(BaseModel):
    id: str
    email: str
    name: str
    role: str


class TenantCreate(BaseModel):
    name: str = Field(min_length=2, max_length=100)
    slug: str = Field(min_length=2, max_length=50)
    settings: dict[str, Any] = Field(default_factory=dict)

    @field_validator("slug")
    @classmethod
    def validate_slug(cls, v: str) -> str:
        v = v.strip()
        if not _SLUG_RE.match(v):
            raise ValueError(
                "slug must be 2-50 lowercase alphanumeric characters and hyphens "
                "(no leading/trailing hyphens)"
            )
        return v

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        v = v.strip()
        if len(v) < 2:
            raise ValueError("name must be at least 2 characters after stripping whitespace")
        return v


class TenantUpdate(BaseModel):
    name: str | None = Field(default=None, min_length=2, max_length=100)
    slug: str | None = Field(default=None, min_length=2, max_length=50)
    settings: dict[str, Any] | None = None

    @field_validator("slug")
    @classmethod
    def validate_slug(cls, v: str | None) -> str | None:
        if v is None:
            return v
        v = v.strip()
        if not _SLUG_RE.match(v):
            raise ValueError(
                "slug must be 2-50 lowercase alphanumeric characters and hyphens "
                "(no leading/trailing hyphens)"
            )
        return v


class TenantResponse(BaseModel):
    id: str
    name: str
    slug: str
    settings: dict[str, Any]
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}

    @classmethod
    def from_orm(cls, tenant: Tenant) -> "TenantResponse":
        return cls(
            id=str(tenant.id),
            name=tenant.name,
            slug=tenant.slug,
            settings=tenant.settings,
            created_at=tenant.created_at,
            updated_at=tenant.updated_at,
        )


class TenantsListResponse(BaseModel):
    items: list[TenantResponse]
    total: int
    page: int
    page_size: int


class AgentTestRequest(BaseModel):
    message: str = Field(min_length=1, max_length=4096)


class AgentTestResponse(BaseModel):
    agent_id: str
    message: str
    response: str


class TenantUserResponse(BaseModel):
    id: str
    name: str
    email: str
    role: str
    created_at: datetime

    model_config = {"from_attributes": True}


class TenantUsersResponse(BaseModel):
    users: list[TenantUserResponse]
    pending_invitations: list[dict[str, Any]]


class AdminUserResponse(BaseModel):
    id: str
    name: str
    email: str
    role: str
    tenant_memberships: list[dict[str, Any]]
    created_at: datetime

    model_config = {"from_attributes": True}


class ImpersonateRequest(BaseModel):
    tenant_id: uuid.UUID


class ImpersonateResponse(BaseModel):
    tenant_id: str
    tenant_name: str
    tenant_slug: str


class AgentCreate(BaseModel):
    name: str = Field(min_length=1, max_length=255)
    role: str = Field(min_length=1, max_length=255)
    persona: str = Field(default="")
    system_prompt: str = Field(default="")
    model_preference: str = Field(default="quality")
    tool_assignments: list[Any] = Field(default_factory=list)
    escalation_rules: list[Any] = Field(default_factory=list)
    is_active: bool = Field(default=True)

    @field_validator("model_preference")
    @classmethod
    def validate_model_preference(cls, v: str) -> str:
        allowed = {"quality", "fast", "balanced", "economy", "local"}
        if v not in allowed:
            raise ValueError(f"model_preference must be one of: {', '.join(sorted(allowed))}")
        return v


class AgentUpdate(BaseModel):
    name: str | None = Field(default=None, min_length=1, max_length=255)
    role: str | None = Field(default=None, min_length=1, max_length=255)
    persona: str | None = None
    system_prompt: str | None = None
    model_preference: str | None = None
    tool_assignments: list[Any] | None = None
    escalation_rules: list[Any] | None = None
    is_active: bool | None = None

    @field_validator("model_preference")
    @classmethod
    def validate_model_preference(cls, v: str | None) -> str | None:
        if v is None:
            return v
        allowed = {"quality", "fast", "balanced", "economy", "local"}
        if v not in allowed:
            raise ValueError(f"model_preference must be one of: {', '.join(sorted(allowed))}")
        return v


class AgentResponse(BaseModel):
    id: str
    tenant_id: str
    name: str
    role: str
    persona: str
    system_prompt: str
    model_preference: str
    tool_assignments: list[Any]
    escalation_rules: list[Any]
    is_active: bool
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}

    @classmethod
    def from_orm(cls, agent: Agent) -> "AgentResponse":
        return cls(
            id=str(agent.id),
            tenant_id=str(agent.tenant_id),
            name=agent.name,
            role=agent.role,
            persona=agent.persona,
            system_prompt=agent.system_prompt,
            model_preference=agent.model_preference,
            tool_assignments=agent.tool_assignments,
            escalation_rules=agent.escalation_rules,
            is_active=agent.is_active,
            created_at=agent.created_at,
            updated_at=agent.updated_at,
        )


# ---------------------------------------------------------------------------
# Auth endpoints
# ---------------------------------------------------------------------------


@portal_router.post("/auth/verify", response_model=AuthVerifyResponse)
async def verify_credentials(
    body: AuthVerifyRequest,
    session: AsyncSession = Depends(get_session),
) -> AuthVerifyResponse:
    """
    Validate email + password against the portal_users table.

    Used by Auth.js v5 Credentials provider. Returns 401 on invalid credentials.
    Response deliberately omits hashed_password.

    Returns role + tenant_ids + active_tenant_id instead of is_admin:
      - platform_admin: all tenant IDs from the tenants table
      - customer_admin / customer_operator: only tenant IDs from user_tenant_roles
    """
    result = await session.execute(select(PortalUser).where(PortalUser.email == body.email))
    user = result.scalar_one_or_none()

    if user is None or not bcrypt.checkpw(body.password.encode(), user.hashed_password.encode()):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")

    # Resolve tenant_ids based on role
    if user.role == "platform_admin":
        # Platform admins see all tenants
        tenants_result = await session.execute(select(Tenant))
        tenant_ids = [str(t.id) for t in tenants_result.scalars().all()]
    else:
        # Customer admins and operators see only their assigned tenants
        memberships_result = await session.execute(
            select(UserTenantRole).where(UserTenantRole.user_id == user.id)
        )
        tenant_ids = [str(m.tenant_id) for m in memberships_result.scalars().all()]

    active_tenant_id = tenant_ids[0] if tenant_ids else None

    return AuthVerifyResponse(
        id=str(user.id),
        email=user.email,
        name=user.name,
        role=user.role,
        tenant_ids=tenant_ids,
        active_tenant_id=active_tenant_id,
        language=user.language,
    )


@portal_router.post("/auth/register", response_model=AuthRegisterResponse, status_code=status.HTTP_201_CREATED)
async def register_user(
    body: AuthRegisterRequest,
    # DEPRECATED: Direct registration is platform-admin only.
    # Standard flow: use POST /api/portal/invitations (invite-only onboarding).
    caller: PortalCaller = Depends(require_platform_admin),
    session: AsyncSession = Depends(get_session),
) -> AuthRegisterResponse:
    """
    Create a new portal user with bcrypt-hashed password.

    DEPRECATED: This endpoint is now restricted to platform admins only.
    The standard onboarding flow is invite-only: POST /api/portal/invitations.

    Returns 409 if email already registered.
    """
    existing = await session.execute(select(PortalUser).where(PortalUser.email == body.email))
    if existing.scalar_one_or_none() is not None:
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Email already registered")

    hashed = bcrypt.hashpw(body.password.encode(), bcrypt.gensalt()).decode()
    user = PortalUser(
        email=body.email,
        hashed_password=hashed,
        name=body.name,
        role="customer_admin",
    )
    session.add(user)
    await session.commit()
    await session.refresh(user)

    return AuthRegisterResponse(
        id=str(user.id),
        email=user.email,
        name=user.name,
        role=user.role,
    )


# ---------------------------------------------------------------------------
# Tenant endpoints (PRTA-01)
# ---------------------------------------------------------------------------


@portal_router.get("/tenants", response_model=TenantsListResponse)
async def list_tenants(
    page: int = Query(default=1, ge=1),
    page_size: int = Query(default=20, ge=1, le=100),
    caller: PortalCaller = Depends(require_platform_admin),
    session: AsyncSession = Depends(get_session),
) -> TenantsListResponse:
    """List all tenants (platform admin view). Paginated."""
    offset = (page - 1) * page_size
    result = await session.execute(select(Tenant).order_by(Tenant.created_at.desc()).offset(offset).limit(page_size))
    tenants = result.scalars().all()

    count_result = await session.execute(select(Tenant))
    total = len(count_result.scalars().all())

    return TenantsListResponse(
        items=[TenantResponse.from_orm(t) for t in tenants],
        total=total,
        page=page,
        page_size=page_size,
    )


@portal_router.post("/tenants", response_model=TenantResponse, status_code=status.HTTP_201_CREATED)
async def create_tenant(
    body: TenantCreate,
    caller: PortalCaller = Depends(require_platform_admin),
    session: AsyncSession = Depends(get_session),
) -> TenantResponse:
    """Create a new tenant. Validates slug uniqueness."""
    # Check name uniqueness
    name_exists = await session.execute(select(Tenant).where(Tenant.name == body.name))
    if name_exists.scalar_one_or_none() is not None:
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Tenant name already exists")

    # Check slug uniqueness
    slug_exists = await session.execute(select(Tenant).where(Tenant.slug == body.slug))
    if slug_exists.scalar_one_or_none() is not None:
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Tenant slug already exists")

    tenant = Tenant(name=body.name, slug=body.slug, settings=body.settings)
    session.add(tenant)
    await session.commit()
    await session.refresh(tenant)
    return TenantResponse.from_orm(tenant)


@portal_router.get("/tenants/{tenant_id}", response_model=TenantResponse)
async def get_tenant(
    tenant_id: uuid.UUID,
    caller: PortalCaller = Depends(require_tenant_member),
    session: AsyncSession = Depends(get_session),
) -> TenantResponse:
    """Get a tenant by ID. Returns 404 if not found."""
    result = await session.execute(select(Tenant).where(Tenant.id == tenant_id))
    tenant = result.scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")
    return TenantResponse.from_orm(tenant)


@portal_router.put("/tenants/{tenant_id}", response_model=TenantResponse)
async def update_tenant(
    tenant_id: uuid.UUID,
    body: TenantUpdate,
    caller: PortalCaller = Depends(require_platform_admin),
    session: AsyncSession = Depends(get_session),
) -> TenantResponse:
    """Update a tenant (partial updates supported)."""
    result = await session.execute(select(Tenant).where(Tenant.id == tenant_id))
    tenant = result.scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")

    if body.name is not None:
        # Check name uniqueness (exclude self)
        name_exists = await session.execute(
            select(Tenant).where(Tenant.name == body.name, Tenant.id != tenant_id)
        )
        if name_exists.scalar_one_or_none() is not None:
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Tenant name already exists")
        tenant.name = body.name

    if body.slug is not None:
        # Check slug uniqueness (exclude self)
        slug_exists = await session.execute(
            select(Tenant).where(Tenant.slug == body.slug, Tenant.id != tenant_id)
        )
        if slug_exists.scalar_one_or_none() is not None:
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Tenant slug already exists")
        tenant.slug = body.slug

    if body.settings is not None:
        tenant.settings = body.settings

    await session.commit()
    await session.refresh(tenant)
    return TenantResponse.from_orm(tenant)


@portal_router.delete("/tenants/{tenant_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_tenant(
    tenant_id: uuid.UUID,
    caller: PortalCaller = Depends(require_platform_admin),
    session: AsyncSession = Depends(get_session),
) -> None:
    """Delete a tenant. Cascade deletes agents and channel_connections."""
    result = await session.execute(select(Tenant).where(Tenant.id == tenant_id))
    tenant = result.scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")
    await session.delete(tenant)
    await session.commit()


# ---------------------------------------------------------------------------
# Agent endpoints (PRTA-02)
# ---------------------------------------------------------------------------


async def _get_tenant_or_404(tenant_id: uuid.UUID, session: AsyncSession) -> Tenant:
    """Shared helper to fetch a tenant or raise 404."""
    result = await session.execute(select(Tenant).where(Tenant.id == tenant_id))
    tenant = result.scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")
    return tenant


@portal_router.get("/tenants/{tenant_id}/agents", response_model=list[AgentResponse])
async def list_agents(
    tenant_id: uuid.UUID,
    caller: PortalCaller = Depends(require_tenant_member),
    session: AsyncSession = Depends(get_session),
) -> list[AgentResponse]:
    """List all agents for a tenant."""
    await _get_tenant_or_404(tenant_id, session)
    token = current_tenant_id.set(tenant_id)
    try:
        result = await session.execute(
            select(Agent).where(Agent.tenant_id == tenant_id).order_by(Agent.created_at.desc())
        )
        agents = result.scalars().all()
    finally:
        current_tenant_id.reset(token)
    return [AgentResponse.from_orm(a) for a in agents]


@portal_router.post(
    "/tenants/{tenant_id}/agents",
    response_model=AgentResponse,
    status_code=status.HTTP_201_CREATED,
)
async def create_agent(
    tenant_id: uuid.UUID,
    body: AgentCreate,
    caller: PortalCaller = Depends(require_tenant_admin),
    session: AsyncSession = Depends(get_session),
) -> AgentResponse:
    """Create an AI employee for a tenant."""
    await _get_tenant_or_404(tenant_id, session)
    token = current_tenant_id.set(tenant_id)
    try:
        agent = Agent(
            tenant_id=tenant_id,
            name=body.name,
            role=body.role,
            persona=body.persona,
            system_prompt=body.system_prompt,
            model_preference=body.model_preference,
            tool_assignments=body.tool_assignments,
            escalation_rules=body.escalation_rules,
            is_active=body.is_active,
        )
        session.add(agent)
        await session.commit()
        await session.refresh(agent)
    finally:
        current_tenant_id.reset(token)
    return AgentResponse.from_orm(agent)


@portal_router.get("/tenants/{tenant_id}/agents/{agent_id}", response_model=AgentResponse)
async def get_agent(
    tenant_id: uuid.UUID,
    agent_id: uuid.UUID,
    caller: PortalCaller = Depends(require_tenant_member),
    session: AsyncSession = Depends(get_session),
) -> AgentResponse:
    """Get an agent by ID (must belong to the specified tenant)."""
    await _get_tenant_or_404(tenant_id, session)
    token = current_tenant_id.set(tenant_id)
    try:
        result = await session.execute(
            select(Agent).where(Agent.id == agent_id, Agent.tenant_id == tenant_id)
        )
        agent = result.scalar_one_or_none()
    finally:
        current_tenant_id.reset(token)
    if agent is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found")
    return AgentResponse.from_orm(agent)


@portal_router.put("/tenants/{tenant_id}/agents/{agent_id}", response_model=AgentResponse)
async def update_agent(
    tenant_id: uuid.UUID,
    agent_id: uuid.UUID,
    body: AgentUpdate,
    caller: PortalCaller = Depends(require_tenant_admin),
    session: AsyncSession = Depends(get_session),
) -> AgentResponse:
    """Update an agent (partial updates supported)."""
    await _get_tenant_or_404(tenant_id, session)
    token = current_tenant_id.set(tenant_id)
    try:
        result = await session.execute(
            select(Agent).where(Agent.id == agent_id, Agent.tenant_id == tenant_id)
        )
        agent = result.scalar_one_or_none()
        if agent is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found")

        update_fields = body.model_dump(exclude_none=True)
        for field, value in update_fields.items():
            setattr(agent, field, value)

        await session.commit()
        await session.refresh(agent)
    finally:
        current_tenant_id.reset(token)
    return AgentResponse.from_orm(agent)


@portal_router.delete("/tenants/{tenant_id}/agents/{agent_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_agent(
    tenant_id: uuid.UUID,
    agent_id: uuid.UUID,
    caller: PortalCaller = Depends(require_tenant_admin),
    session: AsyncSession = Depends(get_session),
) -> None:
    """Delete an agent."""
    await _get_tenant_or_404(tenant_id, session)
    token = current_tenant_id.set(tenant_id)
    try:
        result = await session.execute(
            select(Agent).where(Agent.id == agent_id, Agent.tenant_id == tenant_id)
        )
        agent = result.scalar_one_or_none()
        if agent is None:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found")
        await session.delete(agent)
        await session.commit()
    finally:
        current_tenant_id.reset(token)


# ---------------------------------------------------------------------------
# Test-message endpoint — operators can send test messages to agents
# ---------------------------------------------------------------------------


@portal_router.post(
    "/tenants/{tenant_id}/agents/{agent_id}/test",
    response_model=AgentTestResponse,
)
async def test_agent_message(
    tenant_id: uuid.UUID,
    agent_id: uuid.UUID,
    body: AgentTestRequest,
    caller: PortalCaller = Depends(require_tenant_member),
    session: AsyncSession = Depends(get_session),
) -> AgentTestResponse:
    """
    Send a test message to an agent.

    Available to all tenant members including customer_operator (per locked decision:
    operators can send test messages to agents). Returns a lightweight echo response
    with the agent's identity. Full orchestrator integration is added when the agent
    pipeline is wired to the portal API.
    """
    await _get_tenant_or_404(tenant_id, session)
    token = current_tenant_id.set(tenant_id)
    try:
        result = await session.execute(
            select(Agent).where(Agent.id == agent_id, Agent.tenant_id == tenant_id)
        )
        agent = result.scalar_one_or_none()
    finally:
        current_tenant_id.reset(token)
    if agent is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found")

    # Lightweight test handler — dispatches to orchestrator when integration is complete.
    # Returns a stub response with agent identity until orchestrator is wired to portal API.
    response_text = (
        f"Hi! I'm {agent.name}, your {agent.role}. "
        f"I received your test message: '{body.message[:100]}'. "
        "I'm ready to assist your team!"
    )
    return AgentTestResponse(
        agent_id=str(agent.id),
        message=body.message,
        response=response_text,
    )


# ---------------------------------------------------------------------------
# User listing endpoints
# ---------------------------------------------------------------------------


@portal_router.get("/tenants/{tenant_id}/users", response_model=TenantUsersResponse)
async def list_tenant_users(
    tenant_id: uuid.UUID,
    caller: PortalCaller = Depends(require_tenant_admin),
    session: AsyncSession = Depends(get_session),
) -> TenantUsersResponse:
    """
    List all users and pending invitations for a tenant.

    Returns active users (from user_tenant_roles JOIN portal_users) and
    pending invitations for the given tenant. Requires tenant admin or platform admin.
    """
    await _get_tenant_or_404(tenant_id, session)

    # Load users with membership in this tenant
    result = await session.execute(
        select(PortalUser, UserTenantRole)
        .join(UserTenantRole, PortalUser.id == UserTenantRole.user_id)
        .where(UserTenantRole.tenant_id == tenant_id)
        .order_by(UserTenantRole.created_at.desc())
    )
    rows = result.all()
    users = [
        TenantUserResponse(
            id=str(user.id),
            name=user.name,
            email=user.email,
            role=membership.role,
            created_at=user.created_at,
        )
        for user, membership in rows
    ]

    # Load pending invitations
    inv_result = await session.execute(
        select(PortalInvitation).where(
            PortalInvitation.tenant_id == tenant_id,
            PortalInvitation.status == "pending",
        )
    )
    invitations = inv_result.scalars().all()
    pending = [
        {
            "id": str(inv.id),
            "email": inv.email,
            "name": inv.name,
            "role": inv.role,
            "expires_at": inv.expires_at.isoformat(),
            "created_at": inv.created_at.isoformat(),
        }
        for inv in invitations
    ]

    return TenantUsersResponse(users=users, pending_invitations=pending)


@portal_router.get("/admin/users", response_model=list[AdminUserResponse])
async def list_all_users(
    tenant_id: uuid.UUID | None = Query(default=None),
    role: str | None = Query(default=None),
    caller: PortalCaller = Depends(require_platform_admin),
    session: AsyncSession = Depends(get_session),
) -> list[AdminUserResponse]:
    """
    Global user management endpoint for platform admins.

    Lists ALL portal users with their tenant memberships. Supports optional
    filtering by tenant_id or role. Requires platform admin access.
    """
    query = select(PortalUser).order_by(PortalUser.created_at.desc())
    if role is not None:
        query = query.where(PortalUser.role == role)

    result = await session.execute(query)
    users = result.scalars().all()

    # If filtering by tenant_id, load matching user IDs first
    if tenant_id is not None:
        membership_result = await session.execute(
            select(UserTenantRole).where(UserTenantRole.tenant_id == tenant_id)
        )
        tenant_user_ids = {m.user_id for m in membership_result.scalars().all()}
        users = [u for u in users if u.id in tenant_user_ids]

    # Build response with membership info for each user
    response = []
    for user in users:
        mem_result = await session.execute(
            select(UserTenantRole).where(UserTenantRole.user_id == user.id)
        )
        memberships = [
            {"tenant_id": str(m.tenant_id), "role": m.role}
            for m in mem_result.scalars().all()
        ]
        response.append(
            AdminUserResponse(
                id=str(user.id),
                name=user.name,
                email=user.email,
                role=user.role,
                tenant_memberships=memberships,
                created_at=user.created_at,
            )
        )
    return response


# ---------------------------------------------------------------------------
# Impersonation endpoints (platform admin only)
# ---------------------------------------------------------------------------


@portal_router.post("/admin/impersonate", response_model=ImpersonateResponse)
async def start_impersonation(
    body: ImpersonateRequest,
    caller: PortalCaller = Depends(require_platform_admin),
    session: AsyncSession = Depends(get_session),
) -> ImpersonateResponse:
    """
    Begin platform admin impersonation of a tenant.

    Logs an AuditEvent with action_type='impersonation' containing the platform
    admin's user_id and the target tenant_id. The portal uses this response
    to trigger a JWT update with impersonating_tenant_id in the session.
    """
    result = await session.execute(select(Tenant).where(Tenant.id == body.tenant_id))
    tenant = result.scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")

    # Log impersonation start to audit trail using raw INSERT (per audit.py design)
    await session.execute(
        text(
            "INSERT INTO audit_events (tenant_id, user_id, action_type, metadata) "
            "VALUES (:tenant_id, :user_id, 'impersonation', CAST(:metadata AS jsonb))"
        ),
        {
            "tenant_id": str(body.tenant_id),
            "user_id": str(caller.user_id),
            "metadata": (
                f'{{"action": "start", "platform_admin_id": "{caller.user_id}", '
                f'"target_tenant_id": "{body.tenant_id}"}}'
            ),
        },
    )
    await session.commit()

    return ImpersonateResponse(
        tenant_id=str(tenant.id),
        tenant_name=tenant.name,
        tenant_slug=tenant.slug,
    )


@portal_router.post("/admin/stop-impersonation", status_code=status.HTTP_204_NO_CONTENT)
async def stop_impersonation(
    body: ImpersonateRequest,
    caller: PortalCaller = Depends(require_platform_admin),
    session: AsyncSession = Depends(get_session),
) -> None:
    """
    End platform admin impersonation session.

    Logs an AuditEvent with action_type='impersonation' and action='stop'
    to complete the impersonation audit trail.
    """
    await session.execute(
        text(
            "INSERT INTO audit_events (tenant_id, user_id, action_type, metadata) "
            "VALUES (:tenant_id, :user_id, 'impersonation', CAST(:metadata AS jsonb))"
        ),
        {
            "tenant_id": str(body.tenant_id),
            "user_id": str(caller.user_id),
            "metadata": (
                f'{{"action": "stop", "platform_admin_id": "{caller.user_id}", '
                f'"target_tenant_id": "{body.tenant_id}"}}'
            ),
        },
    )
    await session.commit()


# ---------------------------------------------------------------------------
# Language preference endpoint (Phase 7 multilanguage)
# ---------------------------------------------------------------------------

_SUPPORTED_LANGUAGES = {"en", "es", "pt"}


class LanguagePreferenceRequest(BaseModel):
    language: str


class LanguagePreferenceResponse(BaseModel):
    language: str


@portal_router.patch("/users/me/language", response_model=LanguagePreferenceResponse)
async def update_language_preference(
    body: LanguagePreferenceRequest,
    caller: PortalCaller = Depends(get_portal_caller),
    session: AsyncSession = Depends(get_session),
) -> LanguagePreferenceResponse:
    """
    Update the authenticated user's language preference.

    Accepts: {"language": "es"} (must be one of: en, es, pt)
    Returns: {"language": "es"} on success.
    Returns 400 if language is not in the supported set.
    Returns 401 if not authenticated (no X-Portal-User-Id header).
    """
    if body.language not in _SUPPORTED_LANGUAGES:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Unsupported language '{body.language}'. Supported: en, es, pt",
        )

    result = await session.execute(
        select(PortalUser).where(PortalUser.id == caller.user_id)
    )
    user = result.scalar_one_or_none()
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="User not found",
        )

    user.language = body.language
    await session.commit()

    return LanguagePreferenceResponse(language=body.language)
