"""ExecApprovalService.

Service for managing command execution approvals in plugin-shell.
Provides allowlist management and command analysis.

Example::

    service = ApprovalService()
    check = service.check_command(command="rm -rf /tmp/cache", cwd="/home/user", room_id="room-1")
    if check.requires_approval:
        result = await service.request_approval(check.request)
        if result.decision == ExecApprovalDecision.DENY:
            raise PermissionError("Command denied")
"""

from __future__ import annotations

import logging
import uuid
from dataclasses import dataclass, field

from elizaos_plugin_shell.approvals.allowlist import (
    add_allowlist_entry,
    load_approvals,
    record_allowlist_use,
    resolve_approvals,
)
from elizaos_plugin_shell.approvals.analysis import (
    analyze_shell_command,
    evaluate_shell_allowlist,
    requires_exec_approval,
    resolve_safe_bins,
)
from elizaos_plugin_shell.approvals.types import (
    EXEC_APPROVAL_TIMEOUT_MS,
    ApprovalResult,
    ExecAllowlistEntry,
    ExecApprovalDecision,
    ExecApprovalRequest,
    ExecApprovalsDefaults,
    ExecApprovalsResolved,
    ExecAsk,
    ExecCommandAnalysis,
    ExecSecurity,
)

logger = logging.getLogger(__name__)


@dataclass(slots=True)
class CommandCheckResult:
    """Command check result."""

    allowed: bool
    requires_approval: bool
    reason: str | None = None
    request: ExecApprovalRequest | None = None
    analysis: ExecCommandAnalysis = field(
        default_factory=lambda: ExecCommandAnalysis(ok=False, segments=[])
    )
    allowlist_matches: list[ExecAllowlistEntry] = field(default_factory=list)


