from __future__ import annotations

import asyncio
import logging
import typing

import aiohttp
from aiohttp import ClientSession
from urllib.parse import urljoin

LOGGER = logging.getLogger(__name__)

DEFAULT_BASE_URL = "https://v1.fluss-api.com/v1/"


class FlussApiClientError(Exception):
    """Exception to indicate a general API error."""


class FlussDeviceError(FlussApiClientError):
    """Exception to indicate an error occurred when retrieving devices."""


class FlussApiClientCommunicationError(FlussApiClientError):
    """Exception to indicate a communication error."""


class FlussApiClientAuthenticationError(FlussApiClientError):
    """Exception to indicate an authentication error."""


class FlussDeviceOfflineError(FlussDeviceError):
    """Exception to indicate the device is not connected to the internet."""


class FlussApiClient:
    """Fluss+ API Client.

    Uses the Fluss REST API v1 at https://v1.fluss-api.com/v1.
    Authentication is via API key passed in the Authorization header.
    """

    def __init__(
        self,
        api_key: str,
        session: typing.Optional[ClientSession] = None,
        timeout: int = 10,
        base_url: str = DEFAULT_BASE_URL,
    ) -> None:
        """Initialize the Fluss+ API Client."""
        self._api_key = api_key
        self._base_url = base_url if base_url.endswith("/") else base_url + "/"
        self._timeout = timeout
        self._session = session or ClientSession()

    def _auth_headers(self) -> dict:
        """Return authorization headers for API requests."""
        return {
            "Authorization": self._api_key,
            "Content-Type": "application/json",
        }

    async def async_get_devices(self) -> typing.Any:
        """List all devices associated with the account."""
        try:
            return await self._api_wrapper(
                method="GET",
                endpoint="list",
                headers=self._auth_headers(),
            )
        except FlussApiClientError as error:
            LOGGER.error("Failed to get devices: %s", error)
            raise FlussDeviceError("Failed to retrieve devices") from error

    async def async_get_device_status(self, device_id: str) -> typing.Any:
        """Get the current status of a device."""
        return await self._api_wrapper(
            method="GET",
            endpoint=f"status/{device_id}",
            headers=self._auth_headers(),
        )

    async def async_trigger_device(
        self,
        device_id: str,
        **kwargs: typing.Any,
    ) -> typing.Any:
        """Trigger a device.

        Args:
            device_id: The ID of the device to trigger.
            **kwargs: Optional parameters forwarded to the API
                      (e.g. action="pulse", duration=3).
        """
        data: dict[str, typing.Any] = {"metaData": kwargs.get("metaData", "")}
        return await self._api_wrapper(
            method="POST",
            endpoint=f"trigger/{device_id}",
            headers=self._auth_headers(),
            data=data,
        )

    async def async_open_device(
        self,
        device_id: str,
        **kwargs: typing.Any,
    ) -> typing.Any:
        """Open a device (gate/door).

        Args:
            device_id: The ID of the device to open.
            **kwargs: Optional parameters forwarded to the API.
        """
        return await self._api_wrapper(
            method="POST",
            endpoint=f"open/{device_id}",
            headers=self._auth_headers(),
        )

    async def async_close_device(
        self,
        device_id: str,
        **kwargs: typing.Any,
    ) -> typing.Any:
        """Close a device (gate/door).

        Args:
            device_id: The ID of the device to close.
            **kwargs: Optional parameters forwarded to the API.
        """
        return await self._api_wrapper(
            method="POST",
            endpoint=f"close/{device_id}",
            headers=self._auth_headers(),
        )

    async def _api_wrapper(
        self,
        method: str,
        endpoint: str,
        data: dict | None = None,
        headers: dict | None = None,
    ) -> typing.Any:
        """Make a request to the Fluss API."""
        url = urljoin(self._base_url, endpoint)
        try:
            async with asyncio.timeout(self._timeout):
                async with self._session.request(
                    method=method,
                    url=url,
                    headers=headers,
                    json=data,
                ) as response:
                    if response.status == 401:
                        raise FlussApiClientAuthenticationError("Invalid credentials")
                    if response.status == 403:
                        raise FlussApiClientAuthenticationError("Access forbidden")
                    if response.status == 404:
                        raise FlussDeviceError("Device not found")
                    if response.status == 503:
                        raise FlussDeviceOfflineError(
                            "Device is not connected to the internet"
                        )
                    response.raise_for_status()
                    return await response.json()

        except asyncio.TimeoutError as e:
            LOGGER.error("Timeout error fetching information from %s", url)
            raise FlussApiClientCommunicationError(
                "Timeout error fetching information"
            ) from e
        except aiohttp.ClientError as ex:
            msg = str(ex)
            if "Domain name not found" in msg or "Name or service not known" in msg:
                LOGGER.error(
                    "DNS resolution failed for %s — the Fluss API host is "
                    "unreachable. Verify the base URL is correct",
                    url,
                )
                raise FlussApiClientCommunicationError(
                    f"Cannot resolve API host for {url}. "
                    "The Fluss API domain may have changed."
                ) from ex
            LOGGER.error("Client error fetching information from %s: %s", url, ex)
            raise FlussApiClientCommunicationError(
                "Error fetching information"
            ) from ex
        except FlussApiClientError:
            raise
        except Exception as exception:
            LOGGER.error("Unexpected error occurred: %s", exception)
            raise FlussApiClientError(
                "An unexpected error occurred"
            ) from exception

    async def close(self) -> None:
        """Close the aiohttp session if it was created by this client."""
        if self._session and not self._session.closed:
            await self._session.close()
