"""Google API Python client library exec module.

Copyright (c) 2021-2022 VMware, Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0

This file implements the entirety of the Plugin Oriented Programming (POP)
exec Sub for Google discovery-based Python client library APIs.

The basic operating model is building Subs as needed to match the nature of a
call to an exec. For example, a call like:

    hub.exec.gcp.compute.instance.get(ctx, ...)

will work, even though no exec subdirectories exist to match that call path.
Instead, the API is dynamically discovered and built to match and tie
them to the appropriate GCP Python SDK wrappers within the tool Sub of this
project.
"""
import copy
from inspect import signature
from typing import Dict
from typing import List

from pop.loader import LoadedMod

from idem_gcp.helpers.exc import NotFoundError

_GCP_SUB_CACHE = {}
_GCP_RESOURCE_TYPES_CACHE = {}


def resolve_sub(hub, ref: str, gcp_api):
    """
    Produces a callable from a string (dotted notation) call to an API.
    :param hub: The Hub into which the resolved callable will get placed.
    :param ref: The call path reference. For example:
        "hub.exec.gcp.compute.instance.get".
    """

    def gen_sub(ref: str, gcp_api):
        callpath = ref.split(".")
        resource_type = ".".join(callpath[1:-1])
        native_resource_type = hub.tool.gcp.resolver.find_native_resource_type(
            resource_type
        )
        method_name = callpath[-1]
        callpath = callpath[0:1] + native_resource_type.split(".") + [callpath[-1]]
        callpath[0:-1] = [hub.tool.gcp.case.camel(v) for v in callpath[0:-1]]

        gcp_api_sub = gcp_api
        for c in callpath[1:]:
            gcp_api_sub = getattr(gcp_api_sub, c)
        cm = ContractMod(
            hub, gcp_api_sub, resource_type, native_resource_type, method_name
        )
        _GCP_SUB_CACHE[ref] = cm
        return cm

    return _GCP_SUB_CACHE.get(ref, gen_sub(ref, gcp_api))


class ContractMod(LoadedMod):
    """
    A Sub (class) to represent actually callable methods within Azure
    ...ManagementClient API sets.

    For example, "list" within the virtual_machines API set in
    ComputeManagementClient.
    """

    def __init__(
        self, hub, gcp_method, resource_type, native_resource_type, method_name
    ):
        """
        Initialize the instance.
        :param hub: The redistributed pop central hub to which to attach
        the Subs created by this class.
        :param gcp_method: The _GCPApi representing the method (from the
        tool Sub).
        """
        super().__init__(name=gcp_method.name)
        self._hub = hub
        self._gcp_method = gcp_method
        self._resource_type = resource_type
        self._native_resource_type = native_resource_type
        self._method_name = method_name

    @property
    def signature(self):
        """
        Returns the signature of the __call__ method.

        When calling functions that contain "ctx" within the arguments to the
        function, POP injects the acct profile into "ctx" so it needs the
        call signature in order to do that.
        """
        return signature(self.__call__)

    def _missing(self, item: str):
        """Return a value for the given key (item)."""
        return self

    async def __call__(self, ctx, *args, **kwargs):
        """
        Closure on hub/target that calls the target function.
        :param ctx: A dict with the keys/values for the execution of the Idem run
        located in `hub.idem.RUNS[ctx['run_name']]`.
        :param args: Tuple of positional arguments.
        :param kwargs: dict of named *keyward/value) arguments.
        :return: Results of call in standard exec packaging:
            result: True if the API succeeded, False otherwise.
            ret: Any data returned by the API.
            comment: Any relevant info generated by the API (e.g.,
            "code 200", "code 404", an exception message, etc.).
        """
        result = {"result": True, "ret": None, "comment": []}

        # TODO: Set generation and metageneration

        present_params = copy.deepcopy(kwargs)
        name = present_params.pop("name", None)
        resource_id = present_params.pop("resource_id", None)

        if resource_id:
            present_params.update(
                self._hub.tool.gcp.resource_prop_utils.get_elements_from_resource_id(
                    self._resource_type, resource_id
                )
            )

        if present_params.get("user_project") == "None":
            present_params["user_project"] = None

        exclude_keys_from_transformation = None
        if present_params.get("body", False):
            exclude_keys_from_transformation = self._hub.tool.gcp.resource_prop_utils.get_exclude_keys_from_transformation(
                present_params["body"], self._resource_type
            )

        raw_params = (
            self._hub.tool.gcp.conversion_utils.convert_present_resource_to_raw(
                present_params, self._resource_type, exclude_keys_from_transformation
            )
        )

        # TODO: Fix projection required or not
        if self._method_name in {"create"} and not self._resource_type.startswith(
            "cloudkms"
        ):
            raw_params["projection"] = "full"

        try:
            response = await self._gcp_method(ctx, *args, **raw_params)
            # Is it possible to get None for response. It looks like on error exceptions is thrown. If we are here
            # then the call was successful.
            if response is None:
                result["result"] = False
                return result

            if "#operation" in response.get("kind", ""):
                result["ret"] = response
                return result

            items = response.get("items")
            if isinstance(items, dict):
                response[
                    "items"
                ] = self._hub.tool.gcp.resolver.convert_dict_items_to_list(
                    items, self._native_resource_type
                )

            resource_type_camel = self._hub.tool.gcp.case.camel(
                self._native_resource_type.split(".")[-1]
            )
            if resource_type_camel in response:
                response["items"] = response[resource_type_camel]

            # TODO: Handle regional and global operations

            if response.get("nextPageToken"):
                all_items = response.get("items", [])
                raw_params["pageToken"] = response["nextPageToken"]

                while response.get("nextPageToken"):
                    response = await self._gcp_method(ctx, *args, **raw_params)

                    items = response.get("items")
                    if isinstance(items, dict):
                        response[
                            "items"
                        ] = self._hub.tool.gcp.resolver.convert_dict_items_to_list(
                            items, self._native_resource_type
                        )

                    resource_type_camel = self._hub.tool.gcp.case.camel(
                        self._native_resource_type.split(".")[-1]
                    )
                    if resource_type_camel in response:
                        response["items"] = response[resource_type_camel]

                    all_items += response.get("items", [])
                    raw_params["pageToken"] = response.get("nextPageToken")

                response["items"] = all_items

            result["ret"] = self._hub.tool.gcp.resolver.process_response(
                response, self._resource_type, self._method_name
            )
        except Exception as e:
            if self._method_name == "get" and isinstance(e, NotFoundError):
                result["comment"].append(
                    self._hub.tool.gcp.comment_utils.get_empty_comment(
                        self._resource_type, name if name else resource_id
                    )
                )
            else:
                result["result"] = False
            result["comment"].append(str(e))

        return result


