"""
Agent-to-Agent Call Receipts — Cross-boundary authority enforcement.

This module implements receipts for agent-to-agent calls, addressing the
ServiceNow/AppOmni class of vulnerabilities where low-privilege agents
can recruit high-privilege agents without proper authorization.

Key invariants:
1. No Discovery Without Permit: Agent discovery requires explicit authorization
2. No Borrowed Authority: Subcall privileges = intersection(caller, target, pccap)
3. Recruitment Requires PCCap: Every agent.call needs a signed capability token
4. Receipt Chain: Every call produces auditable receipts

Receipt types:
- AgentDiscoveryAttemptReceipt: When an agent tries to discover others
- AgentCallRequestReceipt: When an agent requests to call another
- AgentCallVerdictReceipt: Guardian decision on the call
- AgentCallOutcomeReceipt: What actually happened
- PrivilegeScopeDeltaReceipt: Track privilege changes

Reference: CSP Tool Safety Profile v1.2, Enterprise Hardening Guide
"""
from __future__ import annotations

import hashlib
import uuid
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, List, Optional, Set

from receipts.compat.pyd import Field
from receipts.base import BaseReceipt, Domain


class AgentCallDecision(str, Enum):
    """Decision outcomes for agent calls."""
    ALLOW = "ALLOW"
    DENY = "DENY"
    ESCALATE = "ESCALATE"


class AgentCallDenyReason(str, Enum):
    """Reasons for denying an agent call."""
    NO_PCCAP = "no_pccap"
    PCCAP_EXPIRED = "pccap_expired"
    PCCAP_SCOPE_MISMATCH = "pccap_scope_mismatch"
    PCCAP_SIGNATURE_INVALID = "pccap_signature_invalid"
    PRINCIPAL_MISMATCH = "principal_mismatch"
    TARGET_NOT_FOUND = "target_not_found"
    TARGET_NOT_DISCOVERABLE = "target_not_discoverable"
    PRIVILEGE_ESCALATION = "privilege_escalation"
    MAX_HOPS_EXCEEDED = "max_hops_exceeded"
    MAX_CALLS_EXCEEDED = "max_calls_exceeded"
    DISCOVERY_DISABLED = "discovery_disabled"
    GUARDIAN_DENY = "guardian_deny"
    KILL_SWITCH = "kill_switch"


class AgentDiscoveryAttemptReceipt(BaseReceipt):
    """
    Receipt for agent discovery attempts.

    Emitted when an agent tries to discover other agents in the system.
    Even failed attempts are recorded for audit/forensics.

    Invariants:
    - Discovery OFF by default; explicit permit required
    - Allowlist-only if enabled
    - Rate-limited per episode
    """
    receipt_type: str = Field(default="AgentDiscoveryAttemptReceipt")
    domain: str = Field(default=Domain.GOVERNANCE.value)
    schema_id: str = Field(default="agent_discovery_attempt.v1")

    # Request details
    requester_agent_id: str = Field(
        description="Agent attempting discovery (e.g., agent:low_priv_reader)"
    )
    discovery_scope: str = Field(
        default="all",
        description="What was being discovered (all, by_capability, by_name)"
    )
    discovery_filter: Optional[Dict[str, Any]] = Field(
        default=None,
        description="Filter criteria used in discovery"
    )

    # Decision
    allowed: bool = Field(
        description="Whether discovery was permitted"
    )
    deny_reason: Optional[AgentCallDenyReason] = Field(
        default=None,
        description="Why discovery was denied"
    )

    # Results (if allowed)
    agents_discovered: List[str] = Field(
        default_factory=list,
        description="List of agent IDs returned (if allowed)"
    )
    agents_filtered: int = Field(
        default=0,
        description="Number of agents filtered out by policy"
    )

    # Episode context
    episode_id: Optional[str] = Field(
        default=None,
        description="Current episode ID"
    )
    hop_depth: int = Field(
        default=0,
        description="Current hop depth in call chain"
    )

    @classmethod
    def create(
        cls,
        requester_agent_id: str,
        allowed: bool,
        discovery_scope: str = "all",
        discovery_filter: Optional[Dict[str, Any]] = None,
        deny_reason: Optional[AgentCallDenyReason] = None,
        agents_discovered: Optional[List[str]] = None,
        agents_filtered: int = 0,
        episode_id: Optional[str] = None,
        hop_depth: int = 0,
        parent_receipt_id: Optional[str] = None,
    ) -> "AgentDiscoveryAttemptReceipt":
        """Factory method."""
        receipt_id = f"adar_{uuid.uuid4().hex[:12]}"
        parent_ids = [parent_receipt_id] if parent_receipt_id else []

        return cls(
            receipt_id=receipt_id,
            requester_agent_id=requester_agent_id,
            discovery_scope=discovery_scope,
            discovery_filter=discovery_filter,
            allowed=allowed,
            deny_reason=deny_reason,
            agents_discovered=agents_discovered or [],
            agents_filtered=agents_filtered,
            episode_id=episode_id,
            hop_depth=hop_depth,
            parent_ids=parent_ids,
            valid_time=datetime.now(timezone.utc),
            allow_orphan=True,
        )


