"""
Autonomy Gate - Four-tier autonomy decision engine.

This module implements the autonomy gate that decides whether agent
actions can proceed automatically or require human approval.

Four Tiers:
1. AUTO-ALLOW: Action proceeds without approval (LOW risk)
2. AUTO-EDIT: File edits OK, commands need review (MEDIUM risk)
3. SUGGEST-ONLY: Always ask, show diff (HIGH risk)
4. AUTO-REJECT: Never allow, block immediately (CRITICAL risk)

Usage:
    from agent_orchestrator.control.autonomy_gate import AutonomyGate

    gate = AutonomyGate(db)
    decision = gate.evaluate("file_edit", "/src/main.py", agent_id)

    if decision.allowed:
        # Proceed with action
    elif decision.needs_approval:
        # Request approval
    elif decision.rejected:
        # Block and log
"""

import logging
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Optional

from ..persistence.database import OrchestratorDB
from ..persistence.models import Approval
from .risk_policy import (
    RiskPolicy,
    RiskLevel,
    RiskClassification,
    get_risk_policy,
)


logger = logging.getLogger(__name__)


class AutonomyTier(Enum):
    """Autonomy tier levels."""

    AUTO_ALLOW = "auto_allow"  # Proceed without approval
    AUTO_EDIT = "auto_edit"  # File edits OK, commands reviewed
    SUGGEST_ONLY = "suggest_only"  # Always ask, show diff
    AUTO_REJECT = "auto_reject"  # Never allow


class ActionType(Enum):
    """Types of agent actions."""

    FILE_READ = "file_read"
    FILE_WRITE = "file_write"
    FILE_EDIT = "file_edit"
    FILE_DELETE = "file_delete"
    COMMAND = "command"
    GIT_OPERATION = "git_operation"
    API_CALL = "api_call"
    NETWORK = "network"


@dataclass
class GateDecision:
    """Decision from the autonomy gate."""

    allowed: bool  # Action can proceed
    needs_approval: bool  # Action requires approval
    rejected: bool  # Action auto-rejected
    tier: AutonomyTier
    risk_level: RiskLevel
    reason: str
    approval_id: Optional[str] = None  # If approval created
    suggestion: Optional[str] = None  # For SUGGEST_ONLY
    diff_preview: Optional[str] = None  # For edits
    metadata: dict[str, Any] = field(default_factory=dict)
    timestamp: datetime = field(default_factory=datetime.now)

    def __str__(self) -> str:
        """Human-readable representation."""
        status = "ALLOWED" if self.allowed else "NEEDS_APPROVAL" if self.needs_approval else "REJECTED"
        return f"GateDecision({status}, tier={self.tier.value}, reason={self.reason})"


@dataclass
class AgentAutonomy:
    """Autonomy configuration for an agent."""

    agent_id: str
    max_risk_tier: RiskLevel = RiskLevel.MEDIUM  # Maximum allowed without approval
    can_auto_edit: bool = True  # Allow file edits at MEDIUM
    can_execute_commands: bool = True  # Allow command execution
    budget_limit_usd: float = 10.0  # Daily budget limit
    commands_allow_list: list[str] = field(default_factory=list)
    commands_block_list: list[str] = field(default_factory=list)
    files_allow_list: list[str] = field(default_factory=list)
    files_block_list: list[str] = field(default_factory=list)