def process_response(hub, response: Dict, resource_type: str, method_name: str) -> Dict:
    # TODO: Get does not return all the necessary properties which should be the present properties so
    #  decide what to do

    present_props_names = hub.tool.gcp.resource_prop_utils.get_present_properties(
        resource_type
    )
    singular_result_expected = hub.tool.gcp.resolver.has_singular_result(
        response, resource_type, method_name
    )
    if singular_result_expected:
        resource_id = hub.tool.gcp.resource_prop_utils.extract_resource_id(
            response, resource_type
        )
        if resource_id:
            present_item = hub.tool.gcp.resolver.filter_raw_properties_to_present(
                response, present_props_names, resource_type, resource_id
            )
            return present_item

        return response
    else:
        # Multiple entries in call result, e.g. after list() call
        ret_items = []
        for service_resource in response.get("items") or {}:
            resource_id = hub.tool.gcp.resource_prop_utils.extract_resource_id(
                service_resource, resource_type
            )

            present_item = None
            if resource_id:
                present_item = hub.tool.gcp.resolver.filter_raw_properties_to_present(
                    service_resource, present_props_names, resource_type, resource_id
                )

            ret_items.append(present_item if present_item else service_resource)

        return {"items": ret_items}

    return {}


def filter_raw_properties_to_present(
    hub,
    raw_props: Dict,
    present_properties_names: List,
    resource_type: str,
    resource_id: str,
) -> Dict:
    filtered = {
        key: value
        for key, value in raw_props.items()
        if key in present_properties_names
    }
    filtered["resource_id"] = resource_id
    filtered = hub.tool.gcp.resource_prop_utils.format_path_params(
        filtered, resource_type
    )
    return hub.tool.gcp.conversion_utils.convert_raw_resource_to_present(filtered)


def convert_dict_items_to_list(hub, dict_items: Dict, resource_type: str) -> list:
    list_items = []
    simple_resource_type = resource_type.split(".")[-1]
    simple_resource_type_camel = hub.tool.gcp.case.camel(simple_resource_type)

    # TODO: How to check that for every resource the nested container in grouped list results is always
    #  named after the resource itself?
    for group_name in dict_items.values():
        if (
            simple_resource_type not in group_name
            and simple_resource_type_camel not in group_name
        ):
            continue
        for service_resource in group_name.get(simple_resource_type_camel):
            list_items.append(service_resource)

    return list_items


def has_singular_result(
    hub, response: Dict, resource_type: str, method_name: str
) -> bool:
    # TODO: Figure out a better mechanism to determine whether a given method is expected to return a single entity
    if isinstance(response.get("items"), list):
        return False
    if method_name in {"list", "aggregatedList", "describe"}:
        return False
    return True


def find_native_resource_type(hub, resource_type: str) -> str:
    def fetch_res_type_mapping(res_type: str) -> str:
        hub_ref = hub.metadata.gcp
        for part in res_type.split("."):
            hub_ref = hub_ref[part]
        try:
            return hub_ref["NATIVE_RESOURCE_TYPE"]
        except AttributeError:
            return res_type

    return _GCP_RESOURCE_TYPES_CACHE.get(
        resource_type, fetch_res_type_mapping(resource_type)
    )
