"""Async HTTPS client for the Garo Entity Pro local REST API (aiohttp)."""

from __future__ import annotations

import json
import ssl
from typing import Any, Final

import aiohttp
from aiohttp import ClientTimeout, helpers
from yarl import URL

from .exceptions import GaroAuthError, GaroConnectionError, GaroHttpError

_DEFAULT_TIMEOUT: Final = ClientTimeout(total=20, connect=10)
# Entity Pro uses /status/... (not legacy /api/v1/... on older chargers).
_DEFAULT_STATUS_PATH: Final = "/status/energy-meter"
_DEFAULT_METER_PATH: Final = "/status/energy-meter"
_DEFAULT_TEMPERATURES_PATH: Final = "/status/temperatures"
_DEFAULT_METERVALUE_SAMPLE_PATH: Final = "/status/metervalue-sample"
_DEFAULT_CHARGING_STATE_PATH: Final = "/status/charging-state"


class GaroApiClient:
    """HTTPS client using HTTP Basic authentication for Garo Entity Pro REST API."""

    def __init__(
        self,
        host: str,
        username: str,
        password: str,
        *,
        port: int | None = None,
        verify_ssl: bool = True,
        timeout: ClientTimeout | None = None,
        status_path: str = _DEFAULT_STATUS_PATH,
        meter_path: str = _DEFAULT_METER_PATH,
        temperatures_path: str = _DEFAULT_TEMPERATURES_PATH,
        metervalue_sample_path: str = _DEFAULT_METERVALUE_SAMPLE_PATH,
        charging_state_path: str = _DEFAULT_CHARGING_STATE_PATH,
    ) -> None:
        self._username = username.strip()
        self._password = password.strip()
        self._timeout = timeout or _DEFAULT_TIMEOUT
        self._status_path = status_path
        self._meter_path = meter_path
        self._temperatures_path = temperatures_path
        self._metervalue_sample_path = metervalue_sample_path
        self._charging_state_path = charging_state_path
        self._session: aiohttp.ClientSession | None = None

        base = host.strip()
        if not base.lower().startswith(("http://", "https://")):
            base = f"https://{base}"
        url = URL(base)
        if port is not None:
            url = url.with_port(port)
        self._base_url: Final[URL] = url

        if verify_ssl:
            self._ssl: bool | ssl.SSLContext = True
        else:
            self._ssl = False

    @property
    def base_url(self) -> str:
        """Configured base URL (scheme + host + optional port)."""
        return str(self._base_url)

    @property
    def status_path(self) -> str:
        """Configured GET path for primary telemetry (e.g. energy meter)."""
        return self._status_path

    @property
    def meter_path(self) -> str:
        """Configured GET path for meter data (may match status on Entity Pro)."""
        return self._meter_path

    @property
    def temperatures_path(self) -> str:
        """Configured GET path for CPU / baseboard temperatures."""
        return self._temperatures_path

    @property
    def metervalue_sample_path(self) -> str:
        """GET path for latest OCPP sampledValue snapshot."""
        return self._metervalue_sample_path

    @property
    def charging_state_path(self) -> str:
        """GET path for charging state (JSON string or object)."""
        return self._charging_state_path

    async def __aenter__(self) -> GaroApiClient:
        await self._ensure_session()
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: object | None,
    ) -> None:
        await self.close()

    async def close(self) -> None:
        """Close the underlying HTTP session."""
        if self._session is not None and not self._session.closed:
            await self._session.close()
        self._session = None

    async def _ensure_session(self) -> aiohttp.ClientSession:
        if self._session is None or self._session.closed:
            connector = aiohttp.TCPConnector(limit=10, ssl=self._ssl)
            self._session = aiohttp.ClientSession(
                timeout=self._timeout,
                connector=connector,
                raise_for_status=False,
            )
        return self._session

    async def get_status(self) -> dict[str, Any]:
        """GET primary telemetry (default: /status/energy-meter)."""
        return await self.get_json(self._status_path)

    async def get_meter(self) -> dict[str, Any]:
        """GET meter data (default: /status/energy-meter; may equal status on Entity Pro)."""
        return await self.get_json(self._meter_path)

    async def get_temperatures(self) -> dict[str, Any]:
        """GET /status/temperatures (CPU and baseboard; JSON object or array)."""
        session = await self._ensure_session()
        url = str(self._base_url.join(URL(self._temperatures_path)))
        return await self._request_json(session, "GET", url, json_body=None)

    async def get_metervalue_sample(self) -> dict[str, Any]:
        """GET /status/metervalue-sample (single OCPP block with sampledValue)."""
        return await self.get_json(self._metervalue_sample_path)

    async def get_charging_state(self) -> dict[str, Any]:
        """GET /status/charging-state (may be a JSON string, number, or object)."""
        return await self.get_json(self._charging_state_path)

    async def get_json(self, path: str) -> dict[str, Any]:
        """GET request; returns parsed JSON object."""
        session = await self._ensure_session()
        url = str(self._base_url.join(URL(path)))
        return await self._request_json(session, "GET", url, json_body=None)

    def _basic_header(self) -> str:
        return helpers.BasicAuth(self._username, self._password).encode()

    async def _request_json(
        self,
        session: aiohttp.ClientSession,
        method: str,
        url: str,
        *,
        json_body: dict[str, Any] | None,
    ) -> dict[str, Any]:
        text, status = await self._send_with_auth(session, method, url, json_body=json_body)
        self._raise_for_status(status, text)
        if not text.strip():
            return {}
        try:
            data = json.loads(text)
        except json.JSONDecodeError as err:
            msg = "Response is not valid JSON"
            raise GaroHttpError(msg, status=status, body=text) from err
        if isinstance(data, dict):
            return data
        if data is None:
            return {}
        # Entity Pro /status/energy-meter can return [] before the meter is ready.
        if isinstance(data, list):
            return {"_list": data}
        # /status/charging-state may return a bare JSON string, number, or boolean.
        if isinstance(data, str):
            return {"state": data, "charging_state": data}
        if isinstance(data, (int, float)):
            return {"state": data, "charging_state": data}
        if isinstance(data, bool):
            return {"state": data, "charging_state": data, "charging": data}
        msg = "Unexpected JSON value in response"
        raise GaroHttpError(msg, status=status, body=text)

    async def _send_with_auth(
        self,
        session: aiohttp.ClientSession,
        method: str,
        url: str,
        *,
        json_body: dict[str, Any] | None,
    ) -> tuple[str, int]:
        """Send request with HTTP Basic Authorization header."""
        req_kwargs: dict[str, Any] = {
            "method": method,
            "url": url,
            "headers": {"Authorization": self._basic_header()},
            "ssl": self._ssl,
        }
        if json_body is not None:
            req_kwargs["json"] = json_body

        try:
            async with session.request(**req_kwargs) as resp:
                text = await resp.text(encoding="utf-8")
                return text, resp.status
        except aiohttp.ClientConnectorError as err:
            msg = f"Connection failed: {err}"
            raise GaroConnectionError(msg) from err
        except aiohttp.ServerDisconnectedError as err:
            msg = f"Server disconnected: {err}"
            raise GaroConnectionError(msg) from err
        except TimeoutError as err:
            msg = "Request timed out"
            raise GaroConnectionError(msg) from err
        except aiohttp.ClientError as err:
            msg = f"HTTP client error: {err}"
            raise GaroConnectionError(msg) from err

    @staticmethod
    def _raise_for_status(status: int, body: str) -> None:
        if status == 401:
            detail = (body or "").strip()[:300]
            msg = "Authentication failed (check username/password and that the account is enabled)"
            if detail:
                msg = f"{msg}. Server response: {detail}"
            raise GaroAuthError(msg)
        if status == 403:
            detail = (body or "").strip()[:300]
            msg = "Forbidden (wrong role or API disabled)"
            if detail:
                msg = f"{msg}. Server response: {detail}"
            raise GaroAuthError(msg)
        if 400 <= status < 600:
            msg = f"HTTP {status}"
            raise GaroHttpError(msg, status=status, body=body or None)
