"""ESI access helpers for aa-pootest.

The corporation structures endpoint (`GetCorporationsCorporationIdStructures`)
requires the `esi-corporations.read_structures.v1` scope AND a character that
holds the **Director** or **Station Manager** role in the corporation. ESI
enforces the role server-side, so a token from a non-director will return no
data / a 403.

Fuel:
- ESI's structures endpoint exposes the fuel *expiry time* (`fuel_expires`),
  not an absolute fuel quantity. "Current fuel level" is therefore reported as
  the number of days remaining until that expiry.
- "Fuel Required" (to run for 3 months) is derived from the structure's actual
  current burn rate: we read the current fuel-block count from the corp's assets
  (the structure's `StructureFuel` bay) and divide by the hours left until
  `fuel_expires` to get blocks/hour, then scale to 90 days. This reflects the
  structure's real online-service load. Requires
  `esi-assets.read_corporation_assets.v1`.

"Last update" has no per-structure ESI field, so we stamp each row with the
time the data was pulled from ESI.
"""

import datetime as dt
import math

from django.utils.timezone import now

from allianceauth.services.hooks import get_extension_logger

from .providers import esi

logger = get_extension_logger(__name__)

# Location flag of an Upwell structure's fuel bay in the assets endpoint.
FUEL_BAY_FLAG = "StructureFuel"

# Target fuelled duration for the "Fuel Required" figure.
FUEL_TARGET_DAYS = 90

# Remaining-fuel thresholds (days) for the colour-coded card background.
FUEL_CRITICAL_DAYS = 7   # light red below this
FUEL_WARNING_DAYS = 60   # light yellow below this


def _esi_value(obj, key):
    """Read a field from an ESI result that may be a dict or an object."""
    return obj.get(key) if isinstance(obj, dict) else getattr(obj, key, None)


def _to_datetime(value):
    """Coerce an ESI date-time (datetime or ISO string) to an aware datetime."""
    if value is None:
        return None
    if isinstance(value, str):
        try:
            value = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
        except ValueError:
            return None
    if isinstance(value, dt.datetime):
        if value.tzinfo is None:
            value = value.replace(tzinfo=dt.timezone.utc)
        return value
    return None


def _hours_until(fuel_expires):
    """Hours from now until `fuel_expires`, or None if unknown / already passed."""
    expires = _to_datetime(fuel_expires)
    if expires is None:
        return None
    hours = (expires - now()).total_seconds() / 3600
    return hours if hours > 0 else None


def _fuel_display(fuel_expires) -> str:
    """Human-readable fuel / days-remaining string from `fuel_expires`."""
    expires = _to_datetime(fuel_expires)
    if expires is None:
        return "Unknown (no fuel data)"
    days = (expires - now()).total_seconds() / 86400
    stamp = expires.strftime("%Y-%m-%d %H:%M UTC")
    if days <= 0:
        return f"Out of fuel (expired {stamp})"
    return f"{days:.1f} days remaining (expires {stamp})"


def _fuel_required_display(current_fuel, fuel_expires) -> str:
    """Fuel blocks needed to run for FUEL_TARGET_DAYS at the current burn rate.

    Burn rate is derived from the structure's current fuel count and its
    time-to-empty (`fuel_expires`).
    """
    hours_remaining = _hours_until(fuel_expires)
    if not current_fuel or hours_remaining is None:
        return "Unknown (needs current fuel + expiry)"
    rate_per_hour = current_fuel / hours_remaining
    required = math.ceil(rate_per_hour * 24 * FUEL_TARGET_DAYS)
    return f"{required:,} fuel blocks (~{rate_per_hour:.2f}/hr for 3 months)"


def _fuel_css_class(fuel_expires) -> str:
    """Return the CSS background class for a structure's remaining fuel.

    ``pootest-fuel-critical`` (< 7 days, light red),
    ``pootest-fuel-warning`` (< 60 days, light yellow), or ``""`` (ok / unknown).
    The more severe (critical) state takes precedence.
    """
    expires = _to_datetime(fuel_expires)
    if expires is None:
        return ""
    remaining = expires - now()
    if remaining < dt.timedelta(days=FUEL_CRITICAL_DAYS):
        return "pootest-fuel-critical"
    if remaining < dt.timedelta(days=FUEL_WARNING_DAYS):
        return "pootest-fuel-warning"
    return ""


