"""Actor resolution: app session -> users row -> person party ->
engagements -> role + capability set (02-domain-model §4 pipeline).

The CURRENT tenant is request-scoped (ADR #015, superseding ADR #004
#13's "tenant-is-a-setting"): a user's allowed tenants are derived from
their engagement graph (PLUS every tenant for super admins), and the
`with_tenant` cookie selects one of them per request. `tenant_id` enters
the Actor HERE — every scope()/can() decision downstream reads it, so
switching tenants needs no server-side invalidation.

View-as (judgment #7) is an app-layer context swap performed HERE:
when the view-as cookie names a target and the REAL user's DB row says
super admin, the Actor's effective_* fields become the target's and
capabilities are derived from the target's role with super-admin power
stripped. Under view-as the tenant context follows the REAL user's
allowed set INTERSECTED with the target's (view-as never widens tenant
access — ADR #015). Both identities ride on the Actor (real_* /
effective_*) — the old app's single `is_super_admin` foot-gun does not
port.
"""

from __future__ import annotations

import uuid
from dataclasses import dataclass, field

from sqlalchemy import select
from sqlalchemy.orm import Session
from starlette.requests import Request

from app.config import settings
from app.models import Engagement, Party, Tenant, User
from app.services.authz.capabilities import CAPABILITIES, capability_allowed
from app.services.authz.view_as import VIEW_AS_COOKIE

#: The request-scoped current-tenant selector: an HttpOnly cookie holding
#: the tenant SLUG. Resolved per request against the actor's allowed set;
#: an absent/stale/forbidden value falls back to the first allowed tenant.
TENANT_COOKIE = "with_tenant"


@dataclass(frozen=True)
class TenantRef:
    """One tenant the actor may act within — the switcher's menu item and
    scope()'s target id."""

    id: uuid.UUID
    slug: str
    name: str


@dataclass(frozen=True)
class RoleInfo:
    """A derived tenant-level role plus the reason it was granted —
    Decision reason chains name the granting engagement."""

    role: str  # owner | manager | worker | none
    reason: str  # e.g. "official_partnership#ab12cd34(owner)"


@dataclass(frozen=True)
class Actor:
    """The resolved request identity. Everything downstream — can(),
    scope(), DTO shaping — reads THIS, never the session cookie."""

    tenant_id: uuid.UUID | None
    tenant_name: str | None
    tenant_slug: str | None

    # real (session) identity
    real_user_id: uuid.UUID | None
    real_username: str
    real_email: str
    real_is_super_admin: bool

    # effective identity (== real unless viewing-as)
    user_id: uuid.UUID | None
    username: str
    email: str
    is_super_admin: bool
    person_party_id: uuid.UUID | None
    role: str  # owner | manager | worker | none
    role_reason: str

    is_viewing_as: bool = False
    view_as_label: str | None = None

    capabilities: dict[str, bool] = field(default_factory=dict)

    #: every tenant this actor may switch to (ordered — the switcher menu).
    #: Under view-as this is the real∩target intersection (ADR #015).
    allowed_tenants: tuple[TenantRef, ...] = ()

    def allowed(self, capability: str) -> bool:
        return self.capabilities.get(capability, False)


def allowed_tenants(
    session: Session,
    person_party_id: uuid.UUID | None,
    is_super_admin: bool,
) -> tuple[TenantRef, ...]:
    """The tenants an identity may act within (ADR #015 membership rule):

    a super admin sees EVERY tenant; anyone else sees the tenants where
    their person party holds an ACTIVE role-bearing engagement — the SAME
    engagement types ``derive_role`` reads (owner via official_partnership,
    manager/worker via subcontract). ``derive_role`` stays the ONE
    authority on which engagements grant a role; this reuses it rather than
    re-encoding the type rule.

    Ordering: alphabetical by name; for a super admin the DEFAULT tenant
    (``settings.app_tenant_slug`` — now only a default-ordering hint, never
    a hard tenant pin) is pulled to the front so his books open first."""
    rows = session.execute(
        select(Tenant.id, Party.slug, Party.name).join(
            Party, Party.id == Tenant.party_id
        )
    ).all()
    all_refs = [TenantRef(id=r[0], slug=r[1], name=r[2]) for r in rows]

    if is_super_admin:
        kept = list(all_refs)
    elif person_party_id is None:
        kept = []
    else:
        # Candidate tenants = those where the person has ANY active
        # engagement; derive_role decides whether that engagement grants a
        # role (so the type rule lives in exactly one place).
        candidate_ids = set(
            session.scalars(
                select(Engagement.tenant_id).where(
                    Engagement.party_b_id == person_party_id,
                    Engagement.ended_on.is_(None),
                )
            ).all()
        )
        kept = [
            t
            for t in all_refs
            if t.id in candidate_ids
            and derive_role(session, t.id, person_party_id).role != "none"
        ]

    kept.sort(key=lambda t: (t.name.lower(), t.slug))
    if is_super_admin:
        # Stable sort: default tenant first, the rest keep alphabetical order.
        kept.sort(key=lambda t: t.slug != settings.app_tenant_slug)
    return tuple(kept)


def _pick_current_tenant(
    allowed: tuple[TenantRef, ...], cookie_slug: str
) -> TenantRef | None:
    """The current tenant for the request: the cookie's slug if it is in
    the allowed set, else the first allowed tenant, else None (the caller
    turns None into the NO_TENANT 403 on data routes)."""
    if not allowed:
        return None
    if cookie_slug:
        for tenant in allowed:
            if tenant.slug == cookie_slug:
                return tenant
    return allowed[0]


