"""
Human-in-the-Loop (HITL) Manager

Manages approval workflows for actions requiring human oversight.
"""

import asyncio
import logging
from collections.abc import Callable
from datetime import datetime, timedelta
from typing import Any
from uuid import uuid4

from ..config import get_safety_config
from ..exceptions import (
    ApprovalDeniedError,
    ApprovalRequiredError,
    ApprovalTimeoutError,
)
from ..models import (
    ActionRequest,
    ApprovalRequest,
    ApprovalResponse,
    ApprovalStatus,
)

logger = logging.getLogger(__name__)


class ApprovalQueue:
    """Queue for pending approval requests."""

    def __init__(self) -> None:
        self._pending: dict[str, ApprovalRequest] = {}
        self._completed: dict[str, ApprovalResponse] = {}
        self._waiters: dict[str, asyncio.Event] = {}
        self._lock = asyncio.Lock()

    async def add(self, request: ApprovalRequest) -> None:
        """Add an approval request to the queue."""
        async with self._lock:
            self._pending[request.id] = request
            self._waiters[request.id] = asyncio.Event()

    async def get_pending(self, request_id: str) -> ApprovalRequest | None:
        """Get a pending request by ID."""
        async with self._lock:
            return self._pending.get(request_id)

    async def complete(self, response: ApprovalResponse) -> bool:
        """Complete an approval request."""
        async with self._lock:
            if response.request_id not in self._pending:
                return False

            del self._pending[response.request_id]
            self._completed[response.request_id] = response

            # Notify waiters
            if response.request_id in self._waiters:
                self._waiters[response.request_id].set()

            return True

    async def wait_for_response(
        self,
        request_id: str,
        timeout_seconds: float,
    ) -> ApprovalResponse | None:
        """Wait for a response to an approval request."""
        async with self._lock:
            waiter = self._waiters.get(request_id)
            if not waiter:
                return self._completed.get(request_id)

        try:
            await asyncio.wait_for(waiter.wait(), timeout=timeout_seconds)
        except TimeoutError:
            return None

        async with self._lock:
            return self._completed.get(request_id)

    async def list_pending(self) -> list[ApprovalRequest]:
        """List all pending requests."""
        async with self._lock:
            return list(self._pending.values())

    async def cancel(self, request_id: str) -> bool:
        """Cancel a pending request."""
        async with self._lock:
            if request_id not in self._pending:
                return False

            del self._pending[request_id]

            # Create cancelled response
            response = ApprovalResponse(
                request_id=request_id,
                status=ApprovalStatus.CANCELLED,
                reason="Cancelled",
            )
            self._completed[request_id] = response

            # Notify waiters
            if request_id in self._waiters:
                self._waiters[request_id].set()

            return True

    async def cleanup_expired(self) -> int:
        """Clean up expired requests."""
        now = datetime.utcnow()
        to_timeout: list[str] = []

        async with self._lock:
            for request_id, request in self._pending.items():
                if request.expires_at and request.expires_at < now:
                    to_timeout.append(request_id)

        count = 0
        for request_id in to_timeout:
            async with self._lock:
                if request_id in self._pending:
                    del self._pending[request_id]
                    self._completed[request_id] = ApprovalResponse(
                        request_id=request_id,
                        status=ApprovalStatus.TIMEOUT,
                        reason="Request timed out",
                    )
                    if request_id in self._waiters:
                        self._waiters[request_id].set()
                    count += 1

        return count


