# Copyright (c) 2020 CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com
import json
import zlib
from datetime import UTC, datetime, timedelta
from decimal import Decimal

import base58
import czid.czrn as czrn
from botocore.exceptions import ClientError
from toolz.dicttoolz import get_in

from src.constants import SECONDS_PER_HOUR, common_boto3_client_config
from src.utils.console import eprint
from src.utils.data import chunks, generate_random_string
from src.utils.time import utcdatetimenow

non_alpha_table = str.maketrans(dict.fromkeys("".join(c for c in map(chr, range(256)) if not c.isalnum())))
METRIC_STATISTICS = ["Maximum", "Average"]
METRICS_CONFIG = {
    "s3": {"bucket": {"name": "BucketName", "namespace": "AWS/S3"}},
    "ec2": {
        "instance": {"name": "InstanceId", "namespace": "AWS/EC2"},
        "volume": {"name": "VolumeId", "namespace": "AWS/EBS"},
    },
    "rds": {
        "instance": {"name": "DBInstanceIdentifier", "namespace": "AWS/RDS"},
        "cluster": {"name": "DBClusterIdentifier", "namespace": "AWS/RDS"},
    },
    "elasticache": {"cluster": {"name": "CacheClusterId"}},
    "ecs": {
        "service": {"name": "ServiceName", "namespace": "AWS/ECS"},
        "cluster": {"name": "ClusterName", "namespace": "AWS/ECS"},
    },
}
accounts_with_errors = []


def get_cwm_v2_for_czrn(czrn_target, session, days=1):
    service, region, cloud_account_id, resource_type, cloud_local_id = czrn.extract_all_components(czrn_target)
    metric_requests = []
    metric_responses = []

    try:
        if region == "cross-region":
            region = "us-east-1"

        cloudwatch = session.client(service_name="cloudwatch", region_name=region, config=common_boto3_client_config())

        metric_resource_name = get_in([service, resource_type, "name"], METRICS_CONFIG, None)
        if not metric_resource_name:
            eprint(
                {"czrn": czrn_target, "message": "No METRICS_CONFIG for target CZRN, unable to get CloudWatch metrics"}
            )
            return []
        dimensions = [{"Name": metric_resource_name, "Value": cloud_local_id}]
        metrics_list = cloudwatch.list_metrics(Dimensions=dimensions).get("Metrics", [])
        # logger.debug({"czrn": czrn_target,
        #               "metrics": metrics_list,
        #               "dimensions": dimensions,
        #               "metric_name": f"{service}.{metric_resource_name}"})

        if metrics_list:
            metric_requests = create_metric_request_v2(metrics_list, period=SECONDS_PER_HOUR)
            # logger.debug({"cwm_request_count": len(metric_requests),
            #               "metric_requests": metric_requests,
            #               "metric_name": f"{service}.{metric_resource_name}",
            #               "czrn": czrn_target})
            utcdatetime = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
            for chunk in chunks(metric_requests, 100):
                response = cloudwatch.get_metric_data(
                    MetricDataQueries=chunk, StartTime=utcdatetime - timedelta(days=days), EndTime=utcdatetime
                )
                metric_responses += response.get("MetricDataResults")
        #         logger.debug({"get_metric_messages": response.get('Messages')})
        #     logger.debug({"metric_responses": metric_responses})
        #     logger.info(f"Received {len(metric_responses)} metrics for {czrn_target}")
        # else:
        #     logger.info(f"No metrics found for {czrn_target}")

    except ClientError as error:
        msg = {
            "error": str(error),
            "function": "get_cwm_for_czrn",
            "input": {"czrn_target": czrn_target, "days": days},
            "activity": "cloudwatch.get_metric_data",
            "cloud_account_id": cloud_account_id,
        }
        if "AccessDenied" in str(error):
            msg["error_code"] = "access_denied"
            accounts_with_errors.append(cloud_account_id)
        elif "Throttling" in str(error):
            msg["error_code"] = "throttled"
            eprint(msg)
            # raise to re-try later
            raise
        else:
            msg["error_code"] = "unexpected_client_error"
        eprint(msg)

    except Exception as error:
        eprint({"error": str(error), "czrn_target": czrn_target, "metric_requests": metric_requests})

    return metric_responses