class AgentCallRequestReceipt(BaseReceipt):
    """
    Receipt for agent-to-agent call requests.

    Captures the full context of a cross-agent call attempt, including
    the PCCap token, caller/target identities, and requested capabilities.

    This receipt is emitted BEFORE the call is made (or denied).
    """
    receipt_type: str = Field(default="AgentCallRequestReceipt")
    domain: str = Field(default=Domain.GOVERNANCE.value)
    schema_id: str = Field(default="agent_call_request.v1")

    # Identities
    caller_agent_id: str = Field(
        description="Agent initiating the call"
    )
    target_agent_id: str = Field(
        description="Agent being called"
    )

    # Capability
    pccap_token_id: Optional[str] = Field(
        default=None,
        description="PCCap token ID authorizing this call"
    )
    pccap_hash: Optional[str] = Field(
        default=None,
        description="Hash of PCCap token for integrity"
    )

    # Request details
    requested_action: str = Field(
        description="What caller is asking target to do"
    )
    requested_tools: List[str] = Field(
        default_factory=list,
        description="Tools caller wants target to use"
    )
    args_hash: Optional[str] = Field(
        default=None,
        description="Hash of arguments (never store raw)"
    )

    # Privilege context
    caller_privilege_level: str = Field(
        default="unknown",
        description="Caller's current privilege level"
    )
    target_privilege_level: str = Field(
        default="unknown",
        description="Target's privilege level"
    )
    effective_privilege_requested: str = Field(
        default="unknown",
        description="Effective privilege being requested"
    )

    # Chain context
    episode_id: Optional[str] = Field(
        default=None,
        description="Current episode ID"
    )
    hop_depth: int = Field(
        default=0,
        description="Current depth in call chain"
    )
    call_chain: List[str] = Field(
        default_factory=list,
        description="Chain of agent IDs leading to this call"
    )

    @classmethod
    def create(
        cls,
        caller_agent_id: str,
        target_agent_id: str,
        requested_action: str,
        pccap_token_id: Optional[str] = None,
        pccap_hash: Optional[str] = None,
        requested_tools: Optional[List[str]] = None,
        args_hash: Optional[str] = None,
        caller_privilege_level: str = "unknown",
        target_privilege_level: str = "unknown",
        episode_id: Optional[str] = None,
        hop_depth: int = 0,
        call_chain: Optional[List[str]] = None,
        parent_receipt_id: Optional[str] = None,
    ) -> "AgentCallRequestReceipt":
        """Factory method."""
        receipt_id = f"acrr_{uuid.uuid4().hex[:12]}"
        parent_ids = [parent_receipt_id] if parent_receipt_id else []

        # Compute effective privilege = intersection rule
        # For now, simple comparison; should be formalized
        effective = min(
            [caller_privilege_level, target_privilege_level],
            key=lambda x: {"high": 3, "medium": 2, "low": 1, "unknown": 0}.get(x, 0)
        )

        return cls(
            receipt_id=receipt_id,
            caller_agent_id=caller_agent_id,
            target_agent_id=target_agent_id,
            requested_action=requested_action,
            pccap_token_id=pccap_token_id,
            pccap_hash=pccap_hash,
            requested_tools=requested_tools or [],
            args_hash=args_hash,
            caller_privilege_level=caller_privilege_level,
            target_privilege_level=target_privilege_level,
            effective_privilege_requested=effective,
            episode_id=episode_id,
            hop_depth=hop_depth,
            call_chain=call_chain or [],
            parent_ids=parent_ids,
            valid_time=datetime.now(timezone.utc),
            allow_orphan=True,
        )


