"""
AgentActionReceipt: Event-side receipt for the Receipt Internet.

While CapabilityReceipt answers "were you allowed to do this?",
AgentActionReceipt answers "what did you actually do?"

Together they form the two sides of the Receipt Internet:
- CapabilityReceipt = permit (WIPE: Who/Intent/Place/Expiry)
- AgentActionReceipt = event (what happened, when, with what outcome)

Every significant agent action should produce both receipts, linked together.

Schema: agent_action.v1
"""
from __future__ import annotations

import hashlib
import json
import uuid
from datetime import datetime, timezone
from typing import Any, Dict, List, Literal, Optional

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

import logging

logger = logging.getLogger(__name__)


class AgentActionReceipt(BaseReceipt):
    """
    Event-level receipt recording what an agent actually did.
    Linked to the CapabilityReceipt that authorized the action.

    Invariants:
    - MUST reference a valid capability_receipt_id
    - effect MUST match the capability's action
    - subject MUST be within capability's scope
    - Only emitted on successful action (denied = no AgentActionReceipt)

    Example:
        {
            "receipt_type": "AgentActionReceipt",
            "action_id": "act_2025-12-10T10:30:00Z_fs_read_abc123",
            "capability_receipt_id": "cap_2025-12-10T03:01:00Z_fs_read_ccio",
            "agent_id": "agent:loom.screen_mac",
            "effect": "fs.read",
            "subject": "/Users/tim/ccio/src/main.py",
            "outcome": "success",
            ...
        }

    Schema: agent_action.v1
    """

    receipt_type: str = Field(default="AgentActionReceipt")
    domain: str = Field(default=Domain.GOVERNANCE.value)

    # Identity
    action_id: str = Field(
        description="Stable ID for this action within the episode"
    )
    episode_id: Optional[str] = Field(
        default=None,
        description="Link to enclosing Episode (if any)"
    )
    capability_receipt_id: str = Field(
        description="FK to CapabilityReceipt that authorized this action"
    )

    # Actor
    agent_id: str = Field(
        description="Which agent performed the action (e.g., 'agent:loom.screen_mac')"
    )
    agent_name: Optional[str] = Field(
        default=None,
        description="Human-readable agent name"
    )

    # Action
    effect: str = Field(
        description="Action type: 'fs.read', 'fs.write', 'shell.exec', 'http.fetch', 'provider.call'"
    )
    subject: str = Field(
        description="What was acted on (file path, URL, resource URI)"
    )

    # Integrity
    input_hash: str = Field(
        description="SHA-256 hash of key input parameters (for verifiability)"
    )
    output_hash: Optional[str] = Field(
        default=None,
        description="SHA-256 hash of result data (optional, for large outputs)"
    )

    # Context
    policy_hash: Optional[str] = Field(
        default=None,
        description="Hash of policy/gate config at decision time"
    )
    action_domain: Optional[str] = Field(
        default=None,
        description="Execution domain (e.g., 'clinical', 'code', 'sandbox')"
    )

    # Outcome
    outcome: Literal["success", "failure", "partial"] = Field(
        default="success",
        description="Result of the action"
    )
    error_code: Optional[str] = Field(
        default=None,
        description="If outcome != success, the specific error code"
    )
    error_message: Optional[str] = Field(
        default=None,
        description="If outcome != success, human-readable error"
    )

    # Timing
    started_at: Optional[datetime] = Field(
        default=None,
        description="When the action started (if tracked)"
    )
    completed_at: Optional[datetime] = Field(
        default=None,
        description="When the action completed"
    )
    duration_ms: Optional[int] = Field(
        default=None,
        description="Duration in milliseconds"
    )

    # Lineage to underlying motor actions
    underlying_action_ids: List[str] = Field(
        default_factory=list,
        description=(
            "IDs of low-level ActionReceipts (e.g., from Hands organ) that were "
            "executed as part of this tool-level action. Enables tracing from "
            "tool invocation down to actual motor/shell commands."
        )
    )

    # Extension
    extra: Dict[str, Any] = Field(
        default_factory=dict,
        description="Reserved for future fields, metrics, etc."
    )

    @field_validator("completed_at", "started_at", mode="before")
    @classmethod
    def _ensure_timezone(cls, v: Any) -> Optional[datetime]:
        """Ensure timestamps are timezone-aware."""
        if v is None:
            return None
        if isinstance(v, str):
            v = datetime.fromisoformat(v.replace("Z", "+00:00"))
        if isinstance(v, datetime) and v.tzinfo is None:
            return v.replace(tzinfo=timezone.utc)
        return v

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        if hasattr(self, "model_dump"):
            return self.model_dump(mode="json")
        else:
            return self.dict()


