"""Async client for the UK Carbon Intensity API."""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Self

import aiohttp

from .exceptions import (
    CarbonIntensityConnectionError,
    CarbonIntensityError,
    CarbonIntensityNoDataError,
    CarbonIntensityTimeoutError,
)
from .models import (
    GenerationFuel,
    Intensity,
    IntensityPeriod,
    NationalGenerationMix,
    NationalIntensity,
    RegionalData,
)

BASE_URL = "https://api.carbonintensity.org.uk"
REQUEST_TIMEOUT = aiohttp.ClientTimeout(total=10)


class CarbonIntensityClient:
    """Async client for the UK Carbon Intensity API.

    Can be used as a context manager or standalone. When used standalone,
    pass an existing aiohttp.ClientSession to avoid creating a new one.
    """

    def __init__(self, session: aiohttp.ClientSession | None = None) -> None:
        """Initialize the client."""
        self._session = session
        self._owns_session = session is None

    async def __aenter__(self) -> Self:
        """Enter async context manager."""
        if self._owns_session:
            self._session = aiohttp.ClientSession(timeout=REQUEST_TIMEOUT)
        return self

    async def __aexit__(self, *args: object) -> None:
        """Exit async context manager."""
        if self._owns_session and self._session:
            await self._session.close()
            self._session = None

    async def close(self) -> None:
        """Close the session if we own it."""
        if self._owns_session and self._session:
            await self._session.close()
            self._session = None

    async def _get(self, path: str) -> dict:
        """Make a GET request to the API."""
        if self._session is None:
            msg = "Session not initialized. Use as context manager or pass a session."
            raise CarbonIntensityError(msg)

        url = f"{BASE_URL}{path}"
        try:
            async with self._session.get(url, timeout=REQUEST_TIMEOUT) as resp:
                if resp.status != 200:
                    msg = f"API returned status {resp.status}"
                    raise CarbonIntensityError(msg)
                return await resp.json()
        except TimeoutError as err:
            msg = f"Timeout connecting to {url}"
            raise CarbonIntensityTimeoutError(msg) from err
        except aiohttp.ClientError as err:
            msg = f"Error connecting to {url}"
            raise CarbonIntensityConnectionError(msg) from err

    @staticmethod
    def _parse_datetime(value: str) -> datetime:
        """Parse an ISO datetime string from the API."""
        return datetime.fromisoformat(value.replace("Z", "+00:00"))

    @staticmethod
    def _parse_intensity(data: dict) -> Intensity:
        """Parse intensity data."""
        return Intensity(
            forecast=data["forecast"],
            index=data["index"],
            actual=data.get("actual"),
        )

    @staticmethod
    def _parse_generationmix(data: list[dict]) -> list[GenerationFuel]:
        """Parse generation mix data."""
        return [GenerationFuel(fuel=item["fuel"], perc=item["perc"]) for item in data]

    def _parse_period(self, data: dict) -> IntensityPeriod:
        """Parse a single intensity period."""
        return IntensityPeriod(
            from_time=self._parse_datetime(data["from"]),
            to_time=self._parse_datetime(data["to"]),
            intensity=self._parse_intensity(data["intensity"]),
            generationmix=self._parse_generationmix(data["generationmix"]),
        )

    async def get_regional_intensity(self, postcode: str) -> RegionalData:
        """Get current regional carbon intensity by postcode.

        Args:
            postcode: UK outward postcode (e.g. "DE45", "SW1", "RG41").

        Returns:
            Regional carbon intensity data including generation mix.

        Raises:
            CarbonIntensityNoDataError: If no data is available for the postcode.
        """
        result = await self._get(f"/regional/postcode/{postcode}")
        data_list = result.get("data", [])
        if not data_list:
            msg = f"No data available for postcode {postcode}"
            raise CarbonIntensityNoDataError(msg)

        region = data_list[0]
        periods = [self._parse_period(p) for p in region.get("data", [])]
        if not periods:
            msg = f"No intensity periods available for postcode {postcode}"
            raise CarbonIntensityNoDataError(msg)

        return RegionalData(
            regionid=region["regionid"],
            dnoregion=region["dnoregion"],
            shortname=region["shortname"],
            postcode=region.get("postcode", postcode),
            periods=periods,
        )

    async def get_regional_forecast(
        self, postcode: str, hours: int = 24
    ) -> RegionalData:
        """Get regional carbon intensity forecast.

        Args:
            postcode: UK outward postcode (e.g. "DE45", "SW1", "RG41").
            hours: Forecast window - 24 or 48 hours.

        Returns:
            Regional forecast data with multiple half-hour periods.
        """
        now = datetime.now(UTC).strftime("%Y-%m-%dT%H:%MZ")
        result = await self._get(
            f"/regional/intensity/{now}/fw{hours}h/postcode/{postcode}"
        )

        region = result.get("data", {})
        if not region:
            msg = f"No forecast data available for postcode {postcode}"
            raise CarbonIntensityNoDataError(msg)

        periods = [self._parse_period(p) for p in region.get("data", [])]
        if not periods:
            msg = f"No forecast periods available for postcode {postcode}"
            raise CarbonIntensityNoDataError(msg)

        return RegionalData(
            regionid=region["regionid"],
            dnoregion=region["dnoregion"],
            shortname=region["shortname"],
            postcode=region.get("postcode", postcode),
            periods=periods,
        )

    async def get_national_intensity(self) -> NationalIntensity:
        """Get current national carbon intensity.

        Returns:
            National carbon intensity with forecast and actual values.
        """
        result = await self._get("/intensity")
        data_list = result.get("data", [])
        if not data_list:
            msg = "No national intensity data available"
            raise CarbonIntensityNoDataError(msg)

        entry = data_list[0]
        return NationalIntensity(
            from_time=self._parse_datetime(entry["from"]),
            to_time=self._parse_datetime(entry["to"]),
            intensity=self._parse_intensity(entry["intensity"]),
        )

    async def get_generation_mix(self) -> NationalGenerationMix:
        """Get current national generation mix.

        Returns:
            National generation mix breakdown by fuel type.
        """
        result = await self._get("/generation")
        data = result.get("data", {})
        if not data:
            msg = "No generation mix data available"
            raise CarbonIntensityNoDataError(msg)

        return NationalGenerationMix(
            from_time=self._parse_datetime(data["from"]),
            to_time=self._parse_datetime(data["to"]),
            generationmix=self._parse_generationmix(data["generationmix"]),
        )
