import os
import time
from collections import OrderedDict

import orjson
import requests

from efootprint.logger import logger


_BOAVIZTAPI_CACHE_MAX_BYTES = 50 * 1024 * 1024
_BOAVIZTAPI_CACHE_TTL_SECONDS = 7 * 24 * 60 * 60
_boaviztapi_cache = OrderedDict()
_boaviztapi_cache_size_bytes = 0


def _make_cache_key(url, method, params, json_payload):
    cache_payload = {"url": url, "method": method, "params": params or {}, "json": json_payload}
    return orjson.dumps(cache_payload, option=orjson.OPT_SORT_KEYS, default=str)


def _estimate_cache_entry_size(cache_key, value):
    try:
        value_bytes = len(orjson.dumps(value, option=orjson.OPT_SORT_KEYS, default=str))
    except TypeError:
        value_bytes = len(repr(value).encode("utf-8"))
    return len(cache_key) + value_bytes


def _cache_get(cache_key):
    entry = _boaviztapi_cache.get(cache_key)
    if entry is None:
        return None
    value, entry_size, expires_at = entry
    if expires_at <= time.time():
        _boaviztapi_cache.pop(cache_key, None)
        global _boaviztapi_cache_size_bytes
        _boaviztapi_cache_size_bytes -= entry_size
        return None
    return value


def _cache_set(cache_key, value):
    global _boaviztapi_cache_size_bytes
    entry_size = _estimate_cache_entry_size(cache_key, value)
    if entry_size > _BOAVIZTAPI_CACHE_MAX_BYTES:
        return
    now = time.time()
    for key, (_, old_size, expires_at) in list(_boaviztapi_cache.items()):
        if expires_at <= now:
            _boaviztapi_cache.pop(key, None)
            _boaviztapi_cache_size_bytes -= old_size
    while _boaviztapi_cache and (_boaviztapi_cache_size_bytes + entry_size) > _BOAVIZTAPI_CACHE_MAX_BYTES:
        _, (_, old_size, _) = _boaviztapi_cache.popitem(last=False)
        _boaviztapi_cache_size_bytes -= old_size
    _boaviztapi_cache[cache_key] = (value, entry_size, now + _BOAVIZTAPI_CACHE_TTL_SECONDS)
    _boaviztapi_cache_size_bytes += entry_size


def call_boaviztapi(url, method="GET", params=None, json=None):
    if os.getenv("USE_BOAVIZTAPI_PACKAGE"):
        return call_boaviztapi_from_package_dependency(url, method, params, json)
    cache_key = _make_cache_key(url, method, params, json)
    cached_response = _cache_get(cache_key)
    if cached_response is not None:
        logger.info(f"Fetched {method} {url} params {params} Boavizta API response from cache.")
        return cached_response
    try:
        response = call_boaviztapi_from_web_request(url, method, params, json)
        _cache_set(cache_key, response)
        return response
    except Exception as e:
        logger.warning(f"Boavizta API call failed with error {e}. Trying to call Boavizta API via package dependency.")
        response = call_boaviztapi_from_package_dependency(url, method, params, json)
        _cache_set(cache_key, response)
        return response


def call_boaviztapi_from_web_request(url, method="GET", params=None, json=None):
    params = params or {}
    from time import perf_counter
    start = perf_counter()
    headers = {'accept': 'application/json'}
    response = None
    if method == "GET":
        response = requests.get(url, headers=headers, params=params)
    elif method == "POST":
        headers["Content-Type"] = "application/json"
        response = requests.post(url, headers=headers, params=params, json=json)

    if response.status_code == 200:
        logger.info(f"Called {method} {url} with params {params} in {int((perf_counter() - start) * 1000)} ms.")
        return response.json()
    else:
        raise ValueError(
            f"{method} request to {url} with params {params} failed with status code {response.status_code}")


