import logging
import os
import urllib.parse
from typing import Dict

import requests

from app.schemas.tool.authentication import Authentication, AuthenticationType
from app.schemas.tool.action import ActionMethod, ActionBodyType, ActionParam


# This function code from the Open Source Project TaskingAI.
# The original code can be found at: https://github.com/TaskingAI/TaskingAI
def _prepare_headers(authentication: Authentication, extra_headers: Dict) -> Dict:
    """
    Prepares the headers for an HTTP request including authentication and additional headers.

    :param authentication: An Authentication object containing the authentication details.
    :param extra_headers: A dictionary of additional headers to include in the request.
    :return: A dictionary of headers for the HTTP request.
    """

    headers = {}

    if extra_headers:
        headers.update(extra_headers)

    if authentication:
        if authentication.type == AuthenticationType.basic:
            # Basic Authentication: Assume the secret is a base64 encoded string
            headers["Authorization"] = f"Basic {authentication.secret}"

        elif authentication.type == AuthenticationType.bearer:
            # Bearer Authentication: Add the secret as a bearer token
            headers["Authorization"] = f"Bearer {authentication.secret}"

        elif authentication.type == AuthenticationType.custom:
            # Custom Authentication: Return the custom content as headers
            headers.update(authentication.content)

    return headers


# This function code from the Open Source Project TaskingAI.
# The original code can be found at: https://github.com/TaskingAI/TaskingAI
def _process_parameters(schema: Dict[str, ActionParam], parameters: Dict) -> Dict:
    """
    Processes parameters based on their schema and provided values.

    :param schema: A dictionary representing the parameter schema.
    :param parameters: A dictionary of provided parameter values.
    :return: A dictionary of processed parameters.
    """
    processed_params = {}
    for param_name, action_param in schema.items():
        param_value = None
        # Check for the presence of 'enum' in the parameter schema
        if action_param.is_single_value_enum():
            param_value = action_param.enum[0]
        elif param_name in parameters:
            param_value = parameters[param_name]

        if param_value is not None:
            processed_params[param_name] = param_value

        # todo: check if required
        # if action_param.required and param_value is None:
        #     raise_http_error(ErrorCode.REQUEST_VALIDATION_ERROR, message=f"Missing required parameter {param_name}")

    return processed_params


# This function code from the Open Source Project TaskingAI.
# The original code can be found at: https://github.com/TaskingAI/TaskingAI
def call_action_api(
    url: str,
    method: ActionMethod,
    path_param_schema: Dict[str, ActionParam],
    query_param_schema: Dict[str, ActionParam],
    body_type: ActionBodyType,
    body_param_schema: Dict[str, ActionParam],
    parameters: Dict,
    headers: Dict,
    authentication: Authentication,
) -> Dict:
    """
    Call an API according to OpenAPI schema.
    :param url: the URL of the API call
    :param method: the method of the API call
    :param path_param_schema: the path parameters schema
    :param query_param_schema: the query parameters schema
    :param body_type: the body type
    :param body_param_schema: the body parameters schema
    :param parameters: the parameters input by the user
    :param headers: the extra headers to be included in the API call
    :param authentication: the authentication of the action
    :return: Response from the API call
    """
    authentication.decrypt()
    # Update URL with path parameters
    if path_param_schema:
        path_params = _process_parameters(path_param_schema, parameters)
        for param_name, param_value in path_params.items():
            url = url.replace(f"{{{param_name}}}", urllib.parse.quote(str(param_value)))

    # Prepare query parameters
    query_params = {}
    if query_param_schema:
        query_params = _process_parameters(query_param_schema, parameters)
        # cast boolean values to string
        for param_name, param_value in query_params.items():
            if isinstance(param_value, bool):
                query_params[param_name] = str(param_value).lower()

    # Prepare body
    body = None
    if body_type != ActionBodyType.NONE:
        body = _process_parameters(body_param_schema, parameters)

    # Prepare headers
    prepared_headers = _prepare_headers(authentication, headers)

    # Making the API call
    try:
        request_kwargs = {"headers": prepared_headers}

        if query_params:
            request_kwargs["params"] = query_params

        if os.environ.get("HTTP_PROXY_URL"):
            request_kwargs["proxy"] = os.environ.get("HTTP_PROXY_URL")

        if body_type == ActionBodyType.JSON:
            request_kwargs["json"] = body
            prepared_headers["Content-Type"] = "application/json"
        elif body_type == ActionBodyType.FORM:
            request_kwargs["data"] = body
            prepared_headers["Content-Type"] = "application/x-www-form-urlencoded"

        logging.info(f"call_action_api url={url} request kwargs: {request_kwargs}")

        with requests.request(method.value, url, **request_kwargs) as response:
            response_content_type = response.headers.get("Content-Type", "").lower()
            if "application/json" in response_content_type:
                data = response.json()
            else:
                data = response.text
            if response.status_code == 500:
                error_message = f"API call failed with status {response.status_code}"
                if data:
                    error_message += f": {data}"
                return {"status": response.status_code, "error": error_message}
            return {"status": response.status_code, "data": data}
    except requests.exceptions.RequestException as e:
        return {"status": 500, "error": f"Failed to make the API call: {e}"}
    except Exception:
        return {"status": 500, "error": "Failed to make the API call"}