def create_metric_request_v2(metrics, period) -> list:
    """
    [{
      "Namespace": "AWS/S3",
      "MetricName": "BucketSizeBytes",
      "Dimensions": [
        {
          "Name": "StorageType",
          "Value": "StandardStorage"
        },
        {
          "Name": "BucketName",
          "Value": "cz-master-billing"
        }
      ]
    }]

    Args:
        metrics:
        period:

    Returns:
        list
    """

    metric_requests = []
    for metric in metrics:
        for statistic in METRIC_STATISTICS:
            metric_requests.append(_metric_request_format(metric, period, statistic))
        #
        # if len(metric['Dimensions']) == 1:
        #     metric_requests.append(__metric_request_format(metric, period, "Average"))
        # else:
        #     for statistic in METRIC_STATISTICS:
        #         metric_requests.append(__metric_request_format(metric, period, statistic))

    return metric_requests


def _metric_request_format(metric, period, statistic):
    metric_dimensions = get_metric_dimensions(metric["Dimensions"])
    metric_id = encode_metric_id(metric_dimensions, statistic, metric["MetricName"])
    return {
        "Id": metric_id,
        "Label": f"{metric['MetricName']}.{statistic}",
        "MetricStat": {
            "Metric": metric,
            "Period": period,
            "Stat": statistic,
            # "Unit": metric_type['unit']
        },
    }


def get_metric_dimensions(dimensions) -> dict:
    """
    Input:
        [
          {
            'Name': 'PodName',
            'Value': 'coredns'
          },
          {
            'Name': 'ClusterName',
            'Value': 'engineering-research'
          },
          {
            'Name': 'Namespace',
            'Value': 'kube-system'
          }
        ]

    Output:
        {'PodName': 'coredns', 'ClusterName': 'engineering-research', 'Namespace': 'kube-system'}

    Args:
        dimensions (list):

    Returns:
        (dict) :

    """
    return {dimension["Name"]: dimension["Value"] for dimension in dimensions}


def format_raw_cwm_metrics_as_resource_metrics(cz_org_id, cz_account_id, czrn_string, metric):
    """
    {
            'Id': 'metric_BucketSizeBytes_Minimum_BucketName',
            'Label': 'StandardStorage BucketSizeBytes Minimum',
            'Timestamps': [datetime.datetime(2020, 4, 29, 0, 0, tzinfo=tzlocal())],
            'Values': [151255356109.0],
            'StatusCode': 'Complete'
    }
    Args:
        czrn_string:
        cz_account_id:
        cz_org_id (str):
        metric (dict):

    Returns:
        list
    """
    service, region, cloud_account_id, resource_type, cloud_local_id = czrn.fast_extract_components(czrn_string)
    metric_unit = "UNKNOWN"

    resource_events = []
    for x in range(len(metric["Timestamps"])):
        resource_events.append(
            {
                "czrn": czrn_string,
                "cz_organization_id": cz_org_id,
                "cz_account_id": cz_account_id,
                "cloud_local_id": cloud_local_id,
                "cloud_account_id": cloud_account_id,
                "connection_region": region,
                "cloud_resource_type": resource_type,
                "cloud_service_type": service,
                "received": utcdatetimenow(),
                "metric_datetime": metric["Timestamps"][x].astimezone(tz=UTC),
                "metric_name": metric["Label"],
                "metric_value": Decimal(str(metric["Values"][x])),
                "metric_unit": metric_unit,
                "metric_label": metric["Label"],
                "metric_id": metric["Id"],
                "tags": {},
                "properties": {},
                "source": "cz.cwm",
            }
        )

    return resource_events


def decode_metric_id(id_string):
    encoded_metric_dimensions = id_string[2:]
    result = json.loads(zlib.decompress(base58.b58decode(encoded_metric_dimensions)))
    result.pop("R", None)
    return result


def encode_metric_id(_dimensions, statistic, metric_name):
    """
    There are rules for the metric id, it must start with a letter,
    be alphanum, must be unique and be less that 255 characters

    To achieve minimum size we use Base58 encoding in addition to zlib compression

    Args:
        dimensions:
        statistic:
        metric_name:

    Returns:

    """
    random_string = generate_random_string(4)
    metric_id = {"R": random_string, "S": statistic, "M": metric_name}
    # 'D': dimensions}

    encoded_metric_id = base58.b58encode(zlib.compress(json.dumps(metric_id).replace(" ", "").encode(), 9)).decode()
    final_metric_id = f"m_{encoded_metric_id}"
    if len(encoded_metric_id) > 255:
        raise Exception(
            f"Metric ID is too long (>255, actual is {len(encoded_metric_id)}). Trying to encode {metric_id}"
        )
    return final_metric_id
