# Copyright: (c) 2021, Alina Buzachis <@alinabuzachis>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)


from __future__ import absolute_import, division, print_function

__metaclass__ = type


import asyncio
import urllib

from ansible.errors import AnsibleLookupError
from ansible.module_utils._text import to_native
from ansible_collections.cloud.common.plugins.module_utils.turbo.exceptions import (
    EmbeddedModuleFailure,
)
from ansible_collections.vmware.vmware_rest.plugins.module_utils.vmware_rest import (
    gen_args,
    open_session,
)

INVENTORY = {
    "resource_pool": {
        "list": {
            "query": {
                "clusters": "clusters",
                "datacenters": "datacenters",
                "hosts": "hosts",
                "names": "names",
                "parent_resource_pools": "parent_resource_pools",
                "resource_pools": "resource_pools",
            }
        }
    },
    "datacenter": {
        "list": {
            "query": {
                "datacenters": "datacenters",
                "folders": "folders",
                "names": "names",
            }
        }
    },
    "folder": {
        "list": {
            "query": {
                "datacenters": "datacenters",
                "folders": "folders",
                "names": "names",
                "parent_folders": "parent_folders",
                "type": "type",
            }
        }
    },
    "cluster": {
        "list": {
            "query": {
                "clusters": "clusters",
                "datacenters": "datacenters",
                "folders": "folders",
                "names": "names",
            }
        }
    },
    "host": {
        "list": {
            "query": {
                "clusters": "clusters",
                "datacenters": "datacenters",
                "folders": "folders",
                "hosts": "hosts",
                "names": "names",
            }
        }
    },
    "datastore": {
        "list": {
            "query": {
                "datacenters": "datacenters",
                "datastores": "datastores",
                "folders": "folders",
                "names": "names",
                "types": "types",
            }
        }
    },
    "vm": {
        "list": {
            "query": {
                "clusters": "clusters",
                "datacenters": "datacenters",
                "folders": "folders",
                "hosts": "hosts",
                "names": "names",
                "resource_pools": "resource_pools",
                "vms": "vms",
            }
        }
    },
    "network": {
        "list": {
            "query": {
                "datacenters": "datacenters",
                "folders": "folders",
                "names": "names",
                "networks": "networks",
                "types": "types",
            }
        }
    },
}


