"""can(actor, action, resource) -> Decision — the row-level twin of
scope() (02-domain-model §4). Decisions carry reason chains naming the
granting engagement, so permission behavior stays debuggable as the
model grows (04-architecture "Decisions carry reasons").

M3 actions (read-only mirror):
  - capability actions: "capability:<name>"  (resource ignored)
  - "entries:read"   resource: anything exposing .import_imported_by
                     (and optionally .tenant_id) — see EntryRef
  - "imports:read"   resource: anything exposing .imported_by

The can()/scope() equivalence property (tests/authz) is the contract:
for every actor and row, can(actor, "entries:read", row).allowed iff
scope(actor, "entries:read", select(...)) returns that row.
"""

from __future__ import annotations

import uuid
from dataclasses import dataclass

from app.services.authz.capabilities import ROLE_GRANTS


@dataclass(frozen=True)
class Decision:
    allowed: bool
    reason_chain: tuple[str, ...]

    def __bool__(self) -> bool:  # `if can(...)` reads naturally
        return self.allowed


@dataclass(frozen=True)
class EntryRef:
    """The minimal row shape can() needs for entry decisions — build it
    from a loaded row (entry JOIN import_batches)."""

    tenant_id: uuid.UUID | None
    import_imported_by: uuid.UUID | None  # None = no batch OR batch has no importer


def _allow(*reasons: str) -> Decision:
    return Decision(True, tuple(reasons))


def _deny(*reasons: str) -> Decision:
    return Decision(False, tuple(reasons))


def _actor_tag(actor) -> str:
    label = actor.username or "<anonymous>"
    return f"user({label})"


def _grant_reason(actor, capability: str) -> tuple[bool, str]:
    if actor.is_super_admin and not actor.is_viewing_as:
        return True, f"{_actor_tag(actor)} is super_admin => {capability}"
    granted = ROLE_GRANTS[capability].get(actor.role, False)
    via = (
        f"{_actor_tag(actor)} --{actor.role_reason}--> tenant({actor.tenant_name})"
        if actor.role != "none"
        else f"{_actor_tag(actor)} has no tenant role"
    )
    verdict = f"{actor.role} => {capability}" if granted else f"{actor.role} =/=> {capability}"
    return granted, f"{via}; {verdict}"


def can(actor, action: str, resource=None) -> Decision:
    if action.startswith("capability:"):
        capability = action.split(":", 1)[1]
        granted, reason = _grant_reason(actor, capability)
        return _allow(reason) if granted else _deny(reason)

    if action == "entries:read":
        return _can_read_entry(actor, resource)

    if action == "imports:read":
        return _can_read_import(actor, resource)

    raise ValueError(f"can(): unknown action {action!r}")


def _tenant_ok(actor, resource) -> Decision | None:
    """Cross-tenant rows are denied for everyone — super admin included
    within a tenant context (parity: rpcs §3.7 'the super-admin branch
    only bypasses importer scoping, not org membership')."""
    resource_tenant = getattr(resource, "tenant_id", None)
    if actor.tenant_id is None:
        return _deny(f"{_actor_tag(actor)}: no tenant context resolved")
    if resource_tenant is not None and resource_tenant != actor.tenant_id:
        return _deny(
            f"row.tenant_id={resource_tenant} outside actor tenant({actor.tenant_name})"
        )
    return None


def _can_read_entry(actor, resource) -> Decision:
    denied = _tenant_ok(actor, resource)
    if denied is not None:
        return denied

    if actor.is_super_admin and not actor.is_viewing_as:
        return _allow(f"{_actor_tag(actor)} is super_admin => entries:read (tenant-wide)")
    if actor.role == "owner":
        return _allow(
            f"{_actor_tag(actor)} --{actor.role_reason}--> tenant({actor.tenant_name})"
            " => entries:read (tenant-wide)"
        )
    if actor.role in ("manager", "worker"):
        via = (
            f"{_actor_tag(actor)} --{actor.role_reason}--> tenant({actor.tenant_name})"
            " => entries:read:own-imports"
        )
        imported_by = getattr(resource, "import_imported_by", None)
        if imported_by is not None and imported_by == actor.user_id:
            return _allow(via, f"entry.import_batch.imported_by == {_actor_tag(actor)}")
        return _deny(
            via,
            "entry has no import batch"
            if imported_by is None
            else "entry.import_batch.imported_by is someone else",
        )
    return _deny(f"{_actor_tag(actor)} has no tenant role => entries:read denied")


def _can_read_import(actor, resource) -> Decision:
    denied = _tenant_ok(actor, resource)
    if denied is not None:
        return denied

    # Batch METADATA is tenant-visible for any role (parity: rpcs §4
    # clockify_imports org-wide policy) — EXCEPT under view-as of a
    # non-owner, where the view narrows to the target's own batches
    # (parity with the old app's stricter view-as overlay, B48).
    if actor.role == "none" and not (actor.is_super_admin and not actor.is_viewing_as):
        return _deny(f"{_actor_tag(actor)} has no tenant role => imports:read denied")
    if actor.is_viewing_as and actor.role not in ("owner",) and not actor.is_super_admin:
        imported_by = getattr(resource, "imported_by", None)
        if imported_by == actor.user_id:
            return _allow(
                f"view-as({actor.username}): batch.imported_by == effective user"
            )
        return _deny(
            f"view-as({actor.username}): batch belongs to someone else"
            " (view-as narrows batch metadata)"
        )
    return _allow(
        f"{_actor_tag(actor)}: batch metadata is tenant-visible (role={actor.role})"
    )
