from typing import Any, Dict, List, Optional

from openai.types.chat.completion_create_params import Function
from pydantic import BaseModel

from api.utils.compat import model_dump


def convert_data_type(param_type: str) -> str:
    """ convert data_type to typescript data type """
    return "number" if param_type in {"integer", "float"} else param_type


def get_param_type(param: Dict[str, Any]) -> str:
    """ get param_type of parameter """
    param_type = "any"
    if "type" in param:
        raw_param_type = param["type"]
        param_type = (
            " | ".join(raw_param_type)
            if type(raw_param_type) is list
            else raw_param_type
        )
    elif "oneOf" in param:
        one_of_types = [
            convert_data_type(item["type"])
            for item in param["oneOf"]
            if "type" in item
        ]
        one_of_types = list(set(one_of_types))
        param_type = " | ".join(one_of_types)
    return convert_data_type(param_type)


def get_format_param(param: Dict[str, Any]) -> Optional[str]:
    """ Get "format" from param. There are cases where format is not directly in param but in oneOf """
    if "format" in param:
        return param["format"]
    if "oneOf" in param:
        formats = [item["format"] for item in param["oneOf"] if "format" in item]
        if formats:
            return " or ".join(formats)
    return None


def get_param_info(param: Dict[str, Any]) -> Optional[str]:
    """ get additional information about parameter such as: format, default value, min, max, ... """
    param_type = param.get("type", "any")
    info_list = []
    if "description" in param:
        desc = param["description"]
        if not desc.endswith("."):
            desc += "."
        info_list.append(desc)

    if "default" in param:
        default_value = param["default"]
        if param_type == "string":
            default_value = f'"{default_value}"'  # if string --> add ""
        info_list.append(f"Default={default_value}.")

    format_param = get_format_param(param)
    if format_param is not None:
        info_list.append(f"Format={format_param}")

    info_list.extend(
        f"{field_name}={str(param[field])}"
        for field, field_name in [
            ("maximum", "Maximum"),
            ("minimum", "Minimum"),
            ("maxLength", "Maximum length"),
            ("minLength", "Minimum length"),
        ]
        if field in param
    )
    if info_list:
        result = "// " + " ".join(info_list)
        return result.replace("\n", " ")
    return None


def append_new_param_info(info_list: List[str], param_declaration: str, comment_info: Optional[str], depth: int):
    """ Append a new parameter with comment to the info_list """
    offset = "".join(["    " for _ in range(depth)]) if depth >= 1 else ""
    if comment_info is not None:
        # if depth == 0:  # format: //comment\nparam: type
        info_list.append(f"{offset}{comment_info}")
    info_list.append(f"{offset}{param_declaration}")


def get_enum_option_str(enum_options: List) -> str:
    """get enum option separated by: "|"

    Args:
        enum_options (List): list of options

    Returns:
        _type_: concatenation of options separated by "|"
    """
    # if each option is string --> add quote
    return " | ".join([f'"{v}"' if type(v) is str else str(v) for v in enum_options])


def get_array_typescript(param_name: Optional[str], param_dic: dict, depth: int = 0) -> str:
    """recursive implementation for generating type script of array

    Args:
        param_name (Optional[str]): name of param, optional
        param_dic (dict): param_dic
        depth (int, optional): nested level. Defaults to 0.

    Returns:
        _type_: typescript of array
    """
    offset = "".join(["    " for _ in range(depth)]) if depth >= 1 else ""
    items_info = param_dic.get("items", {})

    if len(items_info) == 0:
        return f"{offset}{param_name}: []" if param_name is not None else "[]"
    array_type = get_param_type(items_info)
    if array_type == "object":
        info_lines = []
        child_lines = get_parameter_typescript(
            items_info.get("properties", {}), items_info.get("required", []), depth + 1
        )
        # if comment_info is not None:
        #    info_lines.append(f"{offset}{comment_info}")
        if param_name is not None:
            info_lines.append(f"{offset}{param_name}" + ": {")
        else:
            info_lines.append(f"{offset}" + "{")
        info_lines.extend(child_lines)
        info_lines.append(f"{offset}" + "}[]")
        return "\n".join(info_lines)

    elif array_type == "array":
        item_info = get_array_typescript(None, items_info, depth + 1)
        if param_name is None:
            return f"{item_info}[]"
        return f"{offset}{param_name}: {item_info.strip()}[]"

    else:
        if "enum" not in items_info:
            return (
                f"{array_type}[]"
                if param_name is None
                else f"{offset}{param_name}: {array_type}[],"
            )
        item_type = get_enum_option_str(items_info["enum"])
        if param_name is None:
            return f"({item_type})[]"
        else:
            return f"{offset}{param_name}: ({item_type})[]"