def call_boaviztapi_from_package_dependency(url, method="GET", params=None, json=None):
    params = params or {}
    import asyncio
    import inspect
    import warnings
    from scipy.optimize import OptimizeWarning

    warnings.simplefilter("ignore", ResourceWarning)
    warnings.simplefilter("ignore", OptimizeWarning)
    warnings.simplefilter("ignore", DeprecationWarning)
    warnings.simplefilter("ignore", UserWarning)

    from boaviztapi.routers.cloud_router import instance_cloud_impact, server_get_all_provider_name, \
        server_get_all_archetype_name as server_get_all_archetype_name_cloud
    from boaviztapi.routers.server_router import (
        server_impact_from_model, server_get_all_archetype_name as server_get_all_archetype_name_server,
        get_archetype_config)

    url_method_mapping = {
        "https://api.boavizta.org/v1/cloud/instance/all_providers": server_get_all_provider_name,
        "https://api.boavizta.org/v1/cloud/instance/all_instances": server_get_all_archetype_name_cloud,
        "https://api.boavizta.org/v1/cloud/instance": instance_cloud_impact,
        "https://api.boavizta.org/v1/server/": server_impact_from_model,
        "https://api.boavizta.org/v1/server/archetypes": server_get_all_archetype_name_server,
        "https://api.boavizta.org/v1/server/archetype_config": get_archetype_config,
    }

    if url in url_method_mapping:
        method = url_method_mapping[url]
        if "criteria" in inspect.signature(method).parameters:
            params["criteria"] = params.get("criteria", ["gwp"])
        return asyncio.run(method(**params))
    else:
        raise ValueError(
            f"URL {url} is not in the list of available urls: {list(url_method_mapping.keys())}. Please provide a valid "
            f"URL, update the list of urls in call_boaviztapi_from_package_dependency")


def print_archetypes_and_their_configs():
    for archetype in call_boaviztapi('https://api.boavizta.org/v1/server/archetypes'):
        config = call_boaviztapi(
            url="https://api.boavizta.org/v1/server/archetype_config", params={"archetype": archetype})
        impact = call_boaviztapi(
            url="https://api.boavizta.org/v1/server/", params={"archetype": archetype})

        if "default" in config['CPU']['core_units']:
            nb_cpu_core_units = config['CPU']['core_units']['default']
        elif "core_units" in impact["verbose"]['CPU-1']:
            nb_cpu_core_units = impact["verbose"]['CPU-1']['core_units']['value']
        else:
            nb_cpu_core_units = 1

        nb_ssd_units = config['SSD']["units"].get('default', 0)
        nb_hdd_units = config['HDD']["units"].get('default', 0)

        if nb_hdd_units > 0 and nb_ssd_units > 0:
            raise ValueError(
                f"Archetype {archetype} has both SSD and HDD, please check and delete this exception raising if ok")
        storage_type = "SSD"
        if nb_hdd_units > 0:
            storage_type = "HDD"
        nb_storage_units = config[storage_type]["units"]['default']

        print(
            f"{archetype}: type {config['CASE']['case_type']['default']},\n"
            f"    {config['CPU']['units']['default']} cpu units with {nb_cpu_core_units} core units,\n"
            f"    {config['RAM']['units']['default']} RAM units with {config['RAM']['capacity']['default']} GB capacity,\n"
            f"    {nb_storage_units} {storage_type} units with {config[storage_type]['capacity']['default']} GB capacity,")

        total_gwp_embedded_value = impact["impacts"]["gwp"]["embedded"]["value"]
        total_gwp_embedded_unit = impact["impacts"]["gwp"]["unit"]

        if nb_storage_units > 0:
            storage_gwp_embedded_value = impact["verbose"][f"{storage_type}-1"]["impacts"]["gwp"]["embedded"]["value"]
            storage_gwp_embedded_unit = impact["verbose"][f"{storage_type}-1"]["impacts"]["gwp"]["unit"]

            assert total_gwp_embedded_unit == storage_gwp_embedded_unit
        else:
            storage_gwp_embedded_value = 0
            storage_gwp_embedded_unit = "kg"

        average_power_value = impact["verbose"]["avg_power"]["value"]
        average_power_unit = impact["verbose"]["avg_power"]["unit"]

        print(
            f"    Impact fabrication compute: {total_gwp_embedded_value - storage_gwp_embedded_value} {total_gwp_embedded_unit},\n"
            f"    Impact fabrication storage: {storage_gwp_embedded_value} {storage_gwp_embedded_unit},\n"
            f"    Average power: {round(average_power_value, 1)} {average_power_unit}\n")


if __name__ == "__main__":
    from time import perf_counter
    start = perf_counter()
    print_archetypes_and_their_configs()
    print(f"Execution time: {perf_counter() - start} seconds")
