import concurrent.futures
import logging

from spaceone.core.service import *
from spaceone.core.utils import get_dict_value, random_string

from spaceone.monitoring.conf.global_conf import *
from spaceone.monitoring.error import *
from spaceone.monitoring.manager.data_source_manager import DataSourceManager
from spaceone.monitoring.manager.data_source_plugin_manager import (
    DataSourcePluginManager,
)
from spaceone.monitoring.manager.inventory_manager import InventoryManager
from spaceone.monitoring.manager.secret_manager import SecretManager

_LOGGER = logging.getLogger(__name__)


@authentication_handler
@authorization_handler
@mutation_handler
@event_handler
class MetricService(BaseService):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.inventory_mgr: InventoryManager = self.locator.get_manager(
            "InventoryManager"
        )
        self.secret_mgr: SecretManager = self.locator.get_manager("SecretManager")
        self.data_source_mgr: DataSourceManager = self.locator.get_manager(
            "DataSourceManager"
        )
        self.ds_plugin_mgr: DataSourcePluginManager = self.locator.get_manager(
            "DataSourcePluginManager"
        )

    @transaction(
        permission="monitoring:Metric.read",
        role_types=["DOMAIN_ADMIN", "WORKSPACE_OWNER", "WORKSPACE_MEMBER"],
    )
    @check_required(["data_source_id", "resources", "domain_id"])
    def list(self, params):
        """Get resource's metrics

        Args:
            params (dict): {
                'data_source_id': 'str',   # required
                'resources': 'list',       # required
                'domain_id': 'str'         # injected from auth (required)
            }

        Returns:
            metrics (list)
        """
        data_source_id = params["data_source_id"]
        resources = params["resources"]
        domain_id = params["domain_id"]

        data_source_vo = self.data_source_mgr.get_data_source(data_source_id, domain_id)
        self._check_data_source_state(data_source_vo)

        plugin_info = data_source_vo.plugin_info
        plugin_metadata = plugin_info.metadata
        self._check_plugin_metadata(plugin_metadata, params, data_source_id)
        self.plugin_initialize(data_source_vo)

        metrics_dict = {}
        and_metric_keys = []

        response = {"metrics": [], "available_resources": {}, "domain_id": domain_id}

        for resource_id in resources:
            response["available_resources"][resource_id] = False

        required_keys = plugin_metadata.get("required_keys")
        cloud_services_info = self.inventory_mgr.list_resources(
            resources, required_keys, domain_id
        )

        list_metric_params = []
        for cloud_service_info in cloud_services_info:
            secret = self.secret_mgr.get_secret_from_resource(
                cloud_service_info, data_source_vo, domain_id
            )
            secret_data = self.secret_mgr.get_secret_data(
                secret.get("secret_id", ""), domain_id
            )

            query = self.get_query_from_cloud_service(cloud_service_info, plugin_info)

            list_metric_params.append(
                {
                    "schema": secret.get("schema_id"),
                    "options": plugin_info.options,
                    "secret_data": secret_data,
                    "query": query,
                    "cloud_service_id": cloud_service_info["cloud_service_id"],
                    "data_source_vo": data_source_vo,
                    "domain_id": domain_id,
                    "metrics_dict": metrics_dict,
                    "and_metric_keys": and_metric_keys,
                }
            )

        metric_responses = []
        with concurrent.futures.ThreadPoolExecutor(
            max_workers=MAX_CONCURRENT_WORKER
        ) as executor:
            future_executors = []

            for _params in list_metric_params:
                future_executors.append(
                    executor.submit(self.list_metrics_info, _params)
                )

            for future in concurrent.futures.as_completed(future_executors):
                metric_responses.append(future.result())

        response = self._merge_metric(metric_responses, domain_id)
        return response

    @transaction(
        permission="monitoring:Metric.read",
        role_types=["DOMAIN_ADMIN", "WORKSPACE_OWNER", "WORKSPACE_MEMBER"],
    )
    @check_required(
        ["data_source_id", "metric_query", "metric", "start", "end", "domain_id"]
    )
    def get_data(self, params):
        """Get resource's metric data

        Args:
            params (dict): {
                'data_source_id': 'str',    # required
                'metric_query': 'dict',     # required
                'metric': 'str',            # required
                'start': 'str',             # required
                'end': 'str',               # required
                'period': 'int',
                'stat': 'str',
                'domain_id': 'str'          # injected from auth (required)
            }

        Returns:
            metric_data (list)
        """
        data_source_id = params["data_source_id"]
        metric_query = params["metric_query"]
        domain_id = params["domain_id"]

        data_source_vo = self.data_source_mgr.get_data_source(data_source_id, domain_id)
        self._check_data_source_state(data_source_vo)

        plugin_metadata = data_source_vo.plugin_info.metadata
        self._check_plugin_metadata(plugin_metadata, params, data_source_id)
        self.plugin_initialize(data_source_vo)

        response = {"labels": [], "values": {}, "domain_id": domain_id}

        required_keys = plugin_metadata.get("required_keys")
        resource_ids = self.get_resource_ids_from_metric_query(metric_query)
        resources_info = self.inventory_mgr.list_resources(
            resource_ids, required_keys, domain_id
        )
        resources_chunks = self.list_chunk_resources(
            resources_info, metric_query, data_source_vo, domain_id
        )

        _LOGGER.debug(f"[get_data] chunk_resources: {resources_chunks}")

        metric_data_params = self.set_metric_data_params(params)

        for chunk_resources in resources_chunks.values():
            metric_data_params.update(
                {
                    "secret_data": chunk_resources.get("secret_data"),
                    "metric_query": chunk_resources.get("metric_query"),
                    "options": chunk_resources.get("options", {}),
                }
            )

            if "schema" in chunk_resources:
                metric_data_params.update({"schema": chunk_resources.get("schema")})

            metric_data_response = self.get_metric_data(metric_data_params)

            if not response["labels"] and metric_data_response.get("labels", []):
                response["labels"] = metric_data_response["labels"]

            if values := metric_data_response.get("values"):
                response["values"].update(values)

        return response

    def list_chunk_resources(self, resources, metric_query, data_source_vo, domain_id):
        """
        chunk_resources(dict): {
            'provider.region_code.secret_id.random_key': {
                'schema': 'str',
                'secret_data': 'dict',
                'region_name': 'str',
                'options': 'dict',
                'metric_query': 'dict'     # MAX_REQUEST_LIMIT
            },
            ...
        }
        """
        chunk_resources = {}
        provider = data_source_vo.plugin_info["provider"]

        for resource in resources:
            cloud_service_id = resource["cloud_service_id"]
            if cloud_service_id in metric_query:
                region_code = self.get_region_from_resource(resource)
                secret = self.secret_mgr.get_secret_from_resource(
                    resource, data_source_vo, domain_id
                )
                chunk_key = self._generate_chunk_key(
                    provider, region_code, secret.get("secret_id", "")
                )

                metric_query[cloud_service_id]["region_name"] = region_code
                chunk_resource = {cloud_service_id: metric_query[cloud_service_id]}

                if chunk_key in chunk_resources:
                    chunk_info = chunk_resources[chunk_key]

                    if "region_name" not in chunk_info:
                        chunk_info.update({"region_name": region_code})

                    if "secret_data" not in chunk_info:
                        chunk_info.update(
                            {
                                "secret_data": self.secret_mgr.get_secret_data(
                                    secret["secret_id"], domain_id
                                )
                            }
                        )

                    if "schema" not in chunk_info:
                        chunk_info.update({"schema": secret.get("schema_id")})

                    _metric_query = chunk_info["metric_query"]

                    if len(_metric_query) >= MAX_REQUEST_LIMIT:
                        new_chunk_key = f"{chunk_key}.{random_string(1)}"
                        # move chunk to new key
                        chunk_resources[new_chunk_key] = chunk_info
                        # generate a new chunk
                        chunk_resources[chunk_key] = {
                            "region_name": region_code,
                            "secret_data": self.secret_mgr.get_secret_data(
                                secret["secret_id"], domain_id
                            ),
                            "metric_query": chunk_resource,
                        }
                    else:
                        _metric_query[cloud_service_id] = metric_query[cloud_service_id]
                else:
                    # generate a new chunk
                    chunk_resources[chunk_key] = {
                        "region_name": region_code,
                        "secret_data": self.secret_mgr.get_secret_data(
                            secret["secret_id"], domain_id
                        ),
                        "metric_query": chunk_resource,
                    }

        return chunk_resources

    def plugin_initialize(self, data_source_vo):
        endpoint = self.ds_plugin_mgr.get_data_source_plugin_endpoint_by_vo(
            data_source_vo
        )
        self.ds_plugin_mgr.initialize(endpoint)

    def get_metric_data(self, params):
        metric_data_info = {"labels": [], "values": {}}

        try:
            metric_data_info = self.ds_plugin_mgr.get_metric_data(params)
        except Exception as e:
            print(e)

        return metric_data_info

    def list_metrics_info(self, params):
        schema = params.get("schema")
        secret_data = params["secret_data"]
        cloud_service_id = params["cloud_service_id"]
        query = params.get("query", {})
        options = params.get("options", {})

        try:
            metrics_info = self.ds_plugin_mgr.list_metrics(
                schema, options, secret_data, query
            )
            _LOGGER.debug(f"[list_metric_info] metrics_info: {metrics_info}")
            return {
                "metrics": metrics_info.get("metrics", []),
                "resource_id": cloud_service_id,
            }
            # return response
        except Exception as e:
            _LOGGER.error(f"[list_metrics]: {e}")
            return {}

    def _merge_metric(self, metric_responses, domain_id):
        response = {"metrics": [], "available_resources": {}, "domain_id": domain_id}

        (
            available_resources,
            metric_responses,
            cloud_service_metric_set_info,
        ) = self.set_preload_metric(metric_responses)

        _LOGGER.debug(f"available_resources: {available_resources}")
        _LOGGER.debug(f"metric_responses: {metric_responses}")
        _LOGGER.debug(f"cloud_service_metric_set_info: {cloud_service_metric_set_info}")

        if len(metric_responses) == 1:
            response["metrics"] = metric_responses[0]["metrics"]
        elif len(metric_responses) > 1:
            _intersected_metric_keys = self._intersection_metric_keys(
                cloud_service_metric_set_info
            )

            response_metrics = []
            for metric_response in metric_responses:
                for _metric in metric_response["metrics"]:
                    if _metric["key"] in _intersected_metric_keys:
                        match_key = False
                        for response_metric in response_metrics:
                            if response_metric["key"] == _metric["key"]:
                                response_metric["metric_query"].update(
                                    _metric["metric_query"]
                                )
                                match_key = True
                                break

                        if match_key is False:
                            response_metrics.append(_metric)

            response["metrics"] = response_metrics

        response["available_resources"] = available_resources

        _LOGGER.debug(response)
        return response

    @staticmethod
    def set_preload_metric(metric_responses):
        available_resources = {}
        cloud_service_metric_set_info = {}

        for metric_response in metric_responses:
            if cloud_service_id := metric_response.get("resource_id"):
                available_resources[cloud_service_id] = True

                _metric_keys = []
                for _metric in metric_response["metrics"]:
                    _metric_keys.append(_metric["key"])

                    _metric_query = _metric["metric_query"]
                    _metric["metric_query"] = {cloud_service_id: _metric_query}

                cloud_service_metric_set_info[cloud_service_id] = _metric_keys

        return available_resources, metric_responses, cloud_service_metric_set_info

    @staticmethod
    def _intersection_metric_keys(cloud_service_metric_set_info):
        target_metrics = []

        for metrics in cloud_service_metric_set_info.values():
            if metrics and target_metrics:
                target_metrics = list(set(metrics) & set(target_metrics))

            if metrics and not target_metrics:
                target_metrics = metrics

        return target_metrics

    @staticmethod
    def get_resource_ids_from_metric_query(metric_query):
        return [resource_id for resource_id in metric_query.keys()]

    @staticmethod
    def set_metric_data_params(params):
        return_params = {
            "metric": params["metric"],
            "start": params["start"],
            "end": params["end"],
        }

        if "period" in params:
            return_params["period"] = params["period"]

        if "stat" in params:
            return_params["stat"] = params["stat"]

        return return_params

    @staticmethod
    def _generate_chunk_key(provider, region_code, secret_id):
        return f"{provider}.{region_code}.{secret_id}"

    @staticmethod
    def get_region_from_resource(resource):
        return resource.get("region_code", "")

    @staticmethod
    def get_account_from_resource(resource):
        return resource.get("account", "")

    @staticmethod
    def _check_data_source_state(data_source_vo):
        if data_source_vo.state == "DISABLED":
            raise ERROR_DATA_SOURCE_STATE_DISABLED(
                data_source_id=data_source_vo.data_source_id
            )

    @staticmethod
    def _check_resource_type(plugin_metadata, resource_type):
        supported_resource_type = plugin_metadata["supported_resource_type"]

        if resource_type not in supported_resource_type:
            raise ERROR_NOT_SUPPORT_RESOURCE_TYPE(
                supported_resource_type=supported_resource_type
            )

    @staticmethod
    def _check_plugin_metadata(metadata, params, data_source_id):
        if "supported_stat" in metadata and "stat" in params:
            if params["stat"] not in metadata["supported_stat"]:
                raise ERROR_NOT_SUPPORT_METRIC_STAT(stat=params["stat"])

        if "required_keys" in metadata:
            if not metadata["required_keys"]:
                raise ERROR_NOT_FOUND_REQUIRED_KEY(data_source_id=data_source_id)
        else:
            raise ERROR_NOT_FOUND_REQUIRED_KEY(data_source_id=data_source_id)

    @staticmethod
    def _merge_metric_keys(metrics_info, metrics_dict, and_metric_keys):
        for metric_info in metrics_info.get("metrics", []):
            if "key" in metric_info:
                metric_key = metric_info["key"]

                if metric_key not in and_metric_keys:
                    metrics_dict[metric_key] = metric_info
                    and_metric_keys.append(metric_key)

        return {"metrics_dict": metrics_dict, "and_metric_keys": and_metric_keys}

    @staticmethod
    def get_required_key(metadata):
        required_keys = metadata.get("required_keys", [])

        if not required_keys:
            raise

        return required_keys[0]

    @staticmethod
    def get_query_from_cloud_service(cloud_service_info, plugin_info):
        metadata = plugin_info.metadata
        required_keys = metadata.get("required_keys", [])

        if required_keys:
            query_key = required_keys[0]
            return get_dict_value(cloud_service_info, query_key, default_value={})
        else:
            raise ERROR_REQUIRED_KEYS_NOT_EXISTS(plugin_id=plugin_info.plugin_id)
