# coding: utf-8
# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
import asyncio
import json
from typing import AsyncIterator, ClassVar, Dict, Any, Literal, Set

import aiohttp
from oauthlib.common import urlencode
from pydantic import Field, field_validator

from openjiuwen.core.common.exception.codes import StatusCode
from openjiuwen.core.common.exception.errors import build_error
from openjiuwen.core.common.security.ssl_utils import SslUtils
from openjiuwen.core.common.security.url_utils import UrlUtils
from openjiuwen.core.common.utils.schema_utils import SchemaUtils
from openjiuwen.core.foundation.tool import Tool
from openjiuwen.core.foundation.tool.base import Input, Output, ToolCard
from openjiuwen.core.foundation.tool.service_api.api_param_mapper import APIParamLocation, APIParamMapper


class RestfulApiCard(ToolCard):
    """RESTful API tool card with HTTP method validation."""
    SUPPORTED_METHODS: ClassVar[Set[str]] = ["POST", "GET"]
    url: str = Field(..., description="RESTful API path, such as: /api/v1/users")
    method: Literal["POST", "GET"] = Field(default="POST", description="HTTP method, only POST or GET supported")
    headers: Dict[str, Any] = Field(default_factory=dict, description="Request headers")
    queries: Dict[str, Any] = Field(default_factory=dict, description="Request query parameters")
    paths: Dict[str, Any] = Field(default_factory=dict, description="Path parameters for URL placeholders")
    timeout: float = Field(default=60.0, ge=1.0, le=300.0, description="Request timeout in seconds")
    max_response_byte_size: int = Field(default=10 * 1024 * 1024, description="Response max size in bytes")

    @field_validator('method')
    @classmethod
    def validate_method(cls, v: str) -> str:
        v_upper = v.upper()
        if v_upper not in cls.SUPPORTED_METHODS:
            raise build_error(StatusCode.TOOL_RESTFUL_API_CARD_CONFIG_INVALID,
                              reason=f"support invalid method, method={v}, only accepts: {cls.SUPPORTED_METHODS}.")
        return v_upper

    @field_validator('url')
    @classmethod
    def validate_url(cls, v: str) -> str:
        try:
            UrlUtils.check_url_is_valid(v)
        except Exception as e:
            raise build_error(StatusCode.TOOL_RESTFUL_API_CARD_CONFIG_INVALID, cause=e,
                              reason=f"support invalid url, url={v}.")

        return v


class RestfulApi(Tool):
    _RESTFUL_SSL_VERIFY = "RESTFUL_SSL_VERIFY"
    _RESTFUL_SSL_CERT = "RESTFUL_SSL_CERT"

    def __init__(self, card: RestfulApiCard):
        super().__init__(card)
        self._url = card.url
        self._method = card.method
        self._timeout = card.timeout
        self._max_response_byte_size = card.max_response_byte_size
        self._api_param_mapper = APIParamMapper(self._card.input_params,
                                                default_queries=card.queries,
                                                default_paths=card.paths,
                                                default_headers=card.headers)

    async def _async_request(self, map_results: dict, timeout: float, max_response_byte_size: int):
        request_arg = {}
        if self._method in ["GET"]:
            request_arg["params"] = map_results.get(APIParamLocation.BODY)
        else:
            request_arg["json"] = map_results.get(APIParamLocation.BODY)
        ssl_verify, ssl_cert = SslUtils.get_ssl_config(self._RESTFUL_SSL_VERIFY, self._RESTFUL_SSL_CERT, ["false"])
        if ssl_verify:
            ssl_context = SslUtils.create_strict_ssl_context(ssl_cert)
            connector = aiohttp.TCPConnector(ssl=ssl_context)
        else:
            connector = aiohttp.TCPConnector(ssl=False)
        url = self._url
        path_params = {k: str(v) for k, v in map_results.get(APIParamLocation.PATH).items()}
        if path_params:
            url = url.format(**path_params)
        query_params = [(k, v) for k, v in map_results.get(APIParamLocation.QUERY, {}).items()]
        if query_params:
            url = f'{url}?{urlencode(query_params)}'
        proxy = UrlUtils.get_global_proxy_url(url)
        async with aiohttp.ClientSession(proxy=proxy, connector=connector) as session:
            async with session.request(
                    self._method,
                    url,
                    headers=map_results.get(APIParamLocation.HEADER),
                    allow_redirects=False,
                    timeout=aiohttp.ClientTimeout(total=timeout),
                    **request_arg,
            ) as response:
                response.raise_for_status()
                response_data = await self._format_response(response, max_response_byte_size)
        return response_data

    async def invoke(self, inputs: Input, **kwargs) -> Output:
        final_timeout = self._timeout
        try:
            if self._card.input_params is not None:
                inputs = SchemaUtils.format_with_schema(inputs, self._card.input_params,
                                                        skip_none_value=kwargs.get("skip_none_value", False),
                                                        skip_validate=kwargs.get("skip_inputs_validate", False))
            map_results = self._api_param_mapper.map(inputs, default_location=APIParamLocation.BODY)
            final_timeout = kwargs.get("timeout", self._timeout)
            return await self._async_request(map_results,
                                             final_timeout,
                                             kwargs.get("max_response_byte_size", self._max_response_byte_size))
        except (aiohttp.ConnectionTimeoutError, asyncio.TimeoutError) as e:
            raise build_error(StatusCode.TOOL_RESTFUL_API_TIMEOUT, cause=e,
                              interface="invoke", timeout=final_timeout, card=self.card)
        except aiohttp.ClientResponseError as e:
            raise build_error(StatusCode.TOOL_RESTFUL_API_RESPONSE_ERROR, cause=e,
                              interface="invoke", code=e.status, reason=e.message,
                              card=self.card)
        except Exception as e:
            raise build_error(StatusCode.TOOL_RESTFUL_API_EXECUTION_ERROR, cause=e,
                              interface="invoke", reason=str(e),
                              card=self.card)

    async def stream(self, inputs: Input, **kwargs) -> AsyncIterator[Output]:
        raise build_error(StatusCode.TOOL_STREAM_NOT_SUPPORTED, card=self._card)

    async def _format_response(self, response: aiohttp.ClientResponse, response_bytes_size_limit):
        content = b""
        async for chunk in response.content.iter_chunked(1024):
            content += chunk
            if len(content) > response_bytes_size_limit:
                raise build_error(StatusCode.TOOL_RESTFUL_API_RESPONSE_SIZE_EXCEED_LIMIT,
                                  interface="invoke", max_length=response_bytes_size_limit, actual_length=len(content),
                                  card=self._card)
        res = json.loads(content.decode("utf-8"))
        return res