def derive_role(
    session: Session, tenant_id: uuid.UUID | None, person_party_id: uuid.UUID | None
) -> RoleInfo:
    """Tenant-level role from the engagement graph (02 §4 R1 rules):
    official_partnership => owner; subcontract + can_manage_imports =>
    manager; subcontract => worker; else none. Ended engagements grant
    nothing."""
    if tenant_id is None or person_party_id is None:
        return RoleInfo("none", "no person party / no tenant engagement")

    rows = session.execute(
        select(Engagement.id, Engagement.type, Engagement.can_manage_imports)
        .where(
            Engagement.tenant_id == tenant_id,
            Engagement.party_b_id == person_party_id,
            Engagement.ended_on.is_(None),
        )
        .order_by(Engagement.created_at)
    ).all()

    best = RoleInfo("none", "no active engagement with the tenant")
    rank = {"none": 0, "worker": 1, "manager": 2, "owner": 3}
    for eng_id, eng_type, cmi in rows:
        short = str(eng_id)[:8]
        if eng_type == "official_partnership":
            candidate = RoleInfo("owner", f"official_partnership#{short}(owner)")
        elif eng_type == "subcontract" and cmi:
            candidate = RoleInfo(
                "manager", f"subcontract#{short}(can_manage_imports)"
            )
        elif eng_type == "subcontract":
            candidate = RoleInfo("worker", f"subcontract#{short}(worker)")
        else:
            continue
        if rank[candidate.role] > rank[best.role]:
            best = candidate
    return best


def _capability_set(role: str, is_super_admin: bool, is_viewing_as: bool) -> dict[str, bool]:
    return {
        cap: capability_allowed(
            cap, role=role, is_super_admin=is_super_admin, is_viewing_as=is_viewing_as
        )
        for cap in CAPABILITIES
    }


def _load_user(session: Session, username: str) -> User | None:
    return session.scalar(select(User).where(User.idauth_username == username))


def _party_name(session: Session, party_id: uuid.UUID | None) -> str | None:
    if party_id is None:
        return None
    return session.scalar(select(Party.name).where(Party.id == party_id))


def resolve_actor(request: Request, session: Session) -> Actor:
    """Session identity (set by the auth middleware) -> Actor.

    An identity with no users row resolves to a zero-capability actor
    (auto-provisioning happens at login; a mid-session deletion or the
    dev stub land here). The view-as cookie is honored ONLY when the
    real user's DB row is super admin and the target exists — anything
    else is silently the real user (parity: forged cookies impersonate
    nobody)."""
    identity: dict = getattr(request.state, "identity", None) or {}
    username = identity.get("username", "")
    cookie_slug = request.cookies.get(TENANT_COOKIE, "")

    real = _load_user(session, username)
    real_is_sa = bool(real.is_super_admin) if real else False

    target: User | None = None
    if real is not None and real_is_sa:
        target_username = request.cookies.get(VIEW_AS_COOKIE, "")
        if target_username and target_username != real.idauth_username:
            target = _load_user(session, target_username)

    if target is not None:
        # View-as tenant context = real's allowed set ∩ target's allowed
        # set (ADR #015 — impersonation must never widen tenant access).
        real_allowed = allowed_tenants(session, real.person_party_id, True)
        target_allowed = allowed_tenants(
            session, target.person_party_id, bool(target.is_super_admin)
        )
        target_ids = {t.id for t in target_allowed}
        effective_allowed = tuple(t for t in real_allowed if t.id in target_ids)
        current = _pick_current_tenant(effective_allowed, cookie_slug)
        tenant_id = current.id if current else None
        role_info = derive_role(session, tenant_id, target.person_party_id)
        label = _party_name(session, target.person_party_id) or target.idauth_username
        return Actor(
            tenant_id=tenant_id,
            tenant_name=current.name if current else None,
            tenant_slug=current.slug if current else None,
            real_user_id=real.id,
            real_username=real.idauth_username,
            real_email=real.email,
            real_is_super_admin=True,
            user_id=target.id,
            username=target.idauth_username,
            email=target.email,
            is_super_admin=bool(target.is_super_admin),
            person_party_id=target.person_party_id,
            role=role_info.role,
            role_reason=role_info.reason,
            is_viewing_as=True,
            view_as_label=label,
            capabilities=_capability_set(
                role_info.role, bool(target.is_super_admin), is_viewing_as=True
            ),
            allowed_tenants=effective_allowed,
        )

    person_party_id = real.person_party_id if real else None
    effective_allowed = allowed_tenants(session, person_party_id, real_is_sa)
    current = _pick_current_tenant(effective_allowed, cookie_slug)
    tenant_id = current.id if current else None
    role_info = derive_role(session, tenant_id, person_party_id)
    return Actor(
        tenant_id=tenant_id,
        tenant_name=current.name if current else None,
        tenant_slug=current.slug if current else None,
        real_user_id=real.id if real else None,
        real_username=username,
        real_email=real.email if real else identity.get("email", ""),
        real_is_super_admin=real_is_sa,
        user_id=real.id if real else None,
        username=username,
        email=(real.email if real else identity.get("email", "")),
        is_super_admin=real_is_sa,
        person_party_id=person_party_id,
        role=role_info.role,
        role_reason=role_info.reason,
        is_viewing_as=False,
        view_as_label=None,
        capabilities=_capability_set(role_info.role, real_is_sa, is_viewing_as=False),
        allowed_tenants=effective_allowed,
    )


def resolve_actor_dep(request: Request) -> Actor:
    """FastAPI dependency — one Actor per request. Every /api router
    that touches data injects THIS (04: 'no inline permission logic in
    routers')."""
    from app import db

    with Session(db.get_engine()) as session:
        return resolve_actor(request, session)
