"""
Tasks for ESI status monitoring.
"""

# Standard Library
import datetime
import json
from typing import Any

# Third Party
import requests
from celery import shared_task

# Alliance Auth
from allianceauth.services.hooks import get_extension_logger

# AA ESI Status
from esistatus import __user_agent__
from esistatus.constants import ESIMetaUrl
from esistatus.models import EsiStatus
from esistatus.providers.applogger import AppLogger
from esistatus.providers.cache import Cache

logger = AppLogger(my_logger=get_extension_logger(__name__))

request_headers = {"User-Agent": __user_agent__}


def _get_latest_compatibility_date() -> str | None:
    """
    Retrieve the latest ESI compatibility date.

    :return:
    :rtype:
    """

    logger.debug("Retrieving latest ESI compatibility date.")

    url = ESIMetaUrl.COMPATIBILITY_DATES.value
    cache_subkey = "compatibility-dates:latest"
    cached = Cache(subkey=cache_subkey).get()

    if cached:
        logger.debug(f"Using cached ESI compatibility date: {cached}")

        return cached

    try:
        response = requests.get(url=url, headers=request_headers, timeout=10)
        response.raise_for_status()

        dates = response.json().get("compatibility_dates", [])

        logger.debug(f"ESI compatibility dates response: {dates}")

        valid_dates = []

        for d in dates:
            if not isinstance(d, str):
                continue

            try:
                valid_dates.append(datetime.datetime.strptime(d, "%Y-%m-%d").date())
            except ValueError:
                logger.debug(f"Skipping invalid compatibility date: {d}")

                continue

        if not valid_dates:
            logger.debug("No valid ESI compatibility dates found.")

            return None

        latest = max(valid_dates).isoformat()

        logger.debug(f"Latest ESI compatibility date: {latest}")

        Cache(subkey=cache_subkey).set(value=latest)

        return latest
    except (requests.exceptions.RequestException, json.JSONDecodeError) as exc:
        logger.debug(f"Error retrieving ESI compatibility dates: {exc}")

        return None


def _get_esi_status_json(compatibility_date: str) -> dict | None:
    """
    Retrieve the ESI status JSON for a given compatibility date.

    :param compatibility_date:
    :type compatibility_date:
    :return:
    :rtype:
    """

    try:
        # Use a copy of the base headers and add X-Compatibility-Date without mutating the module-level dict
        headers = {**request_headers, "X-Compatibility-Date": compatibility_date}
        response = requests.get(
            url=ESIMetaUrl.STATUS.value, headers=headers, timeout=10
        )
        response.raise_for_status()
        esi_status = response.json()

        logger.info(
            f"ESI status fetched successfully for for compatibility date: {compatibility_date}."
        )

        return esi_status
    except requests.exceptions.RequestException as exc:
        resp = getattr(exc, "response", None)
        error_str = (
            f"{resp.status_code} - {resp.reason}" if resp is not None else str(exc)
        )

        logger.error(f"Unable to update ESI status. Error: {error_str}")

        return None
    except json.JSONDecodeError:
        logger.error("Unable to update ESI status. ESI returned invalid JSON.")

        return None


def _get_openapi_specs_json(compatibility_date: str) -> dict | None:
    """
    Retrieve the ESI OpenAPI specs JSON for a given compatibility date.

    :param compatibility_date:
    :type compatibility_date:
    :return:
    :rtype:
    """

    cacke_subkey = f"openapi:{compatibility_date}"
    cached = Cache(subkey=cacke_subkey).get()

    if cached:
        logger.debug(
            msg=f"Using cached ESI OpenAPI specs for compatibility date: {compatibility_date}."
        )

        return cached

    try:
        # Use a copy of the base headers and add X-Compatibility-Date without mutating the module-level dict
        headers = {**request_headers, "X-Compatibility-Date": compatibility_date}
        response = requests.get(
            url=ESIMetaUrl.OPENAPI_SPECS.value, headers=headers, timeout=10
        )
        response.raise_for_status()
        openapi_specs = response.json()

        logger.info(
            f"ESI OpenAPI specs fetched successfully for compatibility date: {compatibility_date}."
        )

        Cache(subkey=cacke_subkey).set(value=openapi_specs)

        return openapi_specs
    except requests.exceptions.RequestException as exc:
        resp = getattr(exc, "response", None)
        error_str = (
            f"{resp.status_code} - {resp.reason}" if resp is not None else str(exc)
        )

        logger.error(f"Unable to fetch ESI OpenAPI specs. Error: {error_str}")

        return None
    except json.JSONDecodeError:
        logger.error("Unable to fetch ESI OpenAPI specs. ESI returned invalid JSON.")

        return None


