import pathlib
from typing import Dict

import jinja2
import textwrap3

from cloudspec import CloudSpec
from cloudspec import CloudSpecParam

MODULE_DOC = '''"""
Autogenerated module using `pop-create-idem <https://gitlab.com/saltstack/pop/pop-create-idem>`__
"""

'''

EXEC_MODULE = '''
async def {{function.name}}(hub, ctx{{function.header_params}}) -> {{function.return_type}}:
    """
    **Autogenerated function**
    {{function.doc}}
    Examples:
        Call from code:

        .. code-block:: python

            await hub.exec.{{function.ref}}(ctx, {{function.required_call_params}})

        Call from CLI:

        .. code-block:: bash

            idem exec {{function.ref}} {{function.required_call_params}}
    """
'''


def run(hub, ctx, root_directory: pathlib.Path):
    cloud_spec = CloudSpec(**ctx.cloud_spec)
    exec_dir = (
        root_directory
        / ctx.clean_name
        / "exec"
        / ctx.service_name
        / ctx.clean_api_version
    )
    for ref, plugin in cloud_spec.plugins.items():
        split = ref.split(".")
        subs = split[1:-1]
        mod = split[-1]
        ref = ".".join([ctx.service_name] + subs + [mod])
        mod_file = exec_dir
        for sub in subs:
            mod_file = mod_file / sub
        mod_file = mod_file / f"{mod}.py"
        hub.tool.path.touch(mod_file)

        mod_file_data = MODULE_DOC
        for function_name, function_data in plugin.functions.items():
            # Remove any part of the path that is in the function name, They are implicitly part of it
            # TODO do this on a switch
            # for r in ref.split("."):
            #    function_name = function_name.replace(r, "")
            # Remove any resulting double underscores
            function_name = function_name.replace("__", "_")
            # Remove any preceding or trailing double underscores
            function_name = function_name.strip("_")
            # noinspection JinjaAutoinspect
            template: jinja2.Template = jinja2.Environment(
                autoescape=True  # nosec
            ).from_string(f"{EXEC_MODULE}\n    {cloud_spec.request_format}\n\n\n")
            if function_data.doc:
                doc = function_data.doc.replace('"""', "'''")
                doc = "\n" + textwrap3.indent(doc, "    ") + "\n"
            else:
                doc = ""
            mod_file_data += template.render(
                function=dict(
                    name=function_name,
                    hardcoded=function_data.hardcoded,
                    doc=doc
                    + _get_param_sphinx_docs(function_data.params)
                    + _get_func_return_sphinx_doc(function_data),
                    ref=f"{ref}.{function_name}",
                    header_params=_get_header_params(function_data.params),
                    required_call_params=_get_call_params(function_data.params),
                    return_type=function_data.return_type,
                ),
                parameter=dict(mapping=_get_map_params(function_data.params)),
            )

        mod_file.write_text(mod_file_data)


def _get_func_return_sphinx_doc(func_data):
    ret = ""
    if func_data.return_type:
        ret += "\n\n    Returns:\n"
        ret += f"        {func_data.return_type}"
        ret += "\n"
    return ret


def _get_param_sphinx_docs(parameters: CloudSpecParam) -> str:
    if not parameters:
        return ""

    ret = "\n    Args:\n"
    for param in parameters.values():
        ret += f"        {param.snaked}"
        if param.param_type:
            ret += f"({param.param_type}{', optional' if not param.required else ''})"

        ret += f": {param.doc}"
        if not ret.endswith("."):
            ret += "."
        if not param.required:
            ret += f" Defaults to {param.default}."
        ret += "\n"

    return ret.rstrip()


def _get_header_params(parameters: CloudSpecParam) -> str:
    ret = ""

    required_params = {
        name: data for name, data in parameters.items() if data["required"]
    }
    for param_name, param_data in required_params.items():
        if param_data.get("param_type"):
            ret += f", {param_data.snaked}: {param_data['param_type']}"
        else:
            ret += f", {param_data.snaked}"

    unrequired_params = {
        name: data for name, data in parameters.items() if not data["required"]
    }

    for param_name, param_data in unrequired_params.items():
        param_type = param_data.get("param_type")
        if param_type:
            ret += f", {param_data.snaked}: {param_type}"
        else:
            ret += f", {param_data.snaked}"
        ret += f" = {param_data.default}"

    return ret


def _get_call_params(parameters: CloudSpecParam) -> str:
    ret = []

    required_params = {
        name: data for name, data in parameters.items() if data["required"]
    }
    for param_data in required_params.values():
        ret.append(f"{param_data.snaked}={param_data.default or 'value'}")

    return ", ".join(ret)


def _get_map_params(parameters: CloudSpecParam) -> Dict[str, str]:
    ret = {}
    map_params = {
        name: data
        for name, data in parameters.items()
        if data["target_type"] == "mapping"
    }
    for name, data in map_params.items():
        target = data["target"]
        if target not in ret:
            ret[target] = {}
        ret[target][name] = data.snaked

    fmt = lambda item: ", ".join(f'"{k}": {v}' for k, v in item.items())
    return {k: f"{{{fmt(v)}}}" for k, v in ret.items()}
