# Copyright (c) Microsoft. All rights reserved.

import logging
from typing import TYPE_CHECKING, Any

from semantic_kernel.connectors.openapi_plugin.const import OperationExtensions
from semantic_kernel.connectors.openapi_plugin.models.rest_api_operation import RestApiOperation
from semantic_kernel.connectors.openapi_plugin.models.rest_api_parameter import RestApiParameter
from semantic_kernel.connectors.openapi_plugin.models.rest_api_run_options import RestApiRunOptions
from semantic_kernel.connectors.openapi_plugin.models.rest_api_security_requirement import RestApiSecurityRequirement
from semantic_kernel.connectors.openapi_plugin.models.rest_api_uri import Uri
from semantic_kernel.connectors.openapi_plugin.openapi_parser import OpenApiParser
from semantic_kernel.connectors.openapi_plugin.openapi_runner import OpenApiRunner
from semantic_kernel.connectors.openapi_plugin.server_url_validator import ServerUrlValidationOptions
from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException
from semantic_kernel.functions.kernel_arguments import KernelArguments
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.functions.kernel_function_from_method import KernelFunctionFromMethod
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
from semantic_kernel.schema.kernel_json_schema_builder import TYPE_MAPPING
from semantic_kernel.utils.feature_stage_decorator import experimental

if TYPE_CHECKING:
    from semantic_kernel.connectors.openapi_plugin.openapi_function_execution_parameters import (
        OpenAPIFunctionExecutionParameters,
    )

logger: logging.Logger = logging.getLogger(__name__)


@experimental
def create_functions_from_openapi(
    plugin_name: str,
    openapi_document_path: str | None = None,
    openapi_parsed_spec: dict[str, Any] | None = None,
    execution_settings: "OpenAPIFunctionExecutionParameters | None" = None,
) -> list[KernelFunctionFromMethod]:
    """Creates the functions from OpenAPI document.

    Args:
        plugin_name: The name of the plugin
        openapi_document_path: The OpenAPI document path, it must be a file path to the spec (optional)
        openapi_parsed_spec: The parsed OpenAPI spec (optional)
        execution_settings: The execution settings

    Returns:
        list[KernelFunctionFromMethod]: the operations as functions
    """
    parsed_doc: dict[str, Any] | Any = None
    if openapi_parsed_spec is not None:
        parsed_doc = openapi_parsed_spec
    else:
        if openapi_document_path is None:
            raise FunctionExecutionException(
                "Either `openapi_document_path` or `openapi_parsed_spec` must be provided."
            )

        # Parse the document from the given path
        parser = OpenApiParser()
        parsed_doc = parser.parse(
            openapi_document_path,
            enable_file_ref_resolution=(execution_settings.enable_file_ref_resolution if execution_settings else False),
            enable_http_ref_resolution=(execution_settings.enable_http_ref_resolution if execution_settings else False),
        )
        if parsed_doc is None:
            raise FunctionExecutionException(f"Error parsing OpenAPI document: {openapi_document_path}")

    parser = OpenApiParser()
    operations = parser.create_rest_api_operations(parsed_doc, execution_settings=execution_settings)

    global_security_requirements = parsed_doc.get("security", [])

    auth_callback = None
    if execution_settings and execution_settings.auth_callback:
        auth_callback = execution_settings.auth_callback

    openapi_runner = OpenApiRunner(
        parsed_openapi_document=parsed_doc,
        auth_callback=auth_callback,
        http_client=execution_settings.http_client if execution_settings else None,
        enable_dynamic_payload=execution_settings.enable_dynamic_payload if execution_settings else True,
        enable_payload_namespacing=execution_settings.enable_payload_namespacing if execution_settings else False,
        server_url_validation_options=ServerUrlValidationOptions(
            allowed_base_urls=execution_settings.server_url_validation_allowed_base_urls,
            allow_private_network_access=execution_settings.allow_private_network_access,
        )
        if execution_settings
        else None,
    )

    functions = []
    for operation in operations.values():
        try:
            kernel_function = _create_function_from_operation(
                openapi_runner,
                operation,
                plugin_name,
                execution_parameters=execution_settings,
                security=global_security_requirements,
            )
            functions.append(kernel_function)
            operation.freeze()
        except Exception as ex:
            error_msg = f"Error while registering Rest function {plugin_name}.{operation.id}: {ex}"
            logger.error(error_msg)
            raise FunctionExecutionException(error_msg) from ex

    return functions


