"""
Async Interrupt Handler - Webhook-based approvals with timeout.

This module provides V2 of the human interrupt interface:
- Non-blocking approval requests via webhooks
- Support for Slack, Discord, email notifications
- Polling-based response collection
- Configurable timeouts with auto-rejection

Usage:
    from agent_orchestrator.interrupt import AsyncInterruptHandler

    handler = AsyncInterruptHandler(db, webhook_config)
    decision = await handler.request_approval(
        agent_id="claude-code",
        action_type="command",
        target="git push origin main",
        risk_level="high",
    )
"""

import asyncio
import hashlib
import hmac
import json
import logging
import time
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Callable, Optional
from urllib.parse import urljoin

from ..persistence.database import OrchestratorDB
from ..persistence.models import Approval
from .cli_handler import ApprovalDecision, ApprovalResponse


logger = logging.getLogger(__name__)


class NotificationChannel(Enum):
    """Supported notification channels."""

    SLACK = "slack"
    DISCORD = "discord"
    EMAIL = "email"
    WEBHOOK = "webhook"  # Generic webhook
    CONSOLE = "console"  # Fallback to console logging


@dataclass
class WebhookConfig:
    """Configuration for webhook notifications."""

    # Primary notification channel
    channel: NotificationChannel = NotificationChannel.WEBHOOK

    # Webhook URL
    webhook_url: Optional[str] = None

    # Slack-specific
    slack_token: Optional[str] = None
    slack_channel: Optional[str] = None

    # Discord-specific
    discord_webhook_url: Optional[str] = None

    # Email-specific
    email_smtp_host: Optional[str] = None
    email_from: Optional[str] = None
    email_to: list[str] = field(default_factory=list)

    # Response callback URL (for approval responses)
    callback_url: Optional[str] = None

    # Secret for signing callbacks
    callback_secret: Optional[str] = None

    # Timeout settings
    timeout_seconds: int = 1800  # 30 minutes
    poll_interval_seconds: int = 5

    # Auto-reject on timeout
    timeout_decision: ApprovalDecision = ApprovalDecision.REJECTED


@dataclass
class PendingApproval:
    """A pending approval request."""

    approval_id: str
    agent_id: str
    action_type: str
    target: str
    risk_level: str
    created_at: datetime
    expires_at: datetime
    context: dict[str, Any] = field(default_factory=dict)
    notification_sent: bool = False
    response: Optional[ApprovalResponse] = None


