"""Record-level access — which records of a Master or Form a person may see,
edit and delete (RecordAccessPolicy; the rule shape is documented on
app/schemas/access.py::RecordAccessRule).

A policy becomes one SQL condition (record_sql), added to every read of the
source — record lists and counts, dataset queries behind pages, dashboards,
exports and the Data Explorer — and checked for the single record an edit or
delete targets. Security can't depend on a translation that is only a
superset, so a rule whose filters SQL can't express exactly grants nothing.

Not applied to: system administrators (never restricted); tasks and
workflows, which run as the system on the author's behalf; and the labels of
linked records shown in another source's reference fields.
"""

import uuid
from typing import Literal

from fastapi import HTTPException
from sqlalchemy import and_, false, or_, select, true
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.permissions import resolve_action_matrix
from app.crud import record_sql
from app.models.access import RecordAccessPolicy
from app.models.identity import Role, UserRole
from app.schemas.access import RecordAccessRule

Purpose = Literal["view", "edit", "delete"]


async def load_policy(db: AsyncSession, tenant_id: uuid.UUID, source_type: str, source_id: uuid.UUID) -> RecordAccessPolicy | None:
    return (
        await db.execute(
            select(RecordAccessPolicy).where(
                RecordAccessPolicy.tenant_id == tenant_id,
                RecordAccessPolicy.source_type == source_type,
                RecordAccessPolicy.source_id == source_id,
            )
        )
    ).scalar_one_or_none()


async def user_roles(db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID | None) -> tuple[set[str], bool]:
    """(role id strings the user holds, is system administrator)."""
    if user_id is None:
        return set(), False
    rows = await db.execute(
        select(Role.id, Role.is_system_role).join(UserRole, UserRole.role_id == Role.id).where(UserRole.user_id == user_id, Role.tenant_id == tenant_id)
    )
    roles, admin = set(), False
    for rid, is_system in rows.all():
        roles.add(str(rid))
        admin = admin or bool(is_system)
    return roles, admin


async def access_clause(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    user_id: uuid.UUID | None,
    src: record_sql.RecordSource,
    purpose: Purpose = "view",
) -> object | None:
    """The SQL condition limiting `src`'s rows to what this person may
    `purpose` — or None when nothing limits them."""
    policy = await load_policy(db, tenant_id, src.kind, src.source_id)
    if policy is None or not policy.enabled:
        return None
    _, is_admin = await resolve_action_matrix(db, tenant_id, user_id)
    if is_admin:
        return None
    roles, _ = await user_roles(db, tenant_id, user_id)

    from app.crud.dataset_query import prepare_filters

    parts = []
    for raw in policy.rules:
        rule = RecordAccessRule.model_validate(raw)
        if rule.role_ids and not {str(r) for r in rule.role_ids} & roles:
            continue
        if purpose == "edit" and not rule.can_edit:
            continue
        if purpose == "delete" and not rule.can_delete:
            continue
        if rule.scope == "all":
            parts.append(true())
        elif rule.scope == "own":
            parts.append(src.column("created_by") == user_id if user_id else false())
        else:
            filters = await prepare_filters(db, tenant_id, user_id, [f.model_dump() for f in rule.filters], src.field_defs)
            clauses, exact = record_sql.filter_clauses(src, filters, tenant_id)
            parts.append(and_(*clauses) if exact and clauses else false())
    return or_(*parts) if parts else false()


async def require_record_access(
    db: AsyncSession,
    tenant_id: uuid.UUID,
    user_id: uuid.UUID | None,
    src: record_sql.RecordSource,
    record_id: uuid.UUID,
    purpose: Purpose,
) -> None:
    """404 when the record isn't visible to this person, 403 when it is but
    they may not `purpose` it."""
    view = await access_clause(db, tenant_id, user_id, src, "view")
    if view is None:
        return
    model = src.model

    async def passes(clause: object) -> bool:
        stmt = select(model.id).where(model.tenant_id == tenant_id, model.id == record_id, clause)
        return (await db.execute(stmt)).scalar_one_or_none() is not None

    if not await passes(view):
        raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
    if purpose != "view" and not await passes(await access_clause(db, tenant_id, user_id, src, purpose)):
        raise HTTPException(status_code=403, detail=f"You don't have permission to {purpose} this record.")