class ApprovalService:
    """ExecApprovalService provides command execution approval management."""

    service_type = "exec_approval"

    def __init__(self, agent_id: str | None = None) -> None:
        self._agent_id = agent_id
        self._approval_config: ExecApprovalsResolved | None = None
        self._safe_bins: set[str] = resolve_safe_bins()
        self._skill_bins: set[str] = set()

    def start(self, agent_id: str | None = None) -> None:
        """Start the service and load configuration."""
        self._agent_id = agent_id or self._agent_id
        try:
            self._approval_config = resolve_approvals(self._agent_id)
        except Exception:
            logger.exception(
                "Failed to load approval config during startup - using defaults"
            )
            self._approval_config = ExecApprovalsResolved(
                path="",
                socket_path="",
                token="",
                defaults=ExecApprovalsDefaults(
                    security=ExecSecurity.DENY,
                    ask=ExecAsk.ON_MISS,
                    ask_fallback=ExecSecurity.DENY,
                    auto_allow_skills=False,
                ),
                agent=ExecApprovalsDefaults(
                    security=ExecSecurity.DENY,
                    ask=ExecAsk.ON_MISS,
                    ask_fallback=ExecSecurity.DENY,
                    auto_allow_skills=False,
                ),
                allowlist=[],
                file={"version": 1, "agents": {}},  # type: ignore[arg-type]
            )
        logger.info("ApprovalService started for agent %s", self._agent_id)

    def stop(self) -> None:
        """Stop the service."""
        logger.debug("ApprovalService stopped")

    def load_config(self, agent_id: str | None = None) -> ExecApprovalsResolved:
        """Load/reload configuration."""
        self._approval_config = resolve_approvals(agent_id or self._agent_id)
        return self._approval_config

    def get_config(self) -> ExecApprovalsResolved:
        """Get current configuration."""
        if not self._approval_config:
            self._approval_config = resolve_approvals(self._agent_id)
        return self._approval_config

    def set_safe_bins(self, bins: list[str]) -> None:
        """Set safe binaries that don't need approval."""
        self._safe_bins = resolve_safe_bins(bins)

    def set_skill_bins(self, bins: list[str]) -> None:
        """Set skill binaries that are auto-allowed."""
        self._skill_bins = {b.lower() for b in bins}

    def check_command(
        self,
        command: str,
        cwd: str | None = None,
        room_id: str = "",
        env: dict[str, str] | None = None,
        agent_id: str | None = None,
    ) -> CommandCheckResult:
        """Check if a command is allowed to execute."""
        config = self.get_config()

        # Analyze the command
        analysis = analyze_shell_command(command=command, cwd=cwd, env=env)

        if not analysis.ok:
            return CommandCheckResult(
                allowed=False,
                requires_approval=False,
                reason=analysis.reason or "Command analysis failed",
                analysis=analysis,
            )

        # Evaluate against allowlist
        evaluation = evaluate_shell_allowlist(
            command=command,
            allowlist=config.allowlist,
            safe_bins=self._safe_bins,
            cwd=cwd,
            env=env,
            skill_bins=self._skill_bins,
            auto_allow_skills=bool(config.agent.auto_allow_skills),
        )

        # Check security mode
        assert config.agent.security is not None
        assert config.agent.ask is not None
        security = config.agent.security
        ask = config.agent.ask

        # Full security mode - always allow
        if security == ExecSecurity.FULL:
            return CommandCheckResult(
                allowed=True,
                requires_approval=False,
                analysis=analysis,
                allowlist_matches=evaluation.allowlist_matches,
            )

        # Deny mode - always deny unless explicitly allowed
        if security == ExecSecurity.DENY:
            return CommandCheckResult(
                allowed=False,
                requires_approval=False,
                reason="Command execution is disabled",
                analysis=analysis,
            )

        # Allowlist mode - check if command is in allowlist
        if evaluation.allowlist_satisfied:
            # Record usage (best-effort)
            for match in evaluation.allowlist_matches:
                approvals = load_approvals()
                record_allowlist_use(
                    approvals,
                    agent_id or self._agent_id,
                    match,
                    command,
                    analysis.segments[0].resolution.resolved_path
                    if analysis.segments and analysis.segments[0].resolution
                    else None,
                )

            return CommandCheckResult(
                allowed=True,
                requires_approval=False,
                analysis=analysis,
                allowlist_matches=evaluation.allowlist_matches,
            )

        # Check if we need to ask for approval
        needs_approval = requires_exec_approval(
            ask=ask,
            security=security,
            analysis_ok=analysis.ok,
            allowlist_satisfied=evaluation.allowlist_satisfied,
        )

        if not needs_approval:
            assert config.agent.ask_fallback is not None
            fallback = config.agent.ask_fallback
            if fallback == ExecSecurity.DENY:
                return CommandCheckResult(
                    allowed=False,
                    requires_approval=False,
                    reason="Command not in allowlist",
                    analysis=analysis,
                )
            return CommandCheckResult(
                allowed=True,
                requires_approval=False,
                analysis=analysis,
            )

        # Build approval request
        request = ExecApprovalRequest(
            id=str(uuid.uuid4()),
            command=command,
            cwd=cwd,
            security=security,
            ask=ask,
            agent_id=agent_id or self._agent_id,
            resolved_path=(
                analysis.segments[0].resolution.resolved_path
                if analysis.segments and analysis.segments[0].resolution
                else None
            ),
            room_id=room_id,
            timeout_ms=EXEC_APPROVAL_TIMEOUT_MS,
        )

        return CommandCheckResult(
            allowed=False,
            requires_approval=True,
            request=request,
            analysis=analysis,
        )

    async def request_approval(
        self,
        request: ExecApprovalRequest,
    ) -> ApprovalResult:
        """Request approval for a command.

        In a full integration this would call the runtime's ApprovalService.
        Here we provide the protocol; the caller handles the UI.
        """
        logger.info(
            "Approval requested for command: %s (id=%s)", request.command, request.id
        )
        # Default implementation: deny (the integrating runtime provides the UI)
        return ApprovalResult(decision=ExecApprovalDecision.DENY, timed_out=False)

    async def add_to_allowlist(
        self, pattern: str, agent_id: str | None = None
    ) -> bool:
        """Add a pattern to the allowlist."""
        approvals = load_approvals()
        added = add_allowlist_entry(approvals, agent_id or self._agent_id, pattern)
        if added:
            self._approval_config = None  # Force reload
        return added

    async def handle_approval_response(
        self,
        request: ExecApprovalRequest,
        decision: ExecApprovalDecision,
    ) -> ApprovalResult:
        """Handle an approval response, including allow-always logic."""
        if decision == ExecApprovalDecision.ALLOW_ALWAYS and request.resolved_path:
            await self.add_to_allowlist(request.resolved_path, request.agent_id)

        return ApprovalResult(decision=decision, timed_out=False)