class HITLManager:
    """
    Manages Human-in-the-Loop approval workflows.

    Features:
    - Approval request queue
    - Configurable timeout handling (default deny)
    - Approval delegation
    - Batch approval for similar actions
    - Approval with modifications
    - Notification channels
    """

    def __init__(
        self,
        default_timeout: int | None = None,
    ) -> None:
        """
        Initialize the HITLManager.

        Args:
            default_timeout: Default timeout for approval requests in seconds
        """
        config = get_safety_config()

        self._default_timeout = default_timeout or config.hitl_default_timeout
        self._queue = ApprovalQueue()
        self._notification_handlers: list[Callable[..., Any]] = []
        self._running = False
        self._cleanup_task: asyncio.Task[None] | None = None

    async def start(self) -> None:
        """Start the HITL manager background tasks."""
        if self._running:
            return

        self._running = True
        self._cleanup_task = asyncio.create_task(self._periodic_cleanup())
        logger.info("HITL Manager started")

    async def stop(self) -> None:
        """Stop the HITL manager."""
        self._running = False

        if self._cleanup_task:
            self._cleanup_task.cancel()
            try:
                await self._cleanup_task
            except asyncio.CancelledError:
                pass

        logger.info("HITL Manager stopped")

    async def request_approval(
        self,
        action: ActionRequest,
        reason: str,
        timeout_seconds: int | None = None,
        urgency: str = "normal",
        context: dict[str, Any] | None = None,
    ) -> ApprovalRequest:
        """
        Create an approval request for an action.

        Args:
            action: The action requiring approval
            reason: Why approval is required
            timeout_seconds: Timeout for this request
            urgency: Urgency level (low, normal, high, critical)
            context: Additional context for the approver

        Returns:
            The created approval request
        """
        timeout = timeout_seconds or self._default_timeout
        expires_at = datetime.utcnow() + timedelta(seconds=timeout)

        request = ApprovalRequest(
            id=str(uuid4()),
            action=action,
            reason=reason,
            urgency=urgency,
            timeout_seconds=timeout,
            expires_at=expires_at,
            context=context or {},
        )

        await self._queue.add(request)

        # Notify handlers
        await self._notify_handlers("approval_requested", request)

        logger.info(
            "Approval requested: %s for action %s (timeout: %ds)",
            request.id,
            action.id,
            timeout,
        )

        return request

    async def wait_for_approval(
        self,
        request_id: str,
        timeout_seconds: int | None = None,
    ) -> ApprovalResponse:
        """
        Wait for an approval decision.

        Args:
            request_id: ID of the approval request
            timeout_seconds: Override timeout

        Returns:
            The approval response

        Raises:
            ApprovalTimeoutError: If timeout expires
            ApprovalDeniedError: If approval is denied
        """
        request = await self._queue.get_pending(request_id)
        if not request:
            raise ApprovalRequiredError(
                f"Approval request not found: {request_id}",
                approval_id=request_id,
            )

        timeout = timeout_seconds or request.timeout_seconds or self._default_timeout
        response = await self._queue.wait_for_response(request_id, timeout)

        if response is None:
            # Timeout - default deny
            response = ApprovalResponse(
                request_id=request_id,
                status=ApprovalStatus.TIMEOUT,
                reason="Request timed out (default deny)",
            )
            await self._queue.complete(response)

            raise ApprovalTimeoutError(
                "Approval request timed out",
                approval_id=request_id,
                timeout_seconds=timeout,
            )

        if response.status == ApprovalStatus.DENIED:
            raise ApprovalDeniedError(
                response.reason or "Approval denied",
                approval_id=request_id,
                denied_by=response.decided_by,
                denial_reason=response.reason,
            )

        if response.status == ApprovalStatus.TIMEOUT:
            raise ApprovalTimeoutError(
                "Approval request timed out",
                approval_id=request_id,
                timeout_seconds=timeout,
            )

        if response.status == ApprovalStatus.CANCELLED:
            raise ApprovalDeniedError(
                "Approval request was cancelled",
                approval_id=request_id,
                denial_reason="Cancelled",
            )

        return response

    async def approve(
        self,
        request_id: str,
        decided_by: str,
        reason: str | None = None,
        modifications: dict[str, Any] | None = None,
    ) -> bool:
        """
        Approve a pending request.

        Args:
            request_id: ID of the approval request
            decided_by: Who approved
            reason: Optional approval reason
            modifications: Optional modifications to the action

        Returns:
            True if approval was recorded
        """
        response = ApprovalResponse(
            request_id=request_id,
            status=ApprovalStatus.APPROVED,
            decided_by=decided_by,
            reason=reason,
            modifications=modifications,
        )

        success = await self._queue.complete(response)

        if success:
            logger.info(
                "Approval granted: %s by %s",
                request_id,
                decided_by,
            )
            await self._notify_handlers("approval_granted", response)

        return success

    async def deny(
        self,
        request_id: str,
        decided_by: str,
        reason: str | None = None,
    ) -> bool:
        """
        Deny a pending request.

        Args:
            request_id: ID of the approval request
            decided_by: Who denied
            reason: Denial reason

        Returns:
            True if denial was recorded
        """
        response = ApprovalResponse(
            request_id=request_id,
            status=ApprovalStatus.DENIED,
            decided_by=decided_by,
            reason=reason,
        )

        success = await self._queue.complete(response)

        if success:
            logger.info(
                "Approval denied: %s by %s - %s",
                request_id,
                decided_by,
                reason,
            )
            await self._notify_handlers("approval_denied", response)

        return success

    async def cancel(self, request_id: str) -> bool:
        """
        Cancel a pending request.

        Args:
            request_id: ID of the approval request

        Returns:
            True if request was cancelled
        """
        success = await self._queue.cancel(request_id)

        if success:
            logger.info("Approval request cancelled: %s", request_id)

        return success

    async def list_pending(self) -> list[ApprovalRequest]:
        """List all pending approval requests."""
        return await self._queue.list_pending()

    async def get_request(self, request_id: str) -> ApprovalRequest | None:
        """Get an approval request by ID."""
        return await self._queue.get_pending(request_id)

    def add_notification_handler(
        self,
        handler: Callable[..., Any],
    ) -> None:
        """Add a notification handler."""
        self._notification_handlers.append(handler)

    def remove_notification_handler(
        self,
        handler: Callable[..., Any],
    ) -> None:
        """Remove a notification handler."""
        if handler in self._notification_handlers:
            self._notification_handlers.remove(handler)

    async def _notify_handlers(
        self,
        event_type: str,
        data: Any,
    ) -> None:
        """Notify all handlers of an event."""
        for handler in self._notification_handlers:
            try:
                if asyncio.iscoroutinefunction(handler):
                    await handler(event_type, data)
                else:
                    handler(event_type, data)
            except Exception as e:
                logger.error("Error in notification handler: %s", e)

    async def _periodic_cleanup(self) -> None:
        """Background task for cleaning up expired requests."""
        while self._running:
            try:
                await asyncio.sleep(30)  # Check every 30 seconds
                count = await self._queue.cleanup_expired()
                if count:
                    logger.debug("Cleaned up %d expired approval requests", count)
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error("Error in approval cleanup: %s", e)
