import uuid
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union

import httpx

import litellm
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.core_helpers import process_response_headers
from litellm.litellm_core_utils.url_utils import encode_url_path_segment
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
    _safe_convert_created_field,
)
from litellm.llms.openai.common_utils import OpenAIError
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import (
    ResponseAPIUsage,
    ResponseInputParam,
    ResponsesAPIResponse,
)
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import LlmProviders

if TYPE_CHECKING:
    from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj

    LiteLLMLoggingObj = _LiteLLMLoggingObj
else:
    LiteLLMLoggingObj = Any

MANUS_API_BASE = "https://api.manus.im"


class ManusResponsesAPIConfig(OpenAIResponsesAPIConfig):
    """
    Configuration for Manus API's Responses API.

    Manus API is OpenAI-compatible but has some differences:
    - API key passed via `API_KEY` header (not `Authorization: Bearer`)
    - Model format: `manus/{agent_profile}` (e.g., `manus/manus-1.6`)
    - Requires `extra_body` with `task_mode: "agent"` and `agent_profile`

    Reference: https://open.manus.im/docs/openai-compatibility
    """

    @property
    def custom_llm_provider(self) -> LlmProviders:
        return LlmProviders.MANUS

    def should_fake_stream(
        self,
        model: Optional[str],
        stream: Optional[bool],
        custom_llm_provider: Optional[str] = None,
    ) -> bool:
        """
        Manus API doesn't support real-time streaming.
        It returns a task that runs asynchronously.
        We fake streaming by converting the response into streaming events.
        """
        return stream is True

    def _extract_agent_profile(self, model: str) -> str:
        """
        Extract agent profile from model name.

        Model format: `manus/{agent_profile}`
        Examples: `manus/manus-1.6`, `manus/manus-1.6-lite`, `manus/manus-1.6-max`

        Returns:
            str: The agent profile (e.g., "manus-1.6")
        """
        if "/" in model:
            return model.split("/", 1)[1]
        # If no slash, assume the model name itself is the agent profile
        return model

    def validate_environment(
        self, headers: dict, model: str, litellm_params: Optional[GenericLiteLLMParams]
    ) -> dict:
        """
        Validate environment and set up headers for Manus API.

        Manus uses `API_KEY` header instead of `Authorization: Bearer`.
        """
        litellm_params = litellm_params or GenericLiteLLMParams()
        api_key = (
            litellm_params.api_key or litellm.api_key or get_secret_str("MANUS_API_KEY")
        )

        if not api_key:
            raise ValueError(
                "Manus API key is required. Set MANUS_API_KEY environment variable or pass api_key parameter."
            )

        # Manus uses API_KEY header, not Authorization: Bearer
        # Content-Type is required for all requests (including GET)
        headers.update(
            {
                "API_KEY": api_key,
                "Content-Type": "application/json",
            }
        )
        return headers

    def get_complete_url(
        self,
        api_base: Optional[str],
        litellm_params: dict,
    ) -> str:
        """
        Get the complete URL for Manus Responses API endpoint.

        Returns:
            str: The full URL for the Manus /v1/responses endpoint
        """
        api_base = (
            api_base
            or litellm.api_base
            or get_secret_str("MANUS_API_BASE")
            or MANUS_API_BASE
        )

        # Remove trailing slashes
        api_base = api_base.rstrip("/")

        # Manus API uses /v1/responses endpoint (OpenAI-compatible)
        if api_base.endswith("/v1"):
            return f"{api_base}/responses"
        return f"{api_base}/v1/responses"

    def transform_responses_api_request(
        self,
        model: str,
        input: Union[str, ResponseInputParam],
        response_api_optional_request_params: Dict,
        litellm_params: GenericLiteLLMParams,
        headers: dict,
    ) -> Dict:
        """
        Transform the request for Manus API.

        Manus requires:
        - `task_mode: "agent"` in the request body
        - `agent_profile` extracted from model name in the request body
        """
        # First, get the base OpenAI request
        base_request = super().transform_responses_api_request(
            model=model,
            input=input,
            response_api_optional_request_params=response_api_optional_request_params,
            litellm_params=litellm_params,
            headers=headers,
        )

        # Extract agent profile from model name
        agent_profile = self._extract_agent_profile(model=model)

        # Add Manus-specific parameters directly to the request body
        # These will be sent as part of the request
        base_request["task_mode"] = "agent"
        base_request["agent_profile"] = agent_profile

        # Merge any existing extra_body into the request
        extra_body = response_api_optional_request_params.get("extra_body", {}) or {}
        if extra_body:
            base_request.update(extra_body)

        verbose_logger.debug(
            f"Manus: Using agent_profile={agent_profile}, task_mode=agent"
        )

        return base_request

    def transform_response_api_response(
        self,
        model: str,
        raw_response: httpx.Response,
        logging_obj: LiteLLMLoggingObj,
    ) -> ResponsesAPIResponse:
        """
        Transform Manus API response to OpenAI-compatible format.

        Manus uses camelCase (createdAt) instead of snake_case (created_at).
        """
        try:
            logging_obj.post_call(
                original_response=raw_response.text,
                additional_args={"complete_input_dict": {}},
            )
            raw_response_json = raw_response.json()

            # Manus uses camelCase "createdAt" instead of snake_case "created_at"
            if (
                "createdAt" in raw_response_json
                and "created_at" not in raw_response_json
            ):
                raw_response_json["created_at"] = _safe_convert_created_field(
                    raw_response_json["createdAt"]
                )

            # Ensure created_at is set
            if "created_at" in raw_response_json:
                raw_response_json["created_at"] = _safe_convert_created_field(
                    raw_response_json["created_at"]
                )
        except Exception:
            raise OpenAIError(
                message=raw_response.text, status_code=raw_response.status_code
            )

        raw_response_headers = dict(raw_response.headers)
        processed_headers = process_response_headers(raw_response_headers)

        # Ensure reasoning is an empty dict if not present, OpenAI SDK does not allow None
        if (
            "reasoning" not in raw_response_json
            or raw_response_json.get("reasoning") is None
        ):
            raw_response_json["reasoning"] = {}

        if "text" not in raw_response_json or raw_response_json.get("text") is None:
            raw_response_json["text"] = {}

        if "output" not in raw_response_json or raw_response_json.get("output") is None:
            raw_response_json["output"] = []

        # Ensure usage is present with default values if not provided
        if "usage" not in raw_response_json or raw_response_json.get("usage") is None:
            raw_response_json["usage"] = ResponseAPIUsage(
                input_tokens=0,
                output_tokens=0,
                total_tokens=0,
            )

        # Ensure id is present - failed responses may not include it
        if "id" not in raw_response_json or raw_response_json.get("id") is None:
            # Generate a placeholder id for failed responses
            # This allows the response object to be created even when the API doesn't return an id
            raw_response_json["id"] = f"unknown-{uuid.uuid4().hex[:8]}"

        try:
            response = ResponsesAPIResponse(**raw_response_json)
        except Exception:
            verbose_logger.debug(
                f"Error constructing ResponsesAPIResponse: {raw_response_json}, using model_construct"
            )
            response = ResponsesAPIResponse.model_construct(**raw_response_json)

        # Store processed headers in additional_headers so they get returned to the client
        response._hidden_params["additional_headers"] = processed_headers
        response._hidden_params["headers"] = raw_response_headers
        return response

    def supports_native_websocket(self) -> bool:
        """Manus does not support native WebSocket for Responses API"""
        return False

    def transform_get_response_api_request(
        self,
        response_id: str,
        api_base: str,
        litellm_params: GenericLiteLLMParams,
        headers: dict,
    ) -> Tuple[str, Dict]:
        """
        Transform the get response API request into a URL and data.

        Manus API follows OpenAI-compatible format:
        - GET /v1/responses/{response_id}

        Reference: https://open.manus.im/docs/openai-compatibility
        """
        encoded_response_id = encode_url_path_segment(
            response_id, field_name="response_id"
        )
        url = f"{api_base}/{encoded_response_id}"
        data: Dict = {}
        return url, data

    def transform_get_response_api_response(
        self,
        raw_response: httpx.Response,
        logging_obj: LiteLLMLoggingObj,
    ) -> ResponsesAPIResponse:
        """
        Transform Manus API GET response to OpenAI-compatible format.

        Manus uses camelCase (createdAt) instead of snake_case (created_at).
        Same transformation as transform_response_api_response.
        """
        try:
            logging_obj.post_call(
                original_response=raw_response.text,
                additional_args={"complete_input_dict": {}},
            )
            raw_response_json = raw_response.json()

            # Manus uses camelCase "createdAt" instead of snake_case "created_at"
            if (
                "createdAt" in raw_response_json
                and "created_at" not in raw_response_json
            ):
                raw_response_json["created_at"] = _safe_convert_created_field(
                    raw_response_json["createdAt"]
                )

            # Ensure created_at is set
            if "created_at" in raw_response_json:
                raw_response_json["created_at"] = _safe_convert_created_field(
                    raw_response_json["created_at"]
                )
        except Exception:
            raise OpenAIError(
                message=raw_response.text, status_code=raw_response.status_code
            )

        raw_response_headers = dict(raw_response.headers)
        processed_headers = process_response_headers(raw_response_headers)

        # Ensure reasoning, text, output, and usage are present with defaults
        if (
            "reasoning" not in raw_response_json
            or raw_response_json.get("reasoning") is None
        ):
            raw_response_json["reasoning"] = {}

        if "text" not in raw_response_json or raw_response_json.get("text") is None:
            raw_response_json["text"] = {}

        if "output" not in raw_response_json or raw_response_json.get("output") is None:
            raw_response_json["output"] = []

        if "usage" not in raw_response_json or raw_response_json.get("usage") is None:
            raw_response_json["usage"] = ResponseAPIUsage(
                input_tokens=0,
                output_tokens=0,
                total_tokens=0,
            )

        # Ensure id is present - failed responses may not include it
        if "id" not in raw_response_json or raw_response_json.get("id") is None:
            # Generate a placeholder id for failed responses
            raw_response_json["id"] = f"unknown-{uuid.uuid4().hex[:8]}"

        try:
            response = ResponsesAPIResponse(**raw_response_json)
        except Exception:
            verbose_logger.debug(
                f"Error constructing ResponsesAPIResponse: {raw_response_json}, using model_construct"
            )
            response = ResponsesAPIResponse.model_construct(**raw_response_json)

        # Store processed headers in additional_headers so they get returned to the client
        response._hidden_params["additional_headers"] = processed_headers
        response._hidden_params["headers"] = raw_response_headers
        return response