class Lookup:
    def __init__(self, options):
        self._options = options

    @classmethod
    async def entry_point(cls, terms, options):
        if not terms or not terms[0]:
            raise AnsibleLookupError(
                "Option _terms is required but no object has been specified"
            )
        session = None

        try:
            session = await open_session(
                vcenter_hostname=options["vcenter_hostname"],
                vcenter_username=options["vcenter_username"],
                vcenter_password=options["vcenter_password"],
                validate_certs=options.get("vcenter_validate_certs"),
                log_file=options.get("vcenter_rest_log_file"),
            )
        except EmbeddedModuleFailure as e:
            raise AnsibleLookupError(
                f'Unable to connect to vCenter or ESXi API at {options.get("vcenter_hostname")}: {to_native(e)}'
            )

        lookup = cls(options)
        lookup._options["session"] = session

        task = asyncio.ensure_future(lookup.moid(terms[0]))

        return await task

    async def fetch(self, url):
        async with self._options["session"].get(url) as response:
            result = await response.json()
            return result

    def build_url(self, object_type, params):
        try:
            _in_query_parameters = INVENTORY[object_type]["list"]["query"].keys()
            if object_type == "resource_pool":
                object_type = object_type.replace("_", "-")
        except KeyError:
            raise AnsibleLookupError(
                "object_type must be one of [%s]." % ", ".join(list(INVENTORY.keys()))
            )

        return (
            f"https://{self._options['vcenter_hostname']}/api/vcenter/{object_type}"
        ) + gen_args(params, _in_query_parameters)

    async def _helper_fetch(self, object_type, filters):
        _url = self.build_url(object_type, filters)
        return await self.fetch(_url)

    @staticmethod
    def ensure_result(result, object_type, object_name=None):
        object_name_decoded = None

        if object_name:
            object_name_decoded = urllib.parse.unquote(object_name)

        if (
            not result
            or object_name_decoded
            and object_name_decoded not in result[0].values()
        ):
            return ""

        def _filter_result(result):
            return [obj for obj in result if "%2f" not in obj["name"]]

        result = _filter_result(result)
        if result and len(result) > 1:
            raise AnsibleLookupError(
                "More than one object available: [%s]."
                % ", ".join(
                    list(f"{item['name']} => {item[object_type]}" for item in result)
                )
            )
        try:
            object_moid = result[0][object_type]
        except (TypeError, KeyError, IndexError) as e:
            raise AnsibleLookupError(to_native(e))
        return object_moid

    def _init_filter(self):
        filters = {}
        filters["datacenters"] = self._options["dc_moid"]
        return filters

    async def _get_datacenter_moid(self, path):
        filters = {}
        dc_name = ""
        dc_moid = ""
        _result = ""
        folder_moid = ""
        _path = path

        # Retrieve folders MoID if any
        folder_moid, _path = await self._get_folder_moid(path, filters)

        if _path:
            dc_name = _path[0]

        filters["names"] = dc_name
        filters["folders"] = folder_moid
        _result = await self._helper_fetch("datacenter", filters)
        dc_moid = self.ensure_result(_result, "datacenter", dc_name)

        return dc_moid, _path

    async def _fetch_result(self, object_path, object_type, filters):
        _result = ""
        obj_moid = ""
        visited = []
        _object_path_list = list(object_path)

        _result = await self.recursive_folder_or_rp_moid_search(
            object_path, object_type, filters
        )
        if _result:
            if object_path:
                for obj in _result:
                    if _object_path_list:
                        if obj["name"] == _object_path_list[0]:
                            del _object_path_list[0]
                        else:
                            visited.append(obj)
                if not visited:
                    obj_moid = [_result[-1]]
            else:
                obj_moid = _result
        return obj_moid, _object_path_list

    async def _get_folder_moid(self, object_path, filters):
        object_name = ""
        result = ""
        _result = ""
        _object_path = []

        if object_path:
            _result, _object_path = await self._fetch_result(
                object_path, "folder", filters
            )
            result = self.ensure_result(_result, "folder")

        if _result and self._options["object_type"] == "folder":
            if isinstance(_result, list) and _object_path:
                obj_path_set = set(_object_path)
                path_set = set([elem["name"] for elem in _result])
                if path_set - obj_path_set and self._options["_terms"][-1] != "/":
                    return "", _object_path

            if self._options["_terms"][-1] != "/":
                object_name = object_path[-1]

            result = self.ensure_result([_result[-1]], "folder", object_name)

        if (
            self._options["_terms"][-1] == "/"
            and self._options["object_type"] == "folder"
        ):
            result = await self.look_inside(result, filters)

        return result, _object_path

    async def _get_host_moid(self, object_path, filters):
        host_moid = ""
        result = ""
        _object_path = []

        # Host might be inside a cluster
        if self._options["object_type"] in ("host", "vm", "network", "datastore"):
            filters["names"] = object_path[0]
            cluster_moid, _object_path = await self._get_cluster_moid(
                object_path, filters
            )
            if not cluster_moid:
                return "", object_path[1:]

            filters = self._init_filter()
            filters["clusters"] = cluster_moid
            if _object_path:
                filters["names"] = _object_path[0]
            else:
                filters["names"] = ""

        result = await self._helper_fetch("host", filters)
        host_moid = self.ensure_result(result, "host", filters["names"])

        return host_moid, _object_path[1:]

    async def _helper_get_resource_pool_moid(self, object_path, filters):
        result = ""
        rp_moid = ""
        _object_path = []

        # Resource pool might be inside a cluster
        filters["names"] = object_path[0]
        cluster_moid, _object_path = await self._get_cluster_moid(object_path, filters)
        if not cluster_moid:
            return "", _object_path

        filters = self._init_filter()
        filters["clusters"] = cluster_moid

        if _object_path:
            result, _object_path = await self._fetch_result(
                _object_path, "resource_pool", filters
            )
            rp_moid = self.ensure_result(result, "resource_pool")

        if not result and _object_path:
            filters = self._init_filter()
            filters["names"] = _object_path[0]
            filters["clusters"] = cluster_moid
            # Resource pool might be inside a host
            host_moid, _object_path = await self._get_host_moid(_object_path, filters)
            if not host_moid:
                return "", _object_path

            filters["hosts"] = host_moid
            if _object_path:
                filters["names"] = _object_path[0]
                result, _object_path = await self._fetch_result(
                    _object_path, "resource_pool", filters
                )

        if result and self._options["object_type"] == "resource_pool":
            if isinstance(result, list) and _object_path:
                obj_path_set = set(_object_path)
                path_set = set([elem["name"] for elem in result])
                if path_set - obj_path_set and self._options["_terms"][-1] != "/":
                    return "", _object_path

        if (
            self._options["_terms"][-1] == "/"
            and self._options["object_type"] == "resource_pool"
        ):
            result = await self.look_inside(rp_moid, filters)
            return result, _object_path

        result = self.ensure_result(result, "resource_pool")

        return result, _object_path

    async def look_inside(self, pre_object, filters):
        result = ""
        _result = ""
        filters["names"] = ""
        object_type = self._options["object_type"]

        if pre_object:
            if (object_type == "resource_pool" and "resgroup" in pre_object) or (
                object_type == "folder" and "group" in pre_object
            ):
                parent_key = f"parent_{object_type}s"
                filters[parent_key] = pre_object

        object_key = f"{object_type}s"
        filters[object_key] = ""
        _result = await self._helper_fetch(object_type, filters)
        result = self.ensure_result(_result, object_type)

        return result

    async def _get_subset_moid(self, object_path, filters):
        object_name = ""
        result = ""
        _result = ""
        _object_path = []

        if not object_path:
            if self._options["_terms"][-1] != "/":
                return "", object_path
        else:
            if self._options["_terms"][-1] != "/":
                object_name = object_path[-1]

        filters["names"] = object_name
        _result = await self._helper_fetch(self._options["object_type"], filters)
        result = self.ensure_result(_result, self._options["object_type"])
        if not result:
            filters["names"] = ""

            if self._options["object_type"] == "vm":
                # VM might be in a resource pool
                result, _object_path = await self._helper_get_resource_pool_moid(
                    object_path, filters
                )
                if result:
                    return result

            # Object might be inside a host
            host_moid, _object_path = await self._get_host_moid(object_path, filters)
            if not host_moid:
                return ""

            filters["hosts"] = host_moid
            filters["folders"] = ""
            filters["names"] = object_name
            _result = await self._helper_fetch(self._options["object_type"], filters)
            result = self.ensure_result(_result, self._options["object_type"])

        return result

    async def _get_cluster_moid(self, object_path, filters):
        cluster_moid = ""
        result = await self._helper_fetch("cluster", filters)
        cluster_moid = self.ensure_result(result, "cluster", filters["names"])

        return cluster_moid, object_path[1:]

    async def get_all_objects_path_moid(self, object_path, object_type, filters):
        # GET MoID of all the objects specified in the path
        filters["names"] = list(set(object_path))

        if self._options["object_type"] == "vm":
            filters["type"] = "VIRTUAL_MACHINE"
        elif self._options["object_type"] not in ("resource_pool", "cluster", "folder"):
            filters["type"] = self._options[
                "object_type"
            ].upper()  # HOST, DATASTORE, DATACENTER, NETWORK

        return await self._helper_fetch(object_type, filters)

    async def recursive_folder_or_rp_moid_search(
        self,
        object_path,
        object_type,
        filters,
        parent_object=None,
        objects_moid=None,
        result=None,
    ):
        if result is None:
            # GET MoID of all the objects specified in the path
            result = []
            objects_moid = await self.get_all_objects_path_moid(
                object_path, object_type, filters
            )
            if not objects_moid:
                return ""
            elif len(objects_moid) == 1:
                return objects_moid

            return await self.recursive_folder_or_rp_moid_search(
                object_path,
                object_type,
                filters,
                objects_moid=objects_moid,
                result=result,
            )
        parent_key = f"parent_{object_type}s"
        filters[parent_key] = ""

        if objects_moid and object_path and objects_moid[0]["name"] == object_path[0]:
            # There exists a folder MoID with this name
            filters["names"] = object_path[0]

            if parent_object is not None:
                filters[parent_key] = parent_object
                tasks = [
                    asyncio.ensure_future(self._helper_fetch(object_type, filters))
                    for parent_object_info in objects_moid
                    if parent_object_info["name"] == object_path[0]
                ]
                _result = [await i for i in tasks]
                [
                    result.append(elem[0])
                    for elem in _result
                    if elem
                    if _result and elem[0] not in result
                ]
            else:
                _result = await self._helper_fetch(object_type, filters)
                if not _result:
                    return ""
                [result.append(elem) for elem in _result if elem not in result]
                # Update parent_object
                parent_object = result[0][object_type]
                return await self.recursive_folder_or_rp_moid_search(
                    object_path[1:],
                    object_type,
                    filters,
                    parent_object,
                    objects_moid[1:],
                    result,
                )
        if not object_path or (objects_moid and len(objects_moid) == 0):
            # Return result and what left in the path
            if not result:
                result = objects_moid
            return result

        if result:
            # Update parent_object
            parent_object = result[-1][object_type]
        return await self.recursive_folder_or_rp_moid_search(
            object_path[1:],
            object_type,
            filters,
            parent_object,
            objects_moid[1:],
            result,
        )

    async def moid(self, object_path):
        folder_moid = ""
        result = ""
        filters = {}
        _path = []

        if not object_path:
            return ""

        # Split object_path for transversal
        self._options["_terms"] = object_path
        object_type = self._options["object_type"]
        path = tuple(filter(None, object_path.split("/")))

        # Retrieve datacenter MoID
        dc_moid, _path = await self._get_datacenter_moid(path)
        if object_type == "datacenter" or not dc_moid:
            return dc_moid
        self._options["dc_moid"] = dc_moid
        filters["datacenters"] = self._options["dc_moid"]

        if _path:
            _path = _path[1:]

        # Retrieve folders MoID
        folder_moid, _path = await self._get_folder_moid(_path, filters)
        if object_type == "folder" or not folder_moid:
            return folder_moid
        filters["folders"] = folder_moid

        if object_type == "cluster":
            if object_path[-1] != "/":
                # Save object name
                filters["names"] = _path[-1]
            else:
                filters["names"] = ""
            result, _obj_path = await self._get_cluster_moid(_path, filters)

        if object_type in ("datastore", "network"):
            filters = self._init_filter()
            filters["folders"] = folder_moid
            result = await self._get_subset_moid(_path, filters)

        if object_type == "resource_pool":
            result, _obj_path = await self._helper_get_resource_pool_moid(
                _path, filters
            )

        if object_type == "vm":
            result = await self._get_subset_moid(_path, filters)

        if object_type == "host":
            result, _obj_path = await self._get_host_moid(_path, filters)

        return result