class AutonomyGate:
    """
    Decision engine for agent autonomy.

    Evaluates requested actions against risk policy and agent
    autonomy configuration to decide approval requirements.
    """

    def __init__(
        self,
        db: OrchestratorDB,
        risk_policy: Optional[RiskPolicy] = None,
        default_autonomy: Optional[AgentAutonomy] = None,
    ):
        """
        Initialize the autonomy gate.

        Args:
            db: Database for approvals
            risk_policy: Risk classification policy
            default_autonomy: Default autonomy settings
        """
        self.db = db
        self.risk_policy = risk_policy or get_risk_policy()

        # Per-agent autonomy configuration
        self._agent_autonomy: dict[str, AgentAutonomy] = {}

        # Default autonomy for unregistered agents
        self._default_autonomy = default_autonomy or AgentAutonomy(
            agent_id="default",
            max_risk_tier=RiskLevel.MEDIUM,
            can_auto_edit=True,
            can_execute_commands=True,
        )

        # Cache of recent approvals (to avoid duplicate requests)
        self._recent_approvals: dict[str, datetime] = {}
        self._approval_cache_ttl = timedelta(minutes=5)

    def register_agent(self, autonomy: AgentAutonomy) -> None:
        """
        Register autonomy configuration for an agent.

        Args:
            autonomy: Agent autonomy settings
        """
        self._agent_autonomy[autonomy.agent_id] = autonomy
        logger.info(f"Registered autonomy for agent {autonomy.agent_id}")

    def get_autonomy(self, agent_id: str) -> AgentAutonomy:
        """Get autonomy settings for an agent."""
        return self._agent_autonomy.get(agent_id, self._default_autonomy)

    def evaluate(
        self,
        action_type: str,
        target: str,
        agent_id: str,
        context: Optional[dict[str, Any]] = None,
    ) -> GateDecision:
        """
        Evaluate an action and return a gate decision.

        Args:
            action_type: Type of action (file_edit, command, etc.)
            target: Target of the action (file path, command)
            agent_id: Agent requesting the action
            context: Additional context

        Returns:
            GateDecision indicating whether action is allowed
        """
        context = context or {}
        autonomy = self.get_autonomy(agent_id)

        # Classify risk
        risk = self.risk_policy.classify_action(action_type, target)

        # Determine tier based on risk and autonomy
        tier = self._determine_tier(risk, action_type, autonomy)

        # Make decision based on tier
        decision = self._make_decision(tier, risk, action_type, target, agent_id, context)

        # Log the decision
        self._log_decision(decision, agent_id, action_type, target)

        return decision

    def _determine_tier(
        self,
        risk: RiskClassification,
        action_type: str,
        autonomy: AgentAutonomy,
    ) -> AutonomyTier:
        """Determine the autonomy tier for an action."""
        # CRITICAL is always auto-reject
        if risk.level == RiskLevel.CRITICAL:
            return AutonomyTier.AUTO_REJECT

        # HIGH always requires approval
        if risk.level == RiskLevel.HIGH:
            return AutonomyTier.SUGGEST_ONLY

        # MEDIUM depends on action type and autonomy
        if risk.level == RiskLevel.MEDIUM:
            # Check if agent can auto-edit files
            if action_type in ("file_write", "file_edit") and autonomy.can_auto_edit:
                return AutonomyTier.AUTO_EDIT
            # Check if agent can execute commands
            if action_type == "command" and autonomy.can_execute_commands:
                return AutonomyTier.AUTO_EDIT
            # Otherwise suggest only
            return AutonomyTier.SUGGEST_ONLY

        # LOW is auto-allow
        return AutonomyTier.AUTO_ALLOW

    def _make_decision(
        self,
        tier: AutonomyTier,
        risk: RiskClassification,
        action_type: str,
        target: str,
        agent_id: str,
        context: dict[str, Any],
    ) -> GateDecision:
        """Make the final gate decision."""
        if tier == AutonomyTier.AUTO_REJECT:
            return GateDecision(
                allowed=False,
                needs_approval=False,
                rejected=True,
                tier=tier,
                risk_level=risk.level,
                reason=f"Auto-rejected: {risk.reason}",
                metadata={"pattern": risk.pattern_matched},
            )

        if tier == AutonomyTier.SUGGEST_ONLY:
            # Create approval request
            approval_id = self._create_approval(
                agent_id=agent_id,
                action_type=action_type,
                target=target,
                risk_level=risk.level.value,
            )

            suggestion = self._generate_suggestion(action_type, target, context)

            return GateDecision(
                allowed=False,
                needs_approval=True,
                rejected=False,
                tier=tier,
                risk_level=risk.level,
                reason=f"Requires approval: {risk.reason}",
                approval_id=approval_id,
                suggestion=suggestion,
                diff_preview=context.get("diff"),
            )

        if tier == AutonomyTier.AUTO_EDIT:
            # Auto-edit allows file edits but may log/monitor
            return GateDecision(
                allowed=True,
                needs_approval=False,
                rejected=False,
                tier=tier,
                risk_level=risk.level,
                reason=f"Auto-edit allowed: {risk.reason}",
                metadata={"monitored": True},
            )

        # AUTO_ALLOW
        return GateDecision(
            allowed=True,
            needs_approval=False,
            rejected=False,
            tier=tier,
            risk_level=risk.level,
            reason=f"Auto-allowed: {risk.reason}",
        )

    def _create_approval(
        self,
        agent_id: str,
        action_type: str,
        target: str,
        risk_level: str,
    ) -> str:
        """Create an approval request in the database."""
        # Check for recent duplicate
        cache_key = f"{agent_id}:{action_type}:{target}"
        if cache_key in self._recent_approvals:
            if datetime.now() - self._recent_approvals[cache_key] < self._approval_cache_ttl:
                # Return existing approval (don't create duplicate)
                logger.debug(f"Using cached approval for {cache_key}")
                return cache_key

        approval_id = self.db.generate_approval_id()
        approval = Approval(
            id=approval_id,
            agent_id=agent_id,
            action_type=action_type,
            target=target,
            risk_level=risk_level,
            status="pending",
        )
        self.db.create_approval(approval)

        self._recent_approvals[cache_key] = datetime.now()

        return approval_id

    def _generate_suggestion(
        self,
        action_type: str,
        target: str,
        context: dict[str, Any],
    ) -> str:
        """Generate a suggestion message for the user."""
        if action_type == "command":
            return f"Agent wants to execute: `{target}`"
        elif action_type in ("file_write", "file_edit"):
            return f"Agent wants to modify: {target}"
        elif action_type == "file_delete":
            return f"Agent wants to delete: {target}"
        elif action_type == "git_operation":
            return f"Agent wants to perform git operation: {target}"
        else:
            return f"Agent requests action: {action_type} on {target}"

    def _log_decision(
        self,
        decision: GateDecision,
        agent_id: str,
        action_type: str,
        target: str,
    ) -> None:
        """Log the gate decision."""
        if decision.rejected:
            logger.warning(
                f"[BLOCKED] Agent {agent_id}: {action_type} on {target} - {decision.reason}"
            )
        elif decision.needs_approval:
            logger.info(
                f"[APPROVAL] Agent {agent_id}: {action_type} on {target} - {decision.reason}"
            )
        else:
            logger.debug(
                f"[ALLOWED] Agent {agent_id}: {action_type} on {target} - {decision.reason}"
            )

    def check_approval_status(self, approval_id: str) -> Optional[str]:
        """
        Check the status of an approval request.

        Args:
            approval_id: The approval ID

        Returns:
            Status string or None if not found
        """
        pending = self.db.get_pending_approvals()
        for approval in pending:
            if approval.id == approval_id:
                return approval.status
        return None

    def approve(
        self,
        approval_id: str,
        approved_by: str,
        notes: Optional[str] = None,
    ) -> bool:
        """
        Approve a pending request.

        Args:
            approval_id: The approval ID
            approved_by: Who approved it
            notes: Optional notes

        Returns:
            True if approval was updated
        """
        self.db.update_approval(
            approval_id=approval_id,
            status="approved",
            decided_by=approved_by,
            decision_notes=notes,
        )
        return True

    def reject(
        self,
        approval_id: str,
        rejected_by: str,
        reason: Optional[str] = None,
    ) -> bool:
        """
        Reject a pending request.

        Args:
            approval_id: The approval ID
            rejected_by: Who rejected it
            reason: Rejection reason

        Returns:
            True if approval was updated
        """
        self.db.update_approval(
            approval_id=approval_id,
            status="rejected",
            decided_by=rejected_by,
            decision_notes=reason,
        )
        return True

    # =========================================================================
    # Convenience methods for common action types
    # =========================================================================

    def can_read_file(self, file_path: str, agent_id: str) -> GateDecision:
        """Check if agent can read a file."""
        return self.evaluate("file_read", file_path, agent_id)

    def can_write_file(self, file_path: str, agent_id: str) -> GateDecision:
        """Check if agent can write a file."""
        return self.evaluate("file_write", file_path, agent_id)

    def can_edit_file(
        self,
        file_path: str,
        agent_id: str,
        diff: Optional[str] = None,
    ) -> GateDecision:
        """Check if agent can edit a file."""
        context = {"diff": diff} if diff else {}
        return self.evaluate("file_edit", file_path, agent_id, context)

    def can_delete_file(self, file_path: str, agent_id: str) -> GateDecision:
        """Check if agent can delete a file."""
        return self.evaluate("file_delete", file_path, agent_id)

    def can_execute_command(self, command: str, agent_id: str) -> GateDecision:
        """Check if agent can execute a command."""
        return self.evaluate("command", command, agent_id)

    def can_git_operation(self, operation: str, agent_id: str) -> GateDecision:
        """Check if agent can perform a git operation."""
        return self.evaluate("git_operation", operation, agent_id)