def _enrich_status_json(status: dict[str, Any], openapi: dict[str, Any]) -> list[Any]:
    """
    Enrich ESI status routes with description, operation_id, summary and tags from OpenAPI specs.

    Inspired by this script by CCP Pinky:
    https://gist.github.com/ccp-pinky/28e60a5a79df5f7db4f7f46704c9f818

    :param status:
    :type status:
    :param openapi:
    :type openapi:
    :return:
    :rtype:
    """

    for route in status["routes"]:
        spec = openapi["paths"].get(route["path"], {}).get(route["method"].lower(), {})

        route["description"] = spec.get("description", None)
        route["operation_id"] = spec.get("operationId", None)
        route["summary"] = spec.get("summary", None)
        route["tags"] = spec.get("tags", ["Deprecated"])

    return status["routes"]


def _append_value(dict_obj: dict, key: str, value: Any) -> None:
    """
    Append a value to a key in a dictionary

    :param dict_obj: The dictionary
    :type dict_obj: Dict
    :param key: The key
    :type key: str
    :param value: The value
    :type value: Any
    :return: None
    :rtype: None
    """

    dict_obj.setdefault(key, [])

    if not isinstance(dict_obj[key], list):
        dict_obj[key] = [dict_obj[key]]

    dict_obj[key].append(value)


def _esi_endpoint_status_from_json(esi_endpoint_json: list) -> dict:
    """
    Get the ESI endpoint status from the ESI json

    :param esi_endpoint_json: The ESI endpoint json
    :type esi_endpoint_json: dict
    :return: The ESI endpoint status
    :rtype: dict
    """

    esi_endpoint_status = {
        "Unknown": {"endpoints": {}, "count": 0, "percentage": "0.00%"},
        "OK": {"endpoints": {}, "count": 0, "percentage": "0.00%"},
        "Degraded": {"endpoints": {}, "count": 0, "percentage": "0.00%"},
        "Down": {"endpoints": {}, "count": 0, "percentage": "0.00%"},
        "Recovering": {"endpoints": {}, "count": 0, "percentage": "0.00%"},
    }

    for esi_endpoint in esi_endpoint_json:
        status = esi_endpoint["status"]

        _append_value(
            dict_obj=esi_endpoint_status[status]["endpoints"],
            key=esi_endpoint["tags"][0],
            value={
                "path": esi_endpoint["path"],
                "method": esi_endpoint["method"].upper(),
                "operation_id": esi_endpoint["operation_id"],
                "summary": esi_endpoint["summary"],
                "description": esi_endpoint["description"],
            },
        )

        esi_endpoint_status[status]["count"] += 1

    # Compute total endpoints, then sort endpoints and calculate percentages in one pass
    endpoints_total = sum(
        status_data.get("count", 0) for status_data in esi_endpoint_status.values()
    )

    for status_data in esi_endpoint_status.values():
        status_data["endpoints"] = dict(sorted(status_data["endpoints"].items()))
        percentage = (
            (status_data["count"] / endpoints_total * 100)
            if endpoints_total > 0 and status_data["count"] > 0
            else 0
        )
        status_data["percentage"] = f"{percentage:.2f}%"

    # Return the whole jazz
    return {"total_endpoints": endpoints_total, "esi_status": esi_endpoint_status}


@shared_task()
def update_esi_status():
    """
    Task to update ESI status.
    """

    logger.debug("Starting ESI status update task.")

    latest_compatibility_date = _get_latest_compatibility_date()

    if latest_compatibility_date is None:
        logger.error("Failed to retrieve latest compatibility date.")

        return

    esi_status = _get_esi_status_json(compatibility_date=latest_compatibility_date)
    openapi_specs = _get_openapi_specs_json(
        compatibility_date=latest_compatibility_date
    )

    if esi_status is None or openapi_specs is None:
        logger.error("Failed to retrieve ESI status or OpenAPI specs.")

        return

    enriched_status = _enrich_status_json(status=esi_status, openapi=openapi_specs)

    if not any(route.get("tags") for route in enriched_status):
        logger.debug("Enriched ESI status has no tags. Skipping database update.")

        return

    esi_status_data = _esi_endpoint_status_from_json(esi_endpoint_json=enriched_status)

    EsiStatus.objects.update_or_create(
        pk=1,
        defaults={
            "compatibility_date": latest_compatibility_date,
            "status_data": esi_status_data.get("esi_status", {}),
            "total_endpoints": esi_status_data.get("total_endpoints", 0),
        },
    )

    logger.info(
        f"ESI status updated in database for compatibility date: {latest_compatibility_date}."
    )
