from __future__ import annotations

import copy
import fnmatch
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from .events import sha256_json
from .models import PolicyDecision, ToolCall


class CapabilityPolicyError(RuntimeError):
    pass


def _matches(patterns: tuple[str, ...], value: str) -> bool:
    return any(fnmatch.fnmatchcase(value, pattern) for pattern in patterns)


@dataclass(frozen=True)
class TenantBinding:
    resource_patterns: tuple[str, ...]
    tenant_id: str

    @classmethod
    def from_dict(cls, raw: dict[str, Any]) -> "TenantBinding":
        return cls(
            resource_patterns=tuple(str(item) for item in raw["resource_patterns"]),
            tenant_id=str(raw["tenant_id"]),
        )


@dataclass
class CapabilityGrant:
    grant_id: str
    subjects: tuple[str, ...]
    actions: tuple[str, ...]
    resources: tuple[str, ...]
    tenant_id: str
    expires_at_step: int
    constraints: dict[str, Any] = field(default_factory=dict)

    @classmethod
    def from_dict(cls, raw: dict[str, Any]) -> "CapabilityGrant":
        return cls(
            grant_id=str(raw["grant_id"]),
            subjects=tuple(str(item) for item in raw["subjects"]),
            actions=tuple(str(item) for item in raw["actions"]),
            resources=tuple(str(item) for item in raw["resources"]),
            tenant_id=str(raw["tenant_id"]),
            expires_at_step=int(raw["expires_at_step"]),
            constraints=dict(raw.get("constraints", {})),
        )

    def as_dict(self) -> dict[str, Any]:
        return {
            "grant_id": self.grant_id,
            "subjects": list(self.subjects),
            "actions": list(self.actions),
            "resources": list(self.resources),
            "tenant_id": self.tenant_id,
            "expires_at_step": self.expires_at_step,
            "constraints": copy.deepcopy(self.constraints),
        }


@dataclass
class CapabilityPolicy:
    policy_id: str
    version: str
    fail_closed: bool
    reset_required: bool
    require_tenant_binding: bool
    trusted_provenance: tuple[str, ...]
    implicit_effect: str
    tenant_bindings: tuple[TenantBinding, ...]
    grants: list[CapabilityGrant]
    mutation_id: str = ""

    @classmethod
    def load(cls, path: Path) -> "CapabilityPolicy":
        raw = json.loads(path.read_text(encoding="utf-8"))
        policy = cls(
            policy_id=str(raw["policy_id"]),
            version=str(raw["version"]),
            fail_closed=bool(raw["fail_closed"]),
            reset_required=bool(raw["reset_required"]),
            require_tenant_binding=bool(raw["require_tenant_binding"]),
            trusted_provenance=tuple(
                str(item) for item in raw["trusted_provenance"]
            ),
            implicit_effect=str(raw["implicit_effect"]),
            tenant_bindings=tuple(
                TenantBinding.from_dict(item) for item in raw["tenant_bindings"]
            ),
            grants=[CapabilityGrant.from_dict(item) for item in raw["grants"]],
        )
        policy.validate()
        return policy

    def clone(self) -> "CapabilityPolicy":
        return copy.deepcopy(self)

    def validate(self) -> None:
        if self.implicit_effect not in {"allow", "deny"}:
            raise CapabilityPolicyError("Invalid implicit effect")
        if not self.grants:
            raise CapabilityPolicyError("At least one capability grant is required")
        grant_ids = [item.grant_id for item in self.grants]
        if len(grant_ids) != len(set(grant_ids)):
            raise CapabilityPolicyError("Capability grant identifiers must be unique")
        if not self.tenant_bindings:
            raise CapabilityPolicyError("Tenant bindings are required")

    def as_dict(self) -> dict[str, Any]:
        return {
            "policy_id": self.policy_id,
            "version": self.version,
            "architecture": "server-resolved-capability-grants",
            "fail_closed": self.fail_closed,
            "reset_required": self.reset_required,
            "require_tenant_binding": self.require_tenant_binding,
            "trusted_provenance": list(self.trusted_provenance),
            "implicit_effect": self.implicit_effect,
            "mutation_id": self.mutation_id,
            "tenant_bindings": [
                {
                    "resource_patterns": list(item.resource_patterns),
                    "tenant_id": item.tenant_id,
                }
                for item in self.tenant_bindings
            ],
            "grants": [item.as_dict() for item in self.grants],
        }


@dataclass(frozen=True)
class ApprovalBinding:
    action: str
    resource: str
    arguments_sha256: str
    expires_at_step: int


@dataclass(frozen=True)
class MediationEnvelope:
    tenant_id: str
    provenance: str
    step: int
    approval: ApprovalBinding | None
    parent_capabilities: tuple[str, ...]


def _resolve_tenant(policy: CapabilityPolicy, resource: str) -> str:
    matches = {
        binding.tenant_id
        for binding in policy.tenant_bindings
        if _matches(binding.resource_patterns, resource)
    }
    if len(matches) != 1:
        return ""
    return next(iter(matches))


