"""Define an object to work directly with the API."""

from __future__ import annotations

import asyncio
from time import time
from typing import Any, cast
from uuid import uuid4

from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientError

from .const import LOGGER
from .errors import InvalidAuthError, RequestError
from .tile import Tile

API_URL_SCAFFOLD = "https://production.tile-api.com/api/v1"

DEFAULT_API_VERSION = "1.0"
DEFAULT_APP_ID = "ios-tile-production"
DEFAULT_APP_VERSION = "2.89.1.4774"
DEFAULT_LOCALE = "en-US"
DEFAULT_TIMEOUT = 10
DEFAULT_USER_AGENT = "Tile/4774 CFNetwork/1312 Darwin/21.0.0"


class API:
    """Define the API management object."""

    def __init__(  # pylint: disable=too-many-arguments
        self,
        email: str,
        password: str,
        session: ClientSession,
        *,
        client_uuid: str | None = None,
        locale: str = DEFAULT_LOCALE,
    ) -> None:
        """Initialize.

        Args:
            email: An email address for a Tile account.
            password: The account password.
            session: An optional aiohttp ClientSession.
            client_uuid: An optional UUID to identify this API object.
            locale: An optional locale.
        """
        self._client_established: bool = False
        self._email: str = email
        self._locale: str = locale
        self._password: str = password
        self._session: ClientSession = session
        self._session_expiry: int | None = None
        self.client_uuid: str = client_uuid if client_uuid else str(uuid4())
        self.user_uuid: str | None = None

    async def _async_request(
        self, method: str, endpoint: str, **kwargs: dict[str, Any]
    ) -> dict[str, Any]:
        """Make an API request.

        Args:
            method: An HTTP method.
            endpoint: A relative API endpoint.
            **kwargs: Additional kwargs to send with the request.

        Returns:
            An API response payload.

        Raises:
            InvalidAuthError: Raised upon invalid credentials.
            RequestError: Raised upon an underlying HTTP error.
        """
        if self._session_expiry and self._session_expiry <= int(time() * 1000):
            await self.async_init()

        kwargs.setdefault("headers", {})
        kwargs["headers"]["User-Agent"] = DEFAULT_USER_AGENT
        kwargs["headers"]["tile_api_version"] = DEFAULT_API_VERSION
        kwargs["headers"]["tile_app_id"] = DEFAULT_APP_ID
        kwargs["headers"]["tile_app_version"] = DEFAULT_APP_VERSION
        kwargs["headers"]["tile_client_uuid"] = self.client_uuid

        try:
            async with self._session.request(
                method, f"{API_URL_SCAFFOLD}/{endpoint}", **kwargs
            ) as resp:
                resp.raise_for_status()
                data = await resp.json()
        except ClientError as err:
            if "401" in str(err):
                raise InvalidAuthError("Invalid credentials") from err
            raise RequestError(f"Error requesting data from {endpoint}: {err}") from err

        LOGGER.debug("Data received from /%s: %s", endpoint, data)

        return cast(dict[str, Any], data)

    async def async_get_tiles(self) -> dict[str, Tile]:
        """Get all active Tiles from the user's account.

        Returns:
            A dictionary of Tile UUIDs to Tile objects.
        """
        states = await self._async_request("get", "tiles/tile_states")

        details_tasks = {
            tile_uuid: self._async_request("get", f"tiles/{tile_uuid}")
            for tile_uuid in [tile["tile_id"] for tile in states["result"]]
        }

        results = await asyncio.gather(*details_tasks.values(), return_exceptions=True)

        data = {}
        for tile_uuid, result in zip(details_tasks, results):
            if isinstance(result, RequestError) and "412" in str(result):
                # Tile Labels will return an HTTP 412 because they don't have
                # additional details; we can safely ignore these errors and still
                # track the Tile (without additional details):
                continue
            if isinstance(result, BaseException):
                LOGGER.error("Error requesting details for %s: %s", tile_uuid, result)
                continue
            data[tile_uuid] = Tile(self._async_request, result)

        return data

    async def async_init(self) -> None:
        """Create a Tile session."""
        # Invalidate the existing session expiry datetime (if it exists) so that the
        # next few requests don't fail:
        self._session_expiry = None

        if not self._client_established:
            await self._async_request(
                "put",
                f"clients/{self.client_uuid}",
                data={
                    "app_id": DEFAULT_APP_ID,
                    "app_version": DEFAULT_APP_VERSION,
                    "locale": self._locale,
                },
            )
            self._client_established = True

        resp = await self._async_request(
            "post",
            f"clients/{self.client_uuid}/sessions",
            data={"email": self._email, "password": self._password},
        )

        if not self.user_uuid:
            self.user_uuid = resp["result"]["user"]["user_uuid"]
        self._session_expiry = resp["result"]["session_expiration_timestamp"]


async def async_login(
    email: str,
    password: str,
    session: ClientSession,
    *,
    client_uuid: str | None = None,
    locale: str = DEFAULT_LOCALE,
) -> API:
    """Return an authenticated client.

    Args:
        email: An email address for a Tile account.
        password: The account password.
        session: An optional aiohttp ClientSession.
        client_uuid: An optional UUID to identify this API object.
        locale: An optional locale.

    Returns:
        An authenticated API object.
    """
    api: API = API(email, password, session, client_uuid=client_uuid, locale=locale)
    await api.async_init()
    return api