# =============================================================================
# Integrated Gate with Interrupt Handler
# =============================================================================

class IntegratedAutonomyGate(AutonomyGate):
    """
    AutonomyGate with integrated interrupt handler support.

    This class extends AutonomyGate to automatically request human
    approval when needed, using the configured interrupt handler.
    """

    def __init__(
        self,
        db: OrchestratorDB,
        interrupt_handler: Optional[Any] = None,  # CLIInterruptHandler or AsyncInterruptHandler
        risk_policy: Optional[RiskPolicy] = None,
        default_autonomy: Optional[AgentAutonomy] = None,
    ):
        """
        Initialize integrated gate.

        Args:
            db: Database for approvals
            interrupt_handler: Handler for approval requests
            risk_policy: Risk classification policy
            default_autonomy: Default autonomy settings
        """
        super().__init__(db, risk_policy, default_autonomy)
        self._interrupt_handler = interrupt_handler

    def set_interrupt_handler(self, handler: Any) -> None:
        """Set the interrupt handler."""
        self._interrupt_handler = handler

    async def evaluate_and_wait(
        self,
        action_type: str,
        target: str,
        agent_id: str,
        context: Optional[dict[str, Any]] = None,
    ) -> GateDecision:
        """
        Evaluate action and wait for approval if needed.

        Unlike evaluate(), this method will block and request
        human approval if the action requires it.

        Args:
            action_type: Type of action
            target: Target of action
            agent_id: Agent requesting action
            context: Additional context

        Returns:
            GateDecision with final decision after approval (if needed)
        """
        context = context or {}

        # First, evaluate
        decision = self.evaluate(action_type, target, agent_id, context)

        # If allowed or rejected, return immediately
        if decision.allowed or decision.rejected:
            return decision

        # If needs approval, request it
        if decision.needs_approval and self._interrupt_handler:
            from ..interrupt import ApprovalResponse

            response = await self._interrupt_handler.request_approval(
                agent_id=agent_id,
                action_type=action_type,
                target=target,
                risk_level=decision.risk_level.value,
                context=context,
                diff=context.get("diff"),
            )

            # Update decision based on approval response
            if response.approved:
                return GateDecision(
                    allowed=True,
                    needs_approval=False,
                    rejected=False,
                    tier=decision.tier,
                    risk_level=decision.risk_level,
                    reason=f"Approved by {response.decided_by}: {response.reason}",
                    approval_id=response.approval_id,
                    metadata={"approval_response": response.decision.value},
                )
            else:
                return GateDecision(
                    allowed=False,
                    needs_approval=False,
                    rejected=True,
                    tier=decision.tier,
                    risk_level=decision.risk_level,
                    reason=f"Rejected by {response.decided_by}: {response.reason}",
                    approval_id=response.approval_id,
                    metadata={"approval_response": response.decision.value},
                )

        # No handler configured, return original decision
        return decision

    async def request_approval(
        self,
        agent_id: str,
        action_type: str,
        target: str,
        risk_level: str,
        context: Optional[dict[str, Any]] = None,
    ) -> bool:
        """
        Request approval directly.

        Args:
            agent_id: Agent requesting approval
            action_type: Type of action
            target: Target of action
            risk_level: Risk level
            context: Additional context

        Returns:
            True if approved
        """
        if not self._interrupt_handler:
            logger.warning("No interrupt handler configured, auto-rejecting")
            return False

        response = await self._interrupt_handler.request_approval(
            agent_id=agent_id,
            action_type=action_type,
            target=target,
            risk_level=risk_level,
            context=context or {},
        )

        return response.approved