@experimental
def _create_function_from_operation(
    runner: OpenApiRunner,
    operation: RestApiOperation,
    plugin_name: str | None = None,
    execution_parameters: "OpenAPIFunctionExecutionParameters | None" = None,
    document_uri: str | None = None,
    security: list[RestApiSecurityRequirement] | None = None,
) -> KernelFunctionFromMethod:
    logger.info(f"Registering OpenAPI operation: {plugin_name}.{operation.id}")

    rest_operation_params: list[RestApiParameter] = operation.get_parameters(
        operation=operation,
        add_payload_params_from_metadata=getattr(execution_parameters, "enable_dynamic_payload", True),
        enable_payload_spacing=getattr(execution_parameters, "enable_payload_namespacing", False),
    )

    @kernel_function(
        description=operation.summary if operation.summary else operation.description,
        name=operation.id,
    )
    async def run_openapi_operation(
        **kwargs: dict[str, Any],
    ) -> str:
        try:
            kernel_arguments = KernelArguments()

            for parameter in rest_operation_params:
                if parameter.alternative_name and parameter.alternative_name in kwargs:
                    value = kwargs[parameter.alternative_name]
                    if value is not None:
                        kernel_arguments[parameter.name] = value
                        continue

                if parameter.name in kwargs:
                    value = kwargs[parameter.name]
                    if value is not None:
                        kernel_arguments[parameter.name] = value
                        continue

                if parameter.is_required:
                    raise FunctionExecutionException(
                        f"No variable found in context to use as an argument for the "
                        f"`{parameter.name}` parameter of the `{plugin_name}.{operation.id}` REST function."
                    )

            options = RestApiRunOptions(
                server_url_override=(
                    execution_parameters.server_url_override
                    if execution_parameters and execution_parameters.server_url_override is not None
                    else None
                ),
                api_host_url=Uri(document_uri).get_left_part() if document_uri is not None else None,
                timeout=execution_parameters.timeout
                if execution_parameters and execution_parameters.timeout is not None
                else None,
            )

            return await runner.run_operation(operation, kernel_arguments, options)
        except Exception as e:
            logger.error(f"Error running OpenAPI operation: {operation.id}", exc_info=True)
            raise FunctionExecutionException(f"Error running OpenAPI operation: {operation.id}") from e

    parameters: list[KernelParameterMetadata] = [
        KernelParameterMetadata(
            name=p.alternative_name or p.name,
            description=f"{p.description or p.name}",
            default_value=p.default_value or "",
            is_required=p.is_required,
            type_=p.type if p.type is not None else TYPE_MAPPING.get(p.type, "object"),
            schema_data=(
                p.schema
                if p.schema is not None and isinstance(p.schema, dict)
                else {"type": f"{p.type}"}
                if p.type
                else None
            ),
        )
        for p in rest_operation_params
    ]

    return_parameter = operation.get_default_return_parameter()

    additional_metadata = {
        OperationExtensions.METHOD_KEY.value: operation.method.upper(),
        OperationExtensions.OPERATION_KEY.value: operation,
        OperationExtensions.SERVER_URLS_KEY.value: (
            [operation.servers[0]["url"]]
            if operation.servers and len(operation.servers) > 0 and operation.servers[0]["url"]
            else []
        ),
    }

    if security is not None:
        additional_metadata[OperationExtensions.SECURITY_KEY.value] = security

    return KernelFunctionFromMethod(
        method=run_openapi_operation,
        plugin_name=plugin_name,
        parameters=parameters,
        return_parameter=return_parameter,
        additional_metadata=additional_metadata,
    )