class AsyncInterruptHandler:
    """
    Async handler for approval requests via webhooks.

    This is the V2 interrupt interface that sends notifications
    via external channels and polls for responses.
    """

    def __init__(
        self,
        db: OrchestratorDB,
        config: Optional[WebhookConfig] = None,
    ):
        """
        Initialize the async interrupt handler.

        Args:
            db: Database for recording decisions
            config: Webhook configuration
        """
        self.db = db
        self.config = config or WebhookConfig()

        # Pending approvals (in-memory cache)
        self._pending: dict[str, PendingApproval] = {}

        # Response queue for external callbacks
        self._response_queue: asyncio.Queue[tuple[str, ApprovalResponse]] = asyncio.Queue()

        # HTTP client (lazy loaded)
        self._http_client = None

    async def request_approval(
        self,
        agent_id: str,
        action_type: str,
        target: str,
        risk_level: str,
        context: Optional[dict[str, Any]] = None,
        diff: Optional[str] = None,
    ) -> ApprovalResponse:
        """
        Request approval via webhook notification.

        Args:
            agent_id: Agent requesting approval
            action_type: Type of action
            target: Target of the action
            risk_level: Risk level
            context: Additional context
            diff: Diff for file edits

        Returns:
            ApprovalResponse with decision
        """
        context = context or {}
        if diff:
            context["diff"] = diff

        # Create approval record
        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)

        # Create pending approval
        now = datetime.now()
        pending = PendingApproval(
            approval_id=approval_id,
            agent_id=agent_id,
            action_type=action_type,
            target=target,
            risk_level=risk_level,
            created_at=now,
            expires_at=now + timedelta(seconds=self.config.timeout_seconds),
            context=context,
        )
        self._pending[approval_id] = pending

        # Send notification
        await self._send_notification(pending)
        pending.notification_sent = True

        # Wait for response with polling
        response = await self._wait_for_response(approval_id)

        # Record decision
        self._record_decision(approval_id, response)

        # Cleanup
        self._pending.pop(approval_id, None)

        return response

    async def _send_notification(self, pending: PendingApproval) -> bool:
        """Send notification to configured channel."""
        try:
            if self.config.channel == NotificationChannel.SLACK:
                return await self._send_slack_notification(pending)
            elif self.config.channel == NotificationChannel.DISCORD:
                return await self._send_discord_notification(pending)
            elif self.config.channel == NotificationChannel.WEBHOOK:
                return await self._send_webhook_notification(pending)
            elif self.config.channel == NotificationChannel.CONSOLE:
                return self._send_console_notification(pending)
            else:
                logger.warning(f"Unknown channel: {self.config.channel}")
                return self._send_console_notification(pending)
        except Exception as e:
            logger.error(f"Failed to send notification: {e}")
            return False

    async def _send_slack_notification(self, pending: PendingApproval) -> bool:
        """Send Slack notification."""
        if not self.config.slack_token or not self.config.slack_channel:
            logger.error("Slack not configured")
            return False

        # Build Slack message blocks
        blocks = self._build_slack_blocks(pending)

        payload = {
            "channel": self.config.slack_channel,
            "blocks": blocks,
            "text": f"🔐 Approval Required: {pending.action_type} by {pending.agent_id}",
        }

        try:
            client = await self._get_http_client()
            response = await client.post(
                "https://slack.com/api/chat.postMessage",
                headers={
                    "Authorization": f"Bearer {self.config.slack_token}",
                    "Content-Type": "application/json",
                },
                json=payload,
            )
            return response.status == 200
        except Exception as e:
            logger.error(f"Slack notification failed: {e}")
            return False

    def _build_slack_blocks(self, pending: PendingApproval) -> list[dict]:
        """Build Slack Block Kit blocks."""
        risk_emoji = {
            "low": "🟢",
            "medium": "🟡",
            "high": "🔴",
            "critical": "⛔",
        }.get(pending.risk_level.lower(), "⚠️")

        blocks = [
            {
                "type": "header",
                "text": {
                    "type": "plain_text",
                    "text": "🔐 Approval Required",
                }
            },
            {
                "type": "section",
                "fields": [
                    {
                        "type": "mrkdwn",
                        "text": f"*Agent:*\n{pending.agent_id}",
                    },
                    {
                        "type": "mrkdwn",
                        "text": f"*Action:*\n{pending.action_type}",
                    },
                    {
                        "type": "mrkdwn",
                        "text": f"*Target:*\n`{pending.target}`",
                    },
                    {
                        "type": "mrkdwn",
                        "text": f"*Risk:*\n{risk_emoji} {pending.risk_level.upper()}",
                    },
                ]
            },
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"*Approval ID:* `{pending.approval_id}`",
                }
            },
        ]

        # Add diff if present
        if pending.context.get("diff"):
            diff_preview = pending.context["diff"][:500]
            if len(pending.context["diff"]) > 500:
                diff_preview += "\n... (truncated)"
            blocks.append({
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"*Changes:*\n```{diff_preview}```",
                }
            })

        # Add action buttons if callback URL configured
        if self.config.callback_url:
            blocks.append({
                "type": "actions",
                "elements": [
                    {
                        "type": "button",
                        "text": {"type": "plain_text", "text": "✅ Approve"},
                        "style": "primary",
                        "action_id": "approve",
                        "value": pending.approval_id,
                    },
                    {
                        "type": "button",
                        "text": {"type": "plain_text", "text": "❌ Reject"},
                        "style": "danger",
                        "action_id": "reject",
                        "value": pending.approval_id,
                    },
                ]
            })

        # Add expiry info
        blocks.append({
            "type": "context",
            "elements": [
                {
                    "type": "mrkdwn",
                    "text": f"⏱ Expires: {pending.expires_at.strftime('%Y-%m-%d %H:%M:%S')} (auto-rejects on timeout)",
                }
            ]
        })

        return blocks

    async def _send_discord_notification(self, pending: PendingApproval) -> bool:
        """Send Discord notification."""
        if not self.config.discord_webhook_url:
            logger.error("Discord webhook not configured")
            return False

        risk_emoji = {
            "low": "🟢",
            "medium": "🟡",
            "high": "🔴",
            "critical": "⛔",
        }.get(pending.risk_level.lower(), "⚠️")

        embed = {
            "title": "🔐 Approval Required",
            "color": 0xFF0000 if pending.risk_level.lower() == "high" else 0xFFFF00,
            "fields": [
                {"name": "Agent", "value": pending.agent_id, "inline": True},
                {"name": "Action", "value": pending.action_type, "inline": True},
                {"name": "Risk", "value": f"{risk_emoji} {pending.risk_level.upper()}", "inline": True},
                {"name": "Target", "value": f"`{pending.target}`", "inline": False},
                {"name": "Approval ID", "value": f"`{pending.approval_id}`", "inline": False},
            ],
            "footer": {
                "text": f"Expires: {pending.expires_at.strftime('%Y-%m-%d %H:%M:%S')}",
            },
        }

        payload = {
            "content": "🔐 **Approval Required**",
            "embeds": [embed],
        }

        try:
            client = await self._get_http_client()
            response = await client.post(
                self.config.discord_webhook_url,
                json=payload,
            )
            return response.status in (200, 204)
        except Exception as e:
            logger.error(f"Discord notification failed: {e}")
            return False

    async def _send_webhook_notification(self, pending: PendingApproval) -> bool:
        """Send generic webhook notification."""
        if not self.config.webhook_url:
            logger.error("Webhook URL not configured")
            return False

        payload = {
            "type": "approval_request",
            "approval_id": pending.approval_id,
            "agent_id": pending.agent_id,
            "action_type": pending.action_type,
            "target": pending.target,
            "risk_level": pending.risk_level,
            "context": pending.context,
            "created_at": pending.created_at.isoformat(),
            "expires_at": pending.expires_at.isoformat(),
            "callback_url": self.config.callback_url,
        }

        # Sign the payload if secret configured
        headers = {"Content-Type": "application/json"}
        if self.config.callback_secret:
            signature = self._sign_payload(json.dumps(payload))
            headers["X-Signature"] = signature

        try:
            client = await self._get_http_client()
            response = await client.post(
                self.config.webhook_url,
                json=payload,
                headers=headers,
            )
            return response.status in (200, 201, 202, 204)
        except Exception as e:
            logger.error(f"Webhook notification failed: {e}")
            return False

    def _send_console_notification(self, pending: PendingApproval) -> bool:
        """Fallback to console logging."""
        logger.warning(
            f"APPROVAL REQUIRED (no webhook configured):\n"
            f"  ID: {pending.approval_id}\n"
            f"  Agent: {pending.agent_id}\n"
            f"  Action: {pending.action_type}\n"
            f"  Target: {pending.target}\n"
            f"  Risk: {pending.risk_level}\n"
            f"  Expires: {pending.expires_at}"
        )
        return True

    async def _wait_for_response(self, approval_id: str) -> ApprovalResponse:
        """Wait for approval response with polling."""
        pending = self._pending.get(approval_id)
        if not pending:
            return ApprovalResponse.reject(
                reason="Approval not found",
                approval_id=approval_id,
            )

        while datetime.now() < pending.expires_at:
            # Check response queue
            try:
                queued_id, response = await asyncio.wait_for(
                    self._response_queue.get(),
                    timeout=self.config.poll_interval_seconds,
                )
                if queued_id == approval_id:
                    return response
                else:
                    # Put back if not ours
                    await self._response_queue.put((queued_id, response))
            except asyncio.TimeoutError:
                pass

            # Check database for external updates
            db_approval = self._check_database_status(approval_id)
            if db_approval:
                return db_approval

            # Check if already has response
            if pending.response:
                return pending.response

        # Timeout reached
        logger.warning(f"Approval {approval_id} timed out")
        return ApprovalResponse.timeout(approval_id=approval_id)

    def _check_database_status(self, approval_id: str) -> Optional[ApprovalResponse]:
        """Check database for approval status changes."""
        # This allows external systems to update the database directly
        approvals = self.db.get_pending_approvals()
        for approval in approvals:
            if approval.id == approval_id and approval.status != "pending":
                decision = {
                    "approved": ApprovalDecision.APPROVED,
                    "rejected": ApprovalDecision.REJECTED,
                    "skipped": ApprovalDecision.SKIPPED,
                    "timeout": ApprovalDecision.TIMEOUT,
                }.get(approval.status, ApprovalDecision.REJECTED)

                return ApprovalResponse(
                    decision=decision,
                    approved=decision == ApprovalDecision.APPROVED,
                    reason=approval.decision_notes or "",
                    decided_by=approval.decided_by or "unknown",
                    approval_id=approval_id,
                )
        return None

    def _record_decision(
        self,
        approval_id: str,
        response: ApprovalResponse,
    ) -> None:
        """Record the approval decision."""
        status_map = {
            ApprovalDecision.APPROVED: "approved",
            ApprovalDecision.REJECTED: "rejected",
            ApprovalDecision.SKIPPED: "skipped",
            ApprovalDecision.TIMEOUT: "timeout",
        }

        self.db.update_approval(
            approval_id=approval_id,
            status=status_map.get(response.decision, "rejected"),
            decided_by=response.decided_by,
            decision_notes=response.reason,
        )

        logger.info(
            f"Approval {approval_id}: {response.decision.value} by {response.decided_by}"
        )

    def _sign_payload(self, payload: str) -> str:
        """Sign payload with HMAC-SHA256."""
        if not self.config.callback_secret:
            return ""
        signature = hmac.new(
            self.config.callback_secret.encode(),
            payload.encode(),
            hashlib.sha256,
        ).hexdigest()
        return f"sha256={signature}"

    def verify_callback_signature(self, payload: str, signature: str) -> bool:
        """Verify callback signature."""
        if not self.config.callback_secret:
            return True  # No secret configured, accept all
        expected = self._sign_payload(payload)
        return hmac.compare_digest(expected, signature)

    async def handle_callback(
        self,
        approval_id: str,
        decision: str,
        decided_by: str = "webhook",
        notes: Optional[str] = None,
    ) -> bool:
        """
        Handle approval callback from external system.

        Args:
            approval_id: The approval ID
            decision: "approve" or "reject"
            decided_by: Who made the decision
            notes: Optional notes

        Returns:
            True if callback was processed
        """
        if approval_id not in self._pending:
            logger.warning(f"Callback for unknown approval: {approval_id}")
            return False

        if decision.lower() in ("approve", "approved", "yes", "y"):
            response = ApprovalResponse.approve(
                reason=notes or "Approved via callback",
                decided_by=decided_by,
                approval_id=approval_id,
            )
        else:
            response = ApprovalResponse.reject(
                reason=notes or "Rejected via callback",
                decided_by=decided_by,
                approval_id=approval_id,
            )

        await self._response_queue.put((approval_id, response))
        return True

    async def _get_http_client(self):
        """Get or create HTTP client."""
        if self._http_client is None:
            try:
                import httpx
                self._http_client = httpx.AsyncClient(timeout=30.0)
            except ImportError:
                logger.error("httpx not installed, webhook notifications disabled")
                raise
        return self._http_client

    async def close(self) -> None:
        """Close HTTP client."""
        if self._http_client:
            await self._http_client.aclose()
            self._http_client = None

    # =========================================================================
    # Convenience methods
    # =========================================================================

    async def approve_command(
        self,
        agent_id: str,
        command: str,
        risk_level: str = "medium",
    ) -> ApprovalResponse:
        """Request approval for a command."""
        return await self.request_approval(
            agent_id=agent_id,
            action_type="command",
            target=command,
            risk_level=risk_level,
        )

    async def approve_file_edit(
        self,
        agent_id: str,
        file_path: str,
        diff: str,
        risk_level: str = "medium",
    ) -> ApprovalResponse:
        """Request approval for a file edit."""
        return await self.request_approval(
            agent_id=agent_id,
            action_type="file_edit",
            target=file_path,
            risk_level=risk_level,
            diff=diff,
        )


class ApprovalCallbackServer:
    """
    Simple callback server for receiving approval responses.

    Can be integrated with existing web frameworks or run standalone.
    """

    def __init__(
        self,
        handler: AsyncInterruptHandler,
        host: str = "0.0.0.0",
        port: int = 8080,
    ):
        """Initialize callback server."""
        self.handler = handler
        self.host = host
        self.port = port

    async def handle_request(self, request_data: dict) -> dict:
        """
        Handle incoming approval callback.

        Expected format:
        {
            "approval_id": "...",
            "decision": "approve" | "reject",
            "decided_by": "user@example.com",
            "notes": "optional notes"
        }
        """
        approval_id = request_data.get("approval_id")
        decision = request_data.get("decision")
        decided_by = request_data.get("decided_by", "webhook")
        notes = request_data.get("notes")

        if not approval_id or not decision:
            return {"error": "Missing approval_id or decision"}

        success = await self.handler.handle_callback(
            approval_id=approval_id,
            decision=decision,
            decided_by=decided_by,
            notes=notes,
        )

        if success:
            return {"status": "ok", "approval_id": approval_id}
        else:
            return {"error": "Approval not found or already processed"}