# =============================================================================
# Decorators for action protection
# =============================================================================

def requires_autonomy_check(action_type: str):
    """
    Decorator to require autonomy check before action.

    Usage:
        @requires_autonomy_check("file_edit")
        async def edit_file(self, path: str, content: str):
            ...
    """
    def decorator(func):
        async def wrapper(self, *args, **kwargs):
            # Get gate from self (assumes object has autonomy_gate attribute)
            gate = getattr(self, 'autonomy_gate', None)
            agent_id = getattr(self, 'agent_id', 'unknown')

            if gate is None:
                logger.warning(f"No autonomy gate for {func.__name__}")
                return await func(self, *args, **kwargs)

            # Get target from first positional arg
            target = args[0] if args else kwargs.get('path', kwargs.get('target', ''))

            decision = gate.evaluate(action_type, str(target), agent_id)

            if decision.rejected:
                raise PermissionError(f"Action blocked: {decision.reason}")

            if decision.needs_approval:
                raise PermissionError(f"Action requires approval: {decision.approval_id}")

            return await func(self, *args, **kwargs)

        return wrapper
    return decorator


# =============================================================================
# Singleton instance
# =============================================================================

_default_gate: Optional[AutonomyGate] = None


def get_autonomy_gate(db: Optional[OrchestratorDB] = None) -> AutonomyGate:
    """Get the default autonomy gate instance."""
    global _default_gate
    if _default_gate is None:
        if db is None:
            raise ValueError("Database required to initialize autonomy gate")
        _default_gate = AutonomyGate(db)
    return _default_gate


def set_autonomy_gate(gate: AutonomyGate) -> None:
    """Set the default autonomy gate instance."""
    global _default_gate
    _default_gate = gate