class AgentCallVerdictReceipt(BaseReceipt):
    """
    Receipt for Guardian's decision on an agent call.

    This is the enforcement point: Guardian evaluates the request
    and decides ALLOW, DENY, or ESCALATE.

    Key checks:
    1. PCCap present and valid
    2. Signature verified
    3. Scope matches request
    4. No privilege escalation
    5. Hop/call limits not exceeded
    """
    receipt_type: str = Field(default="AgentCallVerdictReceipt")
    domain: str = Field(default=Domain.GOVERNANCE.value)
    schema_id: str = Field(default="agent_call_verdict.v1")

    # Link to request
    request_receipt_id: str = Field(
        description="ID of the AgentCallRequestReceipt"
    )

    # Decision
    verdict: AgentCallDecision = Field(
        description="ALLOW, DENY, or ESCALATE"
    )
    deny_reason: Optional[AgentCallDenyReason] = Field(
        default=None,
        description="Why call was denied (if DENY)"
    )
    reason_detail: Optional[str] = Field(
        default=None,
        description="Human-readable explanation"
    )

    # Enforcement details
    pccap_verified: bool = Field(
        default=False,
        description="Whether PCCap was valid"
    )
    privilege_check_passed: bool = Field(
        default=False,
        description="Whether privilege check passed (no escalation)"
    )
    scope_check_passed: bool = Field(
        default=False,
        description="Whether requested scope was within PCCap scope"
    )

    # Effective capabilities (intersection rule)
    effective_tools: List[str] = Field(
        default_factory=list,
        description="Tools the target CAN use (intersection)"
    )
    denied_tools: List[str] = Field(
        default_factory=list,
        description="Tools requested but denied"
    )

    # Timing
    latency_ms: float = Field(
        default=0.0,
        description="Time to make decision"
    )

    @classmethod
    def create(
        cls,
        request_receipt_id: str,
        verdict: AgentCallDecision,
        deny_reason: Optional[AgentCallDenyReason] = None,
        reason_detail: Optional[str] = None,
        pccap_verified: bool = False,
        privilege_check_passed: bool = False,
        scope_check_passed: bool = False,
        effective_tools: Optional[List[str]] = None,
        denied_tools: Optional[List[str]] = None,
        latency_ms: float = 0.0,
        parent_receipt_id: Optional[str] = None,
    ) -> "AgentCallVerdictReceipt":
        """Factory method."""
        receipt_id = f"acvr_{uuid.uuid4().hex[:12]}"
        parent_ids = [parent_receipt_id] if parent_receipt_id else []

        return cls(
            receipt_id=receipt_id,
            request_receipt_id=request_receipt_id,
            verdict=verdict,
            deny_reason=deny_reason,
            reason_detail=reason_detail,
            pccap_verified=pccap_verified,
            privilege_check_passed=privilege_check_passed,
            scope_check_passed=scope_check_passed,
            effective_tools=effective_tools or [],
            denied_tools=denied_tools or [],
            latency_ms=latency_ms,
            parent_ids=parent_ids,
            valid_time=datetime.now(timezone.utc),
            allow_orphan=True,
        )


class AgentCallOutcomeReceipt(BaseReceipt):
    """
    Receipt for the outcome of an agent call.

    Emitted AFTER the call completes (or fails), capturing what
    actually happened: tools used, results, errors, etc.
    """
    receipt_type: str = Field(default="AgentCallOutcomeReceipt")
    domain: str = Field(default=Domain.GOVERNANCE.value)
    schema_id: str = Field(default="agent_call_outcome.v1")

    # Links
    request_receipt_id: str = Field(
        description="ID of the AgentCallRequestReceipt"
    )
    verdict_receipt_id: str = Field(
        description="ID of the AgentCallVerdictReceipt"
    )

    # Outcome
    success: bool = Field(
        description="Whether the call succeeded"
    )
    error_code: Optional[str] = Field(
        default=None,
        description="Error code if failed"
    )
    error_message: Optional[str] = Field(
        default=None,
        description="Error message if failed"
    )

    # What happened
    tools_actually_used: List[str] = Field(
        default_factory=list,
        description="Tools the target actually invoked"
    )
    tool_receipts: List[str] = Field(
        default_factory=list,
        description="Receipt IDs from tool executions"
    )

    # Metrics
    duration_ms: float = Field(
        default=0.0,
        description="Total call duration"
    )
    tokens_used: int = Field(
        default=0,
        description="Tokens consumed by target"
    )

    @classmethod
    def create(
        cls,
        request_receipt_id: str,
        verdict_receipt_id: str,
        success: bool,
        error_code: Optional[str] = None,
        error_message: Optional[str] = None,
        tools_actually_used: Optional[List[str]] = None,
        tool_receipts: Optional[List[str]] = None,
        duration_ms: float = 0.0,
        tokens_used: int = 0,
        parent_receipt_id: Optional[str] = None,
    ) -> "AgentCallOutcomeReceipt":
        """Factory method."""
        receipt_id = f"acor_{uuid.uuid4().hex[:12]}"
        parent_ids = [parent_receipt_id] if parent_receipt_id else []

        return cls(
            receipt_id=receipt_id,
            request_receipt_id=request_receipt_id,
            verdict_receipt_id=verdict_receipt_id,
            success=success,
            error_code=error_code,
            error_message=error_message,
            tools_actually_used=tools_actually_used or [],
            tool_receipts=tool_receipts or [],
            duration_ms=duration_ms,
            tokens_used=tokens_used,
            parent_ids=parent_ids,
            valid_time=datetime.now(timezone.utc),
            allow_orphan=True,
        )