def get_corporation_id_for_character(character_id: int) -> int:
    """Return the corporation ID for a character (public ESI data, no token)."""
    character = esi.client.Character.GetCharactersCharacterId(
        character_id=character_id,
    ).result(use_etag=False)
    return _esi_value(character, "corporation_id")


def fetch_corporation_structures(corporation_id: int, token) -> list:
    """Return details for every structure owned by a corporation.

    Each entry is a dict ready to render as one div per structure, with keys:
    ``system``, ``name``, ``type``, ``fuel``, ``fuel_required`` and
    ``last_update``.
    """
    structures = esi.client.Corporation.GetCorporationsCorporationIdStructures(
        corporation_id=corporation_id,
        token=token,
    ).results(use_etag=False)

    fuel_by_structure = _fuel_quantities_by_structure(corporation_id, token)
    fetched_at = now()
    system_cache: dict = {}
    type_cache: dict = {}

    rows = []
    for structure in structures:
        structure_id = _esi_value(structure, "structure_id")
        fuel_expires = _esi_value(structure, "fuel_expires")
        name = _esi_value(structure, "name") or _resolve_structure_name(
            structure_id, token
        )
        rows.append(
            {
                "system": _resolve_system_name(
                    _esi_value(structure, "system_id"), system_cache
                ),
                "name": name or f"Structure {structure_id}",
                "type": _resolve_type_name(
                    _esi_value(structure, "type_id"), type_cache
                ),
                "fuel": _fuel_display(fuel_expires),
                "fuel_required": _fuel_required_display(
                    fuel_by_structure.get(structure_id), fuel_expires
                ),
                "fuel_class": _fuel_css_class(fuel_expires),
                "last_update": fetched_at,
            }
        )

    return sorted(rows, key=lambda r: (r["system"], r["name"]))


def _fuel_quantities_by_structure(corporation_id: int, token) -> dict:
    """Return {structure_id: total fuel blocks in its fuel bay} from corp assets.

    Needs `esi-assets.read_corporation_assets.v1`. Returns {} on any failure so
    the structures list still renders (fuel required then shows as unknown).
    """
    totals: dict = {}
    try:
        assets = esi.client.Assets.GetCorporationsCorporationIdAssets(
            corporation_id=corporation_id,
            token=token,
        ).results(use_etag=False)
    except Exception as exc:  # noqa: BLE001
        logger.warning("Could not read corp assets for fuel levels: %s", exc)
        return totals

    for asset in assets:
        if _esi_value(asset, "location_flag") == FUEL_BAY_FLAG:
            location_id = _esi_value(asset, "location_id")
            quantity = _esi_value(asset, "quantity") or 0
            totals[location_id] = totals.get(location_id, 0) + quantity
    return totals


def _resolve_structure_name(structure_id, token):
    """Resolve a structure's name via the universe endpoint, or None on error."""
    try:
        info = esi.client.Universe.GetUniverseStructuresStructureId(
            structure_id=structure_id,
            token=token,
        ).result(use_etag=False)
        return _esi_value(info, "name")
    except Exception as exc:  # noqa: BLE001 - stale/offlined ids can 403/404
        logger.warning(
            "Could not resolve name for structure %s: %s", structure_id, exc
        )
        return None


def _resolve_system_name(system_id, cache: dict) -> str:
    """Resolve a solar system name (public ESI), cached per request."""
    if system_id in cache:
        return cache[system_id]
    name = None
    try:
        info = esi.client.Universe.GetUniverseSystemsSystemId(
            system_id=system_id,
        ).result(use_etag=False)
        name = _esi_value(info, "name")
    except Exception as exc:  # noqa: BLE001
        logger.warning("Could not resolve system %s: %s", system_id, exc)
    cache[system_id] = name or f"System {system_id}"
    return cache[system_id]


def _resolve_type_name(type_id, cache: dict) -> str:
    """Resolve an inventory type name (e.g. Fortizar/Raitaru), cached per request."""
    if type_id in cache:
        return cache[type_id]
    name = None
    try:
        info = esi.client.Universe.GetUniverseTypesTypeId(
            type_id=type_id,
        ).result(use_etag=False)
        name = _esi_value(info, "name")
    except Exception as exc:  # noqa: BLE001
        logger.warning("Could not resolve type %s: %s", type_id, exc)
    cache[type_id] = name or f"Type {type_id}"
    return cache[type_id]
