"""Transaction approval tool for irreversible actions.

Agents must request approval before performing irreversible actions like:
- Posting content (social media, forums)
- Sending messages/emails
- Making purchases or payments
- Deleting data
- Transferring files/money
- Changing account settings
"""

from __future__ import annotations

import asyncio
import json
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Awaitable, Callable

from loguru import logger

from madpod_agent.agent.tools.base import Tool
from madpod_agent.bus.events import OutboundMessage


class ApprovalCategory(str, Enum):
    """Categories of actions that may require user approval."""

    POST_CONTENT = "post_content"        # Social media posts, comments, forum posts
    SEND_MESSAGE = "send_message"        # DMs, emails, chat messages
    PURCHASE = "purchase"                # Buy, order, subscribe, pay
    DELETE = "delete"                    # Delete accounts, posts, data
    TRANSFER = "transfer"               # File transfer, money transfer
    ACCOUNT_CHANGE = "account_change"    # Settings, profile, password changes
    FORM_SUBMIT = "form_submit"          # Non-trivial form submissions
    OTHER = "other"


# Legacy value -> new value mapping for backward compatibility
_LEGACY_CATEGORY_MAP: dict[str, str] = {
    "post": ApprovalCategory.POST_CONTENT.value,
    "send": ApprovalCategory.SEND_MESSAGE.value,
    "submit": ApprovalCategory.FORM_SUBMIT.value,
}


def normalize_category(raw: str) -> str:
    """Normalize a category string, supporting legacy values."""
    valid = {c.value for c in ApprovalCategory}
    if raw in valid:
        return raw
    mapped = _LEGACY_CATEGORY_MAP.get(raw)
    if mapped is not None:
        return mapped
    return ApprovalCategory.OTHER.value


@dataclass(frozen=True)
class ApprovalRequest:
    """A structured request for user approval."""

    category: ApprovalCategory
    action_description: str
    details: dict[str, Any] = field(default_factory=dict)
    reversible: bool = False
    estimated_impact: str = ""


@dataclass(frozen=True)
class ApprovalPolicy:
    """Policy controlling which actions need approval."""

    require_approval_categories: frozenset[ApprovalCategory] = frozenset(
        ApprovalCategory,
    )
    auto_approve_categories: frozenset[ApprovalCategory] = frozenset()

    def needs_approval(self, category: ApprovalCategory) -> bool:
        """Return True if the given category requires user approval."""
        if category in self.auto_approve_categories:
            return False
        return category in self.require_approval_categories


@dataclass(frozen=True)
class ApprovalResult:
    """Immutable result of an approval request."""

    approved: bool
    modified_action: str | None = None
    approver: str = ""
    timestamp: str = ""
    request_id: str = ""


# ── Action verb detection ────────────────────────────────────────────────

_ACTION_CATEGORY_MAP: dict[str, ApprovalCategory] = {
    "post": ApprovalCategory.POST_CONTENT,
    "publish": ApprovalCategory.POST_CONTENT,
    "comment": ApprovalCategory.POST_CONTENT,
    "reply": ApprovalCategory.POST_CONTENT,
    "tweet": ApprovalCategory.POST_CONTENT,
    "share": ApprovalCategory.POST_CONTENT,
    "send": ApprovalCategory.SEND_MESSAGE,
    "email": ApprovalCategory.SEND_MESSAGE,
    "dm": ApprovalCategory.SEND_MESSAGE,
    "message": ApprovalCategory.SEND_MESSAGE,
    "buy": ApprovalCategory.PURCHASE,
    "purchase": ApprovalCategory.PURCHASE,
    "order": ApprovalCategory.PURCHASE,
    "subscribe": ApprovalCategory.PURCHASE,
    "pay": ApprovalCategory.PURCHASE,
    "checkout": ApprovalCategory.PURCHASE,
    "delete": ApprovalCategory.DELETE,
    "remove": ApprovalCategory.DELETE,
    "cancel": ApprovalCategory.DELETE,
    "transfer": ApprovalCategory.TRANSFER,
    "upload": ApprovalCategory.TRANSFER,
    "submit": ApprovalCategory.FORM_SUBMIT,
}