class PrivilegeScopeDeltaReceipt(BaseReceipt):
    """
    Receipt tracking privilege/scope changes within an episode.

    Used for the "Scope Expansion Events" HUD metric: in a correct
    system, effective capability should shrink or stay flat, not grow.

    A spike in scope expansion is a red flag for privilege escalation.
    """
    receipt_type: str = Field(default="PrivilegeScopeDeltaReceipt")
    domain: str = Field(default=Domain.GOVERNANCE.value)
    schema_id: str = Field(default="privilege_scope_delta.v1")

    # Episode context
    episode_id: str = Field(
        description="Episode where delta occurred"
    )
    step_index: int = Field(
        description="Step number in episode"
    )

    # Before/after state
    previous_tools: List[str] = Field(
        default_factory=list,
        description="Tools available before this step"
    )
    current_tools: List[str] = Field(
        default_factory=list,
        description="Tools available after this step"
    )

    # Delta analysis
    tools_added: List[str] = Field(
        default_factory=list,
        description="New tools gained (should be empty or rare)"
    )
    tools_removed: List[str] = Field(
        default_factory=list,
        description="Tools lost (normal)"
    )

    # Privilege levels
    previous_privilege_level: str = Field(
        default="unknown"
    )
    current_privilege_level: str = Field(
        default="unknown"
    )
    privilege_increased: bool = Field(
        default=False,
        description="True if privilege level went UP (red flag)"
    )

    # Trigger
    trigger_agent_call_id: Optional[str] = Field(
        default=None,
        description="AgentCallRequestReceipt that caused this delta"
    )

    @classmethod
    def create(
        cls,
        episode_id: str,
        step_index: int,
        previous_tools: List[str],
        current_tools: List[str],
        previous_privilege_level: str = "unknown",
        current_privilege_level: str = "unknown",
        trigger_agent_call_id: Optional[str] = None,
        parent_receipt_id: Optional[str] = None,
    ) -> "PrivilegeScopeDeltaReceipt":
        """Factory method."""
        receipt_id = f"psdr_{uuid.uuid4().hex[:12]}"
        parent_ids = [parent_receipt_id] if parent_receipt_id else []

        # Compute deltas
        prev_set = set(previous_tools)
        curr_set = set(current_tools)
        tools_added = list(curr_set - prev_set)
        tools_removed = list(prev_set - curr_set)

        # Privilege level comparison
        level_order = {"low": 1, "medium": 2, "high": 3, "unknown": 0}
        prev_level = level_order.get(previous_privilege_level, 0)
        curr_level = level_order.get(current_privilege_level, 0)
        privilege_increased = curr_level > prev_level

        return cls(
            receipt_id=receipt_id,
            episode_id=episode_id,
            step_index=step_index,
            previous_tools=previous_tools,
            current_tools=current_tools,
            tools_added=tools_added,
            tools_removed=tools_removed,
            previous_privilege_level=previous_privilege_level,
            current_privilege_level=current_privilege_level,
            privilege_increased=privilege_increased,
            trigger_agent_call_id=trigger_agent_call_id,
            parent_ids=parent_ids,
            valid_time=datetime.now(timezone.utc),
            allow_orphan=True,
        )


# Helper functions

def compute_args_hash(args: Dict[str, Any]) -> str:
    """Hash arguments for receipts (never store raw secrets)."""
    import json
    canonical = json.dumps(args, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(canonical.encode()).hexdigest()


def compute_pccap_hash(pccap_token: Dict[str, Any]) -> str:
    """Hash PCCap token for integrity verification."""
    import json
    # Exclude signature from hash (signature covers everything else)
    token_copy = {k: v for k, v in pccap_token.items() if k != "signature"}
    canonical = json.dumps(token_copy, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(canonical.encode()).hexdigest()


__all__ = [
    "AgentCallDecision",
    "AgentCallDenyReason",
    "AgentDiscoveryAttemptReceipt",
    "AgentCallRequestReceipt",
    "AgentCallVerdictReceipt",
    "AgentCallOutcomeReceipt",
    "PrivilegeScopeDeltaReceipt",
    "compute_args_hash",
    "compute_pccap_hash",
]
