"""Asynchronous client for the PVOutput API."""

from __future__ import annotations

import asyncio
import socket
from dataclasses import dataclass
from datetime import UTC, date, datetime, time
from typing import Any, Self

from aiohttp.client import ClientError, ClientResponseError, ClientSession
from aiohttp.hdrs import METH_GET, METH_POST
from yarl import URL

from .exceptions import (
    PVOutputAuthenticationError,
    PVOutputConnectionError,
    PVOutputError,
    PVOutputNoDataError,
)
from .models import Status, System


@dataclass
class PVOutput:
    """Main class for handling connections with the PVOutput API."""

    api_key: str
    system_id: int

    request_timeout: float = 8.0
    session: ClientSession | None = None

    _close_session: bool = False

    async def _request(
        self,
        uri: str,
        *,
        method: str = METH_GET,
        data: dict[str, Any] | None = None,
    ) -> str:
        """Handle a request to the PVOutput API.

        A generic method for sending/handling HTTP requests done against
        the PVOutput API.

        Args:
        ----
            uri: Request URI, without '/service/r2/'.
            method: HTTP method to use.
            data: Dictionary of parameters to send to the PVOutput API.

        Returns:
        -------
            The response body from the PVOutput API.

        Raises:
        ------
            PVOutputAuthenticationError: If the API key is invalid.
            PVOutputConnectionError: An error occurred while communicating with
                the PVOutput API.
            PVOutputError: Received an unexpected response from the PVOutput
                API.

        """
        url = URL("https://pvoutput.org/service/r2/").join(URL(uri))

        headers = {
            "Accept": "application/json",
            "X-Pvoutput-Apikey": self.api_key,
            "X-Pvoutput-SystemId": str(self.system_id),
        }

        if self.session is None:
            self.session = ClientSession()
            self._close_session = True

        try:
            async with asyncio.timeout(self.request_timeout):
                response = await self.session.request(
                    method,
                    url,
                    data=data,
                    headers=headers,
                )
                response.raise_for_status()
        except TimeoutError as exception:
            msg = "Timeout occurred while connecting to the PVOutput API"
            raise PVOutputConnectionError(msg) from exception
        except ClientResponseError as exception:
            if exception.status == 400 and uri.startswith("getstatus.jsp"):
                msg = "PVOutput has no status data available for this system"
                raise PVOutputNoDataError(msg) from exception
            if exception.status in [401, 403]:
                msg = "Authentication to the PVOutput API failed"
                raise PVOutputAuthenticationError(msg) from exception
            msg = "Error occurred while connecting to the PVOutput API"
            raise PVOutputError(msg) from exception
        except (ClientError, socket.gaierror) as exception:
            msg = "Error occurred while communicating with the PVOutput API"
            raise PVOutputConnectionError(msg) from exception

        return await response.text()

    async def status(self) -> Status:
        """Retrieve system status information and live output data.

        Returns
        -------
            A PVOutput Status object.

        """
        data = await self._request("getstatus.jsp")
        return Status.from_dict(
            dict(
                zip(
                    [
                        "reported_date",
                        "reported_time",
                        "energy_generation",
                        "power_generation",
                        "energy_consumption",
                        "power_consumption",
                        "normalized_output",
                        "temperature",
                        "voltage",
                    ],
                    data.split(","),
                    strict=True,
                )
            ),
        )

    async def system(self) -> System:
        """Retrieve system information.

        Returns
        -------
            A PVOutput System object.

        """
        data = await self._request("getsystem.jsp")
        return System.from_dict(
            dict(
                zip(
                    [
                        "system_name",
                        "system_size",
                        "zipcode",
                        "panels",
                        "panel_power",
                        "panel_brand",
                        "inverters",
                        "inverter_power",
                        "inverter_brand",
                        "orientation",
                        "array_tilt",
                        "shade",
                        "install_date",
                        "latitude",
                        "longitude",
                        "status_interval",
                    ],
                    data.partition(";")[0].split(","),
                    strict=True,
                )
            ),
        )

    # pylint: disable-next=too-many-arguments
    async def add_status(  # noqa: PLR0913
        self,
        *,
        energy_generation: int | None = None,
        power_generation: int | None = None,
        energy_consumption: int | None = None,
        power_consumption: int | None = None,
        temperature: float | None = None,
        voltage: float | None = None,
        cumulative: bool = False,
        net: bool = False,
        reported_date: date | None = None,
        reported_time: time | None = None,
    ) -> None:
        """Upload a live status update to PVOutput.

        Args:
        ----
            energy_generation: Energy generation in watt hours (Wh).
            power_generation: Power generation in watts (W).
            energy_consumption: Energy consumption in watt hours (Wh).
            power_consumption: Power consumption in watts (W).
            temperature: Temperature in celsius (C).
            voltage: Voltage in volts (V).
            cumulative: Whether energy values are lifetime cumulative.
            net: Whether values are net import/export.
            reported_date: Date of the status. Defaults to today.
            reported_time: Time of the status. Defaults to now.

        """
        now = datetime.now(tz=UTC)
        data: dict[str, str] = {
            "d": (reported_date or now.date()).strftime("%Y%m%d"),
            "t": (reported_time or now.time()).strftime("%H:%M"),
        }

        if energy_generation is not None:
            data["v1"] = str(energy_generation)
        if power_generation is not None:
            data["v2"] = str(power_generation)
        if energy_consumption is not None:
            data["v3"] = str(energy_consumption)
        if power_consumption is not None:
            data["v4"] = str(power_consumption)
        if temperature is not None:
            data["v5"] = str(temperature)
        if voltage is not None:
            data["v6"] = str(voltage)
        if cumulative:
            data["c1"] = "1"
        if net:
            data["n"] = "1"

        await self._request("addstatus.jsp", method=METH_POST, data=data)

    async def close(self) -> None:
        """Close open client session."""
        if self.session and self._close_session:
            await self.session.close()

    async def __aenter__(self) -> Self:
        """Async enter.

        Returns
        -------
            The PVOutput object.

        """
        return self

    async def __aexit__(self, *_exc_info: object) -> None:
        """Async exit.

        Args:
        ----
            _exc_info: Exception info.

        """
        await self.close()