def detect_approval_category(action_text: str) -> ApprovalCategory | None:
    """Detect if an action requires approval based on its description.

    Returns the matching ApprovalCategory or None if no irreversible
    action verb is detected.
    """
    lower = action_text.lower()
    for verb, category in _ACTION_CATEGORY_MAP.items():
        if verb in lower:
            return category
    return None


def format_approval_prompt(request: ApprovalRequest) -> str:
    """Format an approval request for display to the user."""
    parts = [
        f"APPROVAL REQUIRED: {request.category.value}",
        f"Action: {request.action_description}",
    ]
    if request.details:
        parts.append("Details:")
        for key, value in request.details.items():
            parts.append(f"  {key}: {value}")
    if request.estimated_impact:
        parts.append(f"Impact: {request.estimated_impact}")
    if not request.reversible:
        parts.append("WARNING: This action is IRREVERSIBLE")
    parts.append("\nRespond with 'approve' or 'deny'")
    return "\n".join(parts)


# ── Constants ────────────────────────────────────────────────────────────

_DEFAULT_TIMEOUT = 600.0   # 10 minutes
_MAX_TIMEOUT = 3600.0      # 1 hour


# ── Tool ─────────────────────────────────────────────────────────────────


class RequestApprovalTool(Tool):
    """Request user approval before executing an irreversible action.

    Sends a structured approval request via the control plane and blocks
    until the user approves, rejects, or a timeout is reached.
    """

    def __init__(
        self,
        send_callback: Callable[[OutboundMessage], Awaitable[None]] | None = None,
        response_future_factory: Callable[[str], asyncio.Future[str]] | None = None,
        default_channel: str = "",
        default_chat_id: str = "",
        policy: ApprovalPolicy | None = None,
    ):
        self._send_callback = send_callback
        self._response_future_factory = response_future_factory
        self._default_channel = default_channel
        self._default_chat_id = default_chat_id
        self._policy = policy or ApprovalPolicy()

    @property
    def policy(self) -> ApprovalPolicy:
        return self._policy

    def set_send_callback(
        self, callback: Callable[[OutboundMessage], Awaitable[None]]
    ) -> None:
        self._send_callback = callback

    def set_response_future_factory(
        self, factory: Callable[[str], asyncio.Future[str]]
    ) -> None:
        self._response_future_factory = factory

    def set_context(self, channel: str, chat_id: str) -> None:
        self._default_channel = channel
        self._default_chat_id = chat_id

    @property
    def name(self) -> str:
        return "request_approval"

    @property
    def description(self) -> str:
        return (
            "Request user approval before executing an irreversible action. "
            "Use this BEFORE posting on social media, making purchases, sending "
            "emails/messages, submitting forms, transferring files/money, "
            "or any action that cannot be undone. "
            "Provide a clear description of what you are about to do and why. "
            "The user can approve, reject, or modify the action."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "action_description": {
                    "type": "string",
                    "description": (
                        "Clear description of the irreversible action "
                        "you want to perform"
                    ),
                },
                "category": {
                    "type": "string",
                    "enum": [c.value for c in ApprovalCategory],
                    "description": (
                        "Category of action "
                        "(post_content, send_message, purchase, delete, "
                        "transfer, account_change, form_submit, other)"
                    ),
                },
                "details": {
                    "type": "object",
                    "description": (
                        "Structured details about the action "
                        '(e.g., {platform: "LinkedIn", content: "...", '
                        'target: "..."})'
                    ),
                },
                "timeout": {
                    "type": "number",
                    "description": (
                        "Seconds to wait for approval "
                        "(default: 600, max: 3600)"
                    ),
                },
            },
            "required": ["action_description", "category"],
        }

    async def execute(
        self,
        action_description: str,
        category: str = "other",
        details: dict[str, Any] | None = None,
        timeout: float | None = None,
        **kwargs: Any,
    ) -> str:
        if not self._send_callback:
            return json.dumps({
                "error": "Message sending not configured — cannot request approval",
            })

        channel = self._default_channel
        chat_id = self._default_chat_id
        if not channel or not chat_id:
            return json.dumps({
                "error": "No target channel/chat configured",
            })

        # Normalize category (supports legacy values)
        safe_category = normalize_category(category)

        # Check policy — auto-approved categories skip the request
        try:
            cat_enum = ApprovalCategory(safe_category)
        except ValueError:
            cat_enum = ApprovalCategory.OTHER
            safe_category = ApprovalCategory.OTHER.value

        if not self._policy.needs_approval(cat_enum):
            logger.info(
                "Auto-approved [{}]: {} (policy skip)",
                safe_category,
                action_description[:100],
            )
            return json.dumps({
                "ok": True,
                "approved": True,
                "auto_approved": True,
                "category": safe_category,
                "message": "Action auto-approved by policy.",
            })

        request_id = uuid.uuid4().hex[:12]
        effective_timeout = min(
            timeout if timeout is not None else _DEFAULT_TIMEOUT,
            _MAX_TIMEOUT,
        )

        approval_payload = {
            "request_id": request_id,
            "action_description": action_description,
            "category": safe_category,
            "details": details or {},
            "timeout_seconds": effective_timeout,
            "requested_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        }

        metadata: dict[str, Any] = {
            "message_id": uuid.uuid4().hex,
            "message_kind": "approval_request",
            "approval_request_id": request_id,
            "approval_category": safe_category,
            "requires_response": True,
        }

        msg = OutboundMessage(
            channel=channel,
            chat_id=chat_id,
            content=json.dumps(approval_payload, ensure_ascii=False),
            media=[],
            metadata=metadata,
        )

        try:
            await self._send_callback(msg)
            logger.info(
                "Approval requested [{}]: {} (timeout={}s)",
                request_id,
                action_description[:100],
                effective_timeout,
            )
        except Exception as exc:
            return json.dumps({
                "error": f"Failed to send approval request: {exc}",
            })

        # Wait for response if a future factory is provided
        if self._response_future_factory is not None:
            return await self._await_response(
                request_id, effective_timeout,
            )

        # Fire-and-forget mode (no response factory)
        return json.dumps({
            "ok": True,
            "fire_and_forget": True,
            "request_id": request_id,
            "message": (
                "Approval request sent. "
                "Proceeding without waiting for response."
            ),
        })

    async def _await_response(
        self,
        request_id: str,
        timeout: float,
    ) -> str:
        """Block until the user responds or the timeout expires."""
        assert self._response_future_factory is not None

        try:
            future = self._response_future_factory(request_id)
            raw_response = await asyncio.wait_for(future, timeout=timeout)
            response_data = (
                json.loads(raw_response)
                if isinstance(raw_response, str)
                else raw_response
            )

            approved = bool(response_data.get("approved", False))
            result = ApprovalResult(
                approved=approved,
                modified_action=response_data.get("modified_action"),
                approver=response_data.get("approver", "user"),
                timestamp=time.strftime(
                    "%Y-%m-%dT%H:%M:%SZ", time.gmtime(),
                ),
                request_id=request_id,
            )

            logger.info(
                "Approval response [{}]: approved={}, modified={}",
                request_id,
                approved,
                bool(result.modified_action),
            )

            return json.dumps({
                "ok": True,
                "approved": result.approved,
                "modified_action": result.modified_action,
                "approver": result.approver,
                "request_id": request_id,
            }, ensure_ascii=False)

        except asyncio.TimeoutError:
            logger.warning(
                "Approval timeout [{}] after {}s", request_id, timeout,
            )
            return json.dumps({
                "ok": False,
                "error": "approval_timeout",
                "request_id": request_id,
                "timeout_seconds": timeout,
                "message": (
                    "User did not respond within the timeout period. "
                    "Do NOT proceed with the action."
                ),
            })
        except Exception as exc:
            return json.dumps({
                "error": f"Failed waiting for approval response: {exc}",
            })