def build_mediation_envelope(
    policy: CapabilityPolicy, call: ToolCall
) -> MediationEnvelope:
    step = int(call.context.get("step", 0))
    approval = None
    if call.context.get("approved") is True:
        approval = ApprovalBinding(
            action=call.action,
            resource=call.resource,
            arguments_sha256=sha256_json(call.arguments),
            expires_at_step=step,
        )
    parent_raw = call.context.get("parent_capabilities", [])
    if not isinstance(parent_raw, list) or not all(
        isinstance(item, str) for item in parent_raw
    ):
        raise CapabilityPolicyError("Invalid parent capability context")
    return MediationEnvelope(
        tenant_id=_resolve_tenant(policy, call.resource),
        provenance=str(call.context.get("provenance", "untrusted")),
        step=step,
        approval=approval,
        parent_capabilities=tuple(parent_raw),
    )


def _constraints_match(
    grant: CapabilityGrant,
    call: ToolCall,
    envelope: MediationEnvelope,
) -> tuple[bool, str]:
    constraints = grant.constraints
    for key, allowed in constraints.get("argument_in", {}).items():
        if call.arguments.get(key) not in allowed:
            return False, f"argument_in:{key}"

    for key, maximum in constraints.get("argument_max", {}).items():
        try:
            value = float(call.arguments[key])
        except (KeyError, TypeError, ValueError):
            return False, f"argument_max:{key}"
        if value > float(maximum):
            return False, f"argument_max:{key}"

    if constraints.get("approval_binding_required"):
        approval = envelope.approval
        if approval is None:
            return False, "approval:missing"
        if approval.action != call.action or approval.resource != call.resource:
            return False, "approval:scope"
        if approval.arguments_sha256 != sha256_json(call.arguments):
            return False, "approval:arguments"
        if approval.expires_at_step < envelope.step:
            return False, "approval:expired"

    if constraints.get("parent_capability_intersection"):
        requested_raw = call.arguments.get("capabilities")
        if not isinstance(requested_raw, list) or not all(
            isinstance(item, str) for item in requested_raw
        ):
            return False, "delegation:requested_capabilities"
        if not set(requested_raw).issubset(set(envelope.parent_capabilities)):
            return False, "delegation:intersection"

    return True, ""


class CapabilityPolicyEngine:
    """Independent capability-grant evaluator with server-resolved bindings."""

    def __init__(self, policy: CapabilityPolicy) -> None:
        self.policy = policy

    def evaluate(
        self,
        call: ToolCall,
        envelope: MediationEnvelope | None = None,
    ) -> PolicyDecision:
        try:
            envelope = envelope or build_mediation_envelope(self.policy, call)
            if envelope.provenance not in self.policy.trusted_provenance:
                return PolicyDecision(
                    effect="deny",
                    rule_id="capability-untrusted-provenance",
                    reason="The mediation provenance is not trusted.",
                )
            if self.policy.require_tenant_binding and not envelope.tenant_id:
                return PolicyDecision(
                    effect="deny",
                    rule_id="capability-tenant-unresolved",
                    reason="The resource has no unique server-side tenant binding.",
                )

            failures: list[dict[str, str]] = []
            candidates = sorted(self.policy.grants, key=lambda item: item.grant_id)
            for grant in candidates:
                if not _matches(grant.subjects, call.subject):
                    continue
                if not _matches(grant.actions, call.action):
                    continue
                if not _matches(grant.resources, call.resource):
                    continue
                if (
                    self.policy.require_tenant_binding
                    and grant.tenant_id != envelope.tenant_id
                ):
                    failures.append(
                        {"grant_id": grant.grant_id, "failed": "tenant"}
                    )
                    continue
                if envelope.step > grant.expires_at_step:
                    failures.append(
                        {"grant_id": grant.grant_id, "failed": "expiry"}
                    )
                    continue
                constraints_ok, failed = _constraints_match(
                    grant, call, envelope
                )
                if not constraints_ok:
                    failures.append(
                        {"grant_id": grant.grant_id, "failed": failed}
                    )
                    continue
                return PolicyDecision(
                    effect="allow",
                    rule_id=grant.grant_id,
                    reason="A server-resolved capability grant authorizes the call.",
                    evidence={
                        "tenant_id": envelope.tenant_id,
                        "constraint_failures": failures,
                    },
                )

            return PolicyDecision(
                effect=self.policy.implicit_effect,
                rule_id="capability-implicit-fallback",
                reason="No capability grant authorized the call.",
                evidence={
                    "tenant_id": envelope.tenant_id,
                    "constraint_failures": failures,
                },
            )
        except Exception as exc:
            if self.policy.fail_closed:
                return PolicyDecision(
                    effect="deny",
                    rule_id="capability-error-fail-closed",
                    reason=(
                        "Capability evaluation failed closed: "
                        f"{type(exc).__name__}"
                    ),
                )
            return PolicyDecision(
                effect="allow",
                rule_id="capability-error-fail-open",
                reason=(
                    "Mutated capability evaluation failed open: "
                    f"{type(exc).__name__}"
                ),
            )
