from __future__ import annotations

from typing import Any

from .audit import AuditEvent, tags_to_strings
from .context import RunContext
from .types import BoundaryMap, PolicyDecision, PolicyRequest, PolicySet, ToolRules


def is_visible(
    rules: ToolRules,
    active_tags: set[Any],
    boundaries: BoundaryMap,
) -> bool:
    if rules.blocked_by and rules.blocked_by.intersection(active_tags):
        return False
    if rules.boundary is None:
        return True
    boundary_rule = boundaries.get(rules.boundary)
    if boundary_rule is True:
        return len(active_tags) == 0
    if boundary_rule:
        if set(boundary_rule).intersection(active_tags):
            return False
    return True


async def evaluate_policies(
    rules: ToolRules,
    request: PolicyRequest,
) -> PolicyDecision:
    policies = rules.policies
    return await _eval_policy_set(policies, request)


async def _eval_policy_set(policies: PolicySet, request: PolicyRequest) -> PolicyDecision:
    if policies.require:
        require_result = await _eval_require(policies, request)
        if require_result.outcome != "allow":
            return _normalize_denial(policies, require_result)

    if policies.any_of:
        any_result = await _eval_any_of(policies, request)
        if any_result.outcome != "allow":
            return _normalize_denial(policies, any_result)

    return PolicyDecision.allow("policies passed")


async def _eval_require(policies: PolicySet, request: PolicyRequest) -> PolicyDecision:
    approvals: list[PolicyDecision] = []
    for policy in policies.require:
        result = await policy(request)
        if result.outcome == "deny":
            return result
        if result.outcome == "approval":
            approvals.append(result)
    if approvals:
        return approvals[0]
    return PolicyDecision.allow("require policies passed")


async def _eval_any_of(policies: PolicySet, request: PolicyRequest) -> PolicyDecision:
    approvals: list[PolicyDecision] = []
    for policy in policies.any_of:
        result = await policy(request)
        if result.outcome == "allow":
            return result
        if result.outcome == "approval":
            approvals.append(result)
    if approvals:
        return approvals[0]
    return PolicyDecision.deny("no any_of policy allowed")


def _normalize_denial(policies: PolicySet, result: PolicyDecision) -> PolicyDecision:
    if result.outcome == "deny" and policies.denied_message:
        return PolicyDecision.deny(policies.denied_message)
    return result


def record_visibility_event(
    ctx: RunContext | None,
    tool_name: str,
    rules: ToolRules,
    active_tags: set[Any],
    visible: bool,
    *,
    tool_kind: str,
    mcp_server: str | None = None,
) -> None:
    if ctx is None:
        return
    if visible:
        return
    reason = "blocked_by" if rules.blocked_by else "boundary"
    ctx.audit(
        AuditEvent(
            event_type="tool_hidden",
            tool_kind=tool_kind,
            tool_name=tool_name,
            mcp_server=mcp_server,
            agent_id=ctx.agent_id,
            run_id=ctx.run_id,
            active_tags=tags_to_strings(active_tags),
            reason=reason,
            boundary=str(rules.boundary) if rules.boundary is not None else None,
        )
    )


def record_policy_event(
    ctx: RunContext | None,
    tool_name: str,
    rules: ToolRules,
    decision: PolicyDecision,
    *,
    tool_kind: str,
    enforced: bool,
    mcp_server: str | None = None,
    tool_call_id: str | None = None,
) -> None:
    if ctx is None:
        return
    if decision.outcome == "allow":
        return
    ctx.audit(
        AuditEvent(
            event_type="policy_decision",
            tool_kind=tool_kind,
            tool_name=tool_name,
            tool_call_id=tool_call_id,
            mcp_server=mcp_server,
            agent_id=ctx.agent_id,
            run_id=ctx.run_id,
            active_tags=tags_to_strings(ctx.active_tags),
            policy_reason=decision.reason,
            decision=decision.outcome,
            enforced=enforced,
        )
    )

