# Copyright (c) 2024, CoSig Contributors
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Cloud API client for CoSig.

This module provides the client for communicating with the CoSig cloud service
(or a self-hosted instance). It handles approval requests, status polling,
and execution confirmation.

API Endpoints (aligned with cosig-cloud):
    POST /api/v1/plans              - Create approval request
    GET  /api/v1/plans/{hash}       - Get plan details
    GET  /api/v1/plans/{hash}/status - Check approval status
    POST /api/v1/plans/{hash}/execute - Mark plan as executed
"""

import asyncio
from typing import Any

import httpx

from cosig.config import get_config


class CloudClientError(Exception):
    """Error from the CoSig cloud API."""

    def __init__(self, message: str, status_code: int | None = None):
        super().__init__(message)
        self.status_code = status_code


class ApprovalPendingError(Exception):
    """Raised when a tool call requires approval.

    This error contains the information needed to approve the call
    via the CoSig web interface.
    """

    def __init__(
        self,
        plan_hash: str,
        approval_url: str,
        message: str = "Tool call requires approval",
    ):
        super().__init__(message)
        self.plan_hash = plan_hash
        self.approval_url = approval_url


class CoSigCloudClient:
    """Client for CoSig cloud API.

    This client handles communication with the CoSig hosted service
    (or self-hosted instance) for approval requests and execution.
    """

    def __init__(self) -> None:
        """Initialize the cloud client."""
        pass

    @property
    def _config(self) -> Any:
        """Get the current configuration."""
        return get_config()

    @property
    def _headers(self) -> dict[str, str]:
        """Get headers for API requests."""
        headers = {
            "Content-Type": "application/json",
        }
        if self._config.api_key:
            headers["Authorization"] = f"Bearer {self._config.api_key}"
        return headers

    def _build_url(self, path: str) -> str:
        """Build full API URL from path.

        Args:
            path: API path (e.g., "/plans" or "/plans/{hash}/status")

        Returns:
            Full URL
        """
        base = self._config.api_url.rstrip("/")
        # Ensure we have /api/v1 prefix
        if not base.endswith("/api/v1"):
            if "/api" not in base:
                base = f"{base}/api/v1"
        return f"{base}{path}"

    def _get_verify_ssl(self) -> bool:
        """Determine whether to verify SSL certificates.

        Returns:
            True to verify SSL (production), False to skip (localhost with allow_insecure)
        """
        config = self._config
        api_url = config.api_url.lower()

        # Allow insecure connections for localhost/127.0.0.1 if explicitly allowed
        is_localhost = "://localhost" in api_url or "://127.0.0.1" in api_url
        if is_localhost and config.allow_insecure:
            return False

        # Always verify SSL for remote hosts
        return True

    async def create_approval_request(
        self,
        plan_hash: str,
        tool_name: str,
        args: list[Any],
        kwargs: dict[str, Any],
        username: str | None,
    ) -> dict[str, Any]:
        """Create an approval request for a tool call.

        This creates a pending approval request in the cloud service.
        The user must approve via the web interface with their hardware token.

        Args:
            plan_hash: Hash of the tool call (computed by fastmcp_integration)
            tool_name: Name of the tool to execute
            args: Positional arguments for the tool
            kwargs: Keyword arguments for the tool
            username: Email address of the user who should approve this request.
                     If not provided, uses the API key's associated user.

        Returns:
            Dict with plan details including plan_hash and status

        Raises:
            CloudClientError: If the API request fails
        """
        # Combine args and kwargs into a single arguments dict
        # Positional args are stored with indexed keys
        arguments = dict(kwargs)
        for i, arg in enumerate(args):
            arguments[f"_arg{i}"] = arg

        payload = {
            "tool_name": tool_name,
            "arguments": arguments,
            "webhook_url": self._config.webhook_url,
        }

        # Add user_email if username is provided (for multi-tenant support)
        # The username should be the user's email address
        if username:
            payload["user_email"] = username

        async with httpx.AsyncClient(
            verify=self._get_verify_ssl(), follow_redirects=True
        ) as client:
            response = await client.post(
                self._build_url("/plans/"),
                headers=self._headers,
                json=payload,
                timeout=30.0,
            )

            if response.status_code not in (200, 201):
                raise CloudClientError(
                    f"Failed to create approval request: {response.text}",
                    status_code=response.status_code,
                )

            response_data: dict[str, Any] = response.json()
            return response_data

    async def check_approval_status(self, plan_hash: str) -> dict[str, Any]:
        """Check the status of an approval request.

        Args:
            plan_hash: Hash of the plan to check

        Returns:
            Dict with status ("pending", "approved", "executed", "rejected")
            and other details

        Raises:
            CloudClientError: If the API request fails (404 if not found)
        """
        async with httpx.AsyncClient(
            verify=self._get_verify_ssl(), follow_redirects=True
        ) as client:
            response = await client.get(
                self._build_url(f"/plans/{plan_hash}/status"),
                headers=self._headers,
                timeout=30.0,
            )

            if response.status_code == 404:
                raise CloudClientError(
                    f"Plan not found: {plan_hash}",
                    status_code=404,
                )

            if response.status_code != 200:
                raise CloudClientError(
                    f"Failed to check status: {response.text}",
                    status_code=response.status_code,
                )

            response_data: dict[str, Any] = response.json()
            return response_data

    async def get_plan(self, plan_hash: str) -> dict[str, Any]:
        """Get full details of a plan.

        Args:
            plan_hash: Hash of the plan

        Returns:
            Dict with full plan details

        Raises:
            CloudClientError: If the API request fails
        """
        async with httpx.AsyncClient(
            verify=self._get_verify_ssl(), follow_redirects=True
        ) as client:
            response = await client.get(
                self._build_url(f"/plans/{plan_hash}"),
                headers=self._headers,
                timeout=30.0,
            )

            if response.status_code == 404:
                raise CloudClientError(
                    f"Plan not found: {plan_hash}",
                    status_code=404,
                )

            if response.status_code != 200:
                raise CloudClientError(
                    f"Failed to get plan: {response.text}",
                    status_code=response.status_code,
                )

            response_data: dict[str, Any] = response.json()
            return response_data

    async def mark_executed(
        self,
        plan_hash: str,
        result: str | None = None,
    ) -> dict[str, Any]:
        """Mark a plan as executed after successful tool execution.

        Args:
            plan_hash: Hash of the plan
            result: Optional result string from the tool execution

        Returns:
            Updated plan details

        Raises:
            CloudClientError: If the API request fails
        """
        payload = {}
        if result is not None:
            payload["result"] = result

        async with httpx.AsyncClient(
            verify=self._get_verify_ssl(), follow_redirects=True
        ) as client:
            response = await client.post(
                self._build_url(f"/plans/{plan_hash}/execute"),
                headers=self._headers,
                json=payload,
                timeout=30.0,
            )

            if response.status_code != 200:
                raise CloudClientError(
                    f"Failed to mark executed: {response.text}",
                    status_code=response.status_code,
                )

            response_data: dict[str, Any] = response.json()
            return response_data

    async def wait_for_approval(
        self,
        plan_hash: str,
        timeout: float = 300.0,
        poll_interval: float = 2.0,
    ) -> dict[str, Any]:
        """Wait for an approval request to be approved.

        Polls the cloud API until the request is approved or timeout.

        Args:
            plan_hash: Hash of the plan to wait for
            timeout: Maximum seconds to wait
            poll_interval: Seconds between status checks

        Returns:
            Dict with approval status

        Raises:
            TimeoutError: If approval not received within timeout
            CloudClientError: If the API request fails
        """
        elapsed = 0.0

        while elapsed < timeout:
            status = await self.check_approval_status(plan_hash)

            if status.get("status") == "approved":
                return status
            elif status.get("status") == "rejected":
                raise CloudClientError("Approval request was rejected")
            elif status.get("status") == "executed":
                return status

            await asyncio.sleep(poll_interval)
            elapsed += poll_interval

        raise TimeoutError(f"Approval timeout after {timeout} seconds")


# Singleton instance
_cloud_client: CoSigCloudClient | None = None


def get_cloud_client() -> CoSigCloudClient:
    """Get the cloud client singleton.

    Returns:
        The cloud client instance
    """
    global _cloud_client
    if _cloud_client is None:
        _cloud_client = CoSigCloudClient()
    return _cloud_client


def reset_cloud_client() -> None:
    """Reset the cloud client singleton.

    Useful for testing.
    """
    global _cloud_client
    _cloud_client = None