def get_parameter_typescript(properties, required_params, depth=0) -> List[str]:
    """Recursion, returning the information about parameters including data type, description and other information
    These kinds of information will be put into the prompt

    Args:
        properties (_type_): properties in parameters
        required_params (_type_): List of required parameters
        depth (int, optional): the depth of params (nested level). Defaults to 0.

    Returns:
        _type_: list of lines containing information about all parameters
    """
    tp_lines = []
    for param_name, param in properties.items():
        # Sometimes properties have "required" field as a list of string.
        # Even though it is supposed to be not under properties. So we skip it
        if not isinstance(param, dict):
            continue
        # Param Description
        comment_info = get_param_info(param)
        # Param Name declaration
        param_declaration = f"{param_name}"
        if isinstance(required_params, list) and param_name not in required_params:
            param_declaration += "?"
        param_type = get_param_type(param)

        offset = ""
        if depth >= 1:
            offset = "".join(["    " for _ in range(depth)])

        if param_type == "object":  # param_type is object
            child_lines = get_parameter_typescript(param.get("properties", {}), param.get("required", []), depth + 1)
            if comment_info is not None:
                tp_lines.append(f"{offset}{comment_info}")

            param_declaration += ": {"
            tp_lines.append(f"{offset}{param_declaration}")
            tp_lines.extend(child_lines)
            tp_lines.append(f"{offset}" + "},")

        elif param_type == "array":  # param_type is an array
            item_info = param.get("items", {})
            if "type" not in item_info:  # don't know type of array
                param_declaration += ": [],"
                append_new_param_info(tp_lines, param_declaration, comment_info, depth)
            else:
                array_declaration = get_array_typescript(param_declaration, param, depth)
                if not array_declaration.endswith(","):
                    array_declaration += ","
                if comment_info is not None:
                    tp_lines.append(f"{offset}{comment_info}")
                tp_lines.append(array_declaration)
        else:
            if "enum" in param:
                param_type = " | ".join([f'"{v}"' for v in param["enum"]])
            param_declaration += f": {param_type},"
            append_new_param_info(tp_lines, param_declaration, comment_info, depth)

    return tp_lines


def generate_schema_from_functions(functions: List[Function], namespace="functions") -> str:
    """
    Convert functions schema to a schema that language models can understand.
    """

    schema = "// Supported function definitions that should be called when necessary.\n"
    schema += f"namespace {namespace} {{\n\n"

    for function in functions:
        # Convert a Function object to dict, if necessary
        if isinstance(function, BaseModel):
            function = model_dump(function)
        function_name = function.get("name", None)
        if function_name is None:
            continue

        description = function.get("description", "")
        schema += f"// {description}\n"
        schema += f"type {function_name}"

        parameters = function.get("parameters", None)
        if parameters is not None and parameters.get("properties") is not None:
            schema += " = (_: {\n"
            required_params = parameters.get("required", [])
            tp_lines = get_parameter_typescript(parameters.get("properties"), required_params, 0)
            schema += "\n".join(tp_lines)
            schema += "\n}) => any;\n\n"
        else:
            # Doesn't have any parameters
            schema += " = () => any;\n\n"

    schema += f"}} // namespace {namespace}"

    return schema


def generate_schema_from_openapi(specification: Dict[str, Any], description: str, namespace: str) -> str:
    """
    Convert OpenAPI specification object to a schema that language models can understand.

    Input:
    specification: can be obtained by json. loads of any OpanAPI json spec, or yaml.safe_load for yaml OpenAPI specs

    Example output:

    // General Description
    namespace functions {

    // Simple GET endpoint
    type getEndpoint = (_: {
    // This is a string parameter
    param_string: string,
    param_integer: number,
    param_boolean?: boolean,
    param_enum: "value1" | "value2" | "value3",
    }) => any;

    } // namespace functions
    """

    description_clean = description.replace("\n", "")

    schema = f"// {description_clean}\n"
    schema += f"namespace {namespace} {{\n\n"

    for path_name, paths in specification.get("paths", {}).items():
        for method_name, method_info in paths.items():
            operationId = method_info.get("operationId", None)
            if operationId is None:
                continue
            description = method_info.get("description", method_info.get("summary", ""))
            schema += f"// {description}\n"
            schema += f"type {operationId}"

            if ("requestBody" in method_info) or (method_info.get("parameters") is not None):
                schema += f"  = (_: {{\n"
                # Body
                if "requestBody" in method_info:
                    try:
                        body_schema = (
                            method_info.get("requestBody", {})
                            .get("content", {})
                            .get("application/json", {})
                            .get("schema", {})
                        )
                    except AttributeError:
                        body_schema = {}
                    for param_name, param in body_schema.get("properties", {}).items():
                        # Param Description
                        description = param.get("description")
                        if description is not None:
                            schema += f"// {description}\n"

                        # Param Name
                        schema += f"{param_name}"
                        if (
                            (not param.get("required", False))
                            or (param.get("nullable", False))
                            or (param_name in body_schema.get("required", []))
                        ):
                            schema += "?"

                        # Param Type
                        param_type = param.get("type", "any")
                        if param_type == "integer":
                            param_type = "number"
                        if "enum" in param:
                            param_type = " | ".join([f'"{v}"' for v in param["enum"]])
                        schema += f": {param_type},\n"

                # URL
                for param in method_info.get("parameters", []):
                    # Param Description
                    if description := param.get("description"):
                        schema += f"// {description}\n"

                    # Param Name
                    schema += f"{param['name']}"
                    if (not param.get("required", False)) or (param.get("nullable", False)):
                        schema += "?"
                    if param.get("schema") is None:
                        continue
                    # Param Type
                    param_type = param["schema"].get("type", "any")
                    if param_type == "integer":
                        param_type = "number"
                    if "enum" in param["schema"]:
                        param_type = " | ".join([f'"{v}"' for v in param["schema"]["enum"]])
                    schema += f": {param_type},\n"

                schema += f"}}) => any;\n\n"
            else:
                # Doesn't have any parameters
                schema += f" = () => any;\n\n"

    schema += f"}} // namespace {namespace}"

    return schema


if __name__ == "__main__":
    functions = [
        {
            "name": "get_current_weather",
            "description": "Get the current weather in a given location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city and state, e.g. San Francisco, CA",
                    },
                    "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                },
                "required": ["location"],
            },
        }
    ]
    print(generate_schema_from_functions(functions))