def compute_input_hash(effect: str, subject: str, options: Optional[Dict[str, Any]] = None) -> str:
    """
    Compute a stable hash of action inputs for integrity verification.

    Args:
        effect: Action type (fs.read, etc.)
        subject: Target resource (file path, URL, etc.)
        options: Optional additional parameters

    Returns:
        SHA-256 hex digest
    """
    data = {
        "effect": effect,
        "subject": subject,
        "options": options or {},
    }
    canonical = json.dumps(data, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(canonical.encode()).hexdigest()


def compute_output_hash(content: bytes) -> str:
    """
    Compute SHA-256 hash of output content.

    Args:
        content: Raw bytes of the output

    Returns:
        SHA-256 hex digest
    """
    return hashlib.sha256(content).hexdigest()


class CapabilityRefusalReceipt(BaseReceipt):
    """
    Receipt emitted when a capability gate denies an action.

    Unlike AgentActionReceipt (only on success), CapabilityRefusalReceipt
    documents why an action was NOT allowed to proceed.

    This is critical for:
    - Audit trails (proof of what DIDN'T happen)
    - Debugging capability configurations
    - Trust/reputation systems (track denied attempts)
    - Policy tuning (understand what gets blocked)

    Schema: capability_refusal.v1
    """

    receipt_type: str = Field(default="CapabilityRefusalReceipt")
    domain: str = Field(default=Domain.GOVERNANCE.value)

    # Identity
    refusal_id: str = Field(
        description="Stable ID for this refusal event"
    )
    episode_id: Optional[str] = Field(
        default=None,
        description="Link to enclosing Episode (if any)"
    )

    # What was attempted
    agent_id: str = Field(
        description="Which agent attempted the action"
    )
    attempted_effect: str = Field(
        description="What action was attempted (fs.read, shell.exec, etc.)"
    )
    attempted_subject: str = Field(
        description="Target resource that was denied"
    )
    capability_id: Optional[str] = Field(
        default=None,
        description="Capability ID that was checked (if provided)"
    )

    # Why it was denied
    denial_reason: str = Field(
        description="Machine-readable denial code (expired, scope_violation, action_mismatch, etc.)"
    )
    denial_message: str = Field(
        description="Human-readable explanation"
    )
    failed_checks: list = Field(
        default_factory=list,
        description="List of specific checks that failed"
    )

    # Context
    policy_hash: Optional[str] = Field(
        default=None,
        description="Policy hash at denial time"
    )
    action_domain: Optional[str] = Field(
        default=None,
        description="Execution domain where denial occurred"
    )

    # Timing
    denied_at: Optional[datetime] = Field(
        default=None,
        description="When the denial occurred"
    )

    # Extension
    extra: Dict[str, Any] = Field(
        default_factory=dict,
        description="Reserved for additional context"
    )

    @field_validator("denied_at", mode="before")
    @classmethod
    def _ensure_denied_at_tz(cls, v: Any) -> Optional[datetime]:
        """Ensure denied_at is timezone-aware."""
        if v is None:
            return None
        if isinstance(v, str):
            v = datetime.fromisoformat(v.replace("Z", "+00:00"))
        if isinstance(v, datetime) and v.tzinfo is None:
            return v.replace(tzinfo=timezone.utc)
        return v

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        if hasattr(self, "model_dump"):
            return self.model_dump(mode="json")
        else:
            return self.dict()


def create_capability_refusal_receipt(
    agent_id: str,
    attempted_effect: str,
    attempted_subject: str,
    denial_reason: str,
    denial_message: str,
    capability_id: Optional[str] = None,
    episode_id: Optional[str] = None,
    policy_hash: Optional[str] = None,
    action_domain: Optional[str] = None,
    failed_checks: Optional[list] = None,
    extra: Optional[Dict[str, Any]] = None,
) -> CapabilityRefusalReceipt:
    """
    Create a CapabilityRefusalReceipt documenting why an action was denied.

    Args:
        agent_id: Which agent attempted the action
        attempted_effect: What action was attempted
        attempted_subject: Target resource
        denial_reason: Machine-readable denial code
        denial_message: Human-readable explanation
        capability_id: The capability that was checked (if any)
        episode_id: Optional parent episode
        policy_hash: Policy version at denial time
        action_domain: Execution domain
        failed_checks: Specific checks that failed
        extra: Additional metadata

    Returns:
        CapabilityRefusalReceipt ready for storage
    """
    now = datetime.now(timezone.utc)
    ts = now.strftime("%Y-%m-%dT%H:%M:%SZ")
    refusal_id = f"ref_{ts}_{attempted_effect.replace('.', '_')}_{uuid.uuid4().hex[:8]}"
    receipt_id = f"crr_{uuid.uuid4().hex[:16]}"

    return CapabilityRefusalReceipt(
        receipt_id=receipt_id,
        refusal_id=refusal_id,
        agent_id=agent_id,
        attempted_effect=attempted_effect,
        attempted_subject=attempted_subject,
        capability_id=capability_id,
        denial_reason=denial_reason,
        denial_message=denial_message,
        failed_checks=failed_checks or [],
        episode_id=episode_id,
        policy_hash=policy_hash,
        action_domain=action_domain,
        denied_at=now,
        extra=extra or {},
    )


def create_agent_action_receipt(
    capability_receipt_id: str,
    agent_id: str,
    effect: str,
    subject: str,
    input_hash: str,
    outcome: Literal["success", "failure", "partial"] = "success",
    output_hash: Optional[str] = None,
    episode_id: Optional[str] = None,
    policy_hash: Optional[str] = None,
    action_domain: Optional[str] = None,
    error_code: Optional[str] = None,
    error_message: Optional[str] = None,
    duration_ms: Optional[int] = None,
    underlying_action_ids: Optional[List[str]] = None,
    extra: Optional[Dict[str, Any]] = None,
) -> AgentActionReceipt:
    """
    Create an AgentActionReceipt with proper defaults.

    This is the primary factory function for creating action receipts.

    Args:
        capability_receipt_id: ID of the authorizing CapabilityReceipt
        agent_id: Which agent performed the action
        effect: Action type (fs.read, fs.write, etc.)
        subject: Target resource
        input_hash: Hash of input parameters
        outcome: Result status
        output_hash: Optional hash of result
        episode_id: Optional parent episode
        policy_hash: Optional policy version hash
        action_domain: Optional execution domain
        error_code: Error code if failed
        error_message: Error message if failed
        duration_ms: Action duration
        underlying_action_ids: IDs of low-level ActionReceipts (e.g., from Hands)
        extra: Additional metadata

    Returns:
        AgentActionReceipt ready for storage
    """
    now = datetime.now(timezone.utc)
    ts = now.strftime("%Y-%m-%dT%H:%M:%SZ")
    action_id = f"act_{ts}_{effect.replace('.', '_')}_{uuid.uuid4().hex[:8]}"
    receipt_id = f"aar_{uuid.uuid4().hex[:16]}"

    return AgentActionReceipt(
        receipt_id=receipt_id,
        action_id=action_id,
        capability_receipt_id=capability_receipt_id,
        episode_id=episode_id,
        agent_id=agent_id,
        effect=effect,
        subject=subject,
        input_hash=input_hash,
        output_hash=output_hash,
        policy_hash=policy_hash,
        action_domain=action_domain,
        outcome=outcome,
        error_code=error_code,
        error_message=error_message,
        completed_at=now,
        duration_ms=duration_ms,
        underlying_action_ids=underlying_action_ids or [],
        extra=extra or {},
    )


__all__ = [
    "AgentActionReceipt",
    "CapabilityRefusalReceipt",
    "compute_input_hash",
    "compute_output_hash",
    "create_agent_action_receipt",
    "create_capability_refusal_receipt",
]
