"""Library to handle connection with Tibber API."""

import asyncio
import datetime as dt
import logging
from collections.abc import Awaitable, Callable
from ssl import SSLContext
from typing import Any

import aiohttp

from .const import API_ENDPOINT, DEFAULT_TIMEOUT, DEMO_TOKEN, __version__
from .data_api import TibberDataAPI
from .exceptions import (
    FatalHttpExceptionError,
    InvalidLoginError,
    RetryableHttpExceptionError,
    UserAgentMissingError,
)
from .gql_queries import INFO, PUSH_NOTIFICATION
from .home import TibberHome
from .realtime import TibberRT
from .response_handler import extract_response_data

_LOGGER = logging.getLogger(__name__)

__all__ = [
    "FatalHttpExceptionError",
    "InvalidLoginError",
    "RetryableHttpExceptionError",
    "Tibber",
    "TibberDataAPI",
    "TibberHome",
    "TibberRT",
]


class Tibber:
    """Class to communicate with the Tibber api."""

    def __init__(
        self,
        access_token: str = DEMO_TOKEN,
        timeout: int = DEFAULT_TIMEOUT,
        websession: aiohttp.ClientSession | None = None,
        time_zone: dt.tzinfo | None = None,
        user_agent: str | None = None,
        ssl: SSLContext | bool = True,
        refresh_access_token: Callable[[], Awaitable[str | None]] | None = None,
    ) -> None:
        """Initialize the Tibber connection.

        :param access_token: The access token to access the Tibber API with.
        :param timeout: The timeout in seconds to use when communicating with the Tibber API.
        :param websession: The websession to use when communicating with the Tibber API.
        :param time_zone: The time zone to display times in and to use.
        :param user_agent: User agent identifier for the platform running this. Required if websession is None.
        :param ssl: SSLContext to use.
        :param refresh_access_token: Async callback that returns a refreshed Tibber API access token before
            reconnecting.
        """
        if websession is None:
            websession = aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=ssl))
        elif user_agent is None:
            user_agent = websession.headers.get(aiohttp.hdrs.USER_AGENT)
        if user_agent is None:
            raise UserAgentMissingError("Please provide value for HTTP user agent")
        self._user_agent: str = f"{user_agent} pyTibber/{__version__} "
        self.websession = websession
        self.timeout: int = timeout
        self._access_token: str = access_token
        self._refresh_access_token = refresh_access_token
        self.realtime = TibberRT(
            access_token,
            timeout,
            self._user_agent,
            ssl=ssl,
            refresh_access_token=self._refresh_access_token_for_reconnect if refresh_access_token is not None else None,
        )

        self.time_zone: dt.tzinfo = time_zone or dt.UTC
        self._name: str = ""
        self._user_id: str | None = None
        self._active_home_ids: list[str] = []
        self._all_home_ids: list[str] = []
        self._homes: dict[str, TibberHome] = {}
        self.data_api: TibberDataAPI = TibberDataAPI(
            access_token,
            timeout=timeout,
            websession=websession,
            user_agent=self._user_agent,
        )

    async def _refresh_access_token_for_reconnect(self) -> str | None:
        """Refresh access token before reconnecting realtime subscriptions."""
        if self._refresh_access_token is None:
            return None

        access_token = await self._refresh_access_token()
        if access_token is not None and access_token != self._access_token:
            _LOGGER.debug("Updating access token")
            self._access_token = access_token
            self.data_api.set_access_token(access_token)
        return access_token

    async def close_connection(self) -> None:
        """Close the Tibber connection.
        This method simply closes the websession used by the object.
        """
        await self.websession.close()

    async def execute(
        self,
        document: str,
        variable_values: dict[Any, Any] | None = None,
        timeout: int | None = None,  # noqa: ASYNC109
        retry: int = 3,
    ) -> dict[Any, Any] | None:
        """Execute a GraphQL query and return the data.

        :param document: The GraphQL query to request.
        :param variable_values: The GraphQL variables to parse with the request.
        :param timeout: The timeout to use for the request.
        :param retry: The number of times to retry the request.
        """
        timeout = timeout or self.timeout

        payload = {"query": document, "variables": variable_values or {}}

        _LOGGER.debug(
            "Executing query: %s with variables: %s",
            document.replace(" ", "").replace("\n", "_"),
            variable_values,
        )
        try:
            resp = await self.websession.post(
                API_ENDPOINT,
                headers={
                    "Authorization": "Bearer " + self._access_token,
                    aiohttp.hdrs.USER_AGENT: self._user_agent,
                },
                data=payload,
                timeout=aiohttp.ClientTimeout(total=self.timeout),
            )
            return (await extract_response_data(resp)).get("data")
        except (TimeoutError, aiohttp.ClientError) as err:
            if retry > 0:
                return await self.execute(
                    document,
                    variable_values,
                    timeout,
                    retry - 1,
                )
            if isinstance(err, asyncio.TimeoutError):
                _LOGGER.debug("Timed out when connecting to Tibber")
            else:
                _LOGGER.debug("Error connecting to Tibber", exc_info=True)
            raise

    async def update_info(self) -> None:
        """Updates home info asynchronously."""
        if (data := await self.execute(INFO)) is None:
            return

        if not (viewer := data.get("viewer")):
            return

        if sub_endpoint := viewer.get("websocketSubscriptionUrl"):
            await self.realtime.set_subscription_endpoint(sub_endpoint)

        self._name = viewer.get("name")
        self._user_id = viewer.get("userId")

        self._active_home_ids = []
        self._all_home_ids = []
        for _home in viewer.get("homes", []):
            if not (home_id := _home.get("id")):
                continue
            self._all_home_ids += [home_id]
            if not (subs := _home.get("subscriptions")):
                continue
            if subs[0].get("status") is not None and subs[0]["status"].lower() == "running":
                self._active_home_ids += [home_id]

    def get_home_ids(self, only_active: bool = True) -> list[str]:
        """Return list of home ids."""
        if only_active:
            return self._active_home_ids
        return self._all_home_ids

    def get_homes(self, only_active: bool = True) -> list[TibberHome]:
        """Return list of Tibber homes."""
        return [home for home_id in self.get_home_ids(only_active) if (home := self.get_home(home_id))]

    def get_home(self, home_id: str) -> TibberHome | None:
        """Return an instance of TibberHome for given home id."""
        if home_id not in self._all_home_ids:
            _LOGGER.error("Could not find any Tibber home with id: %s", home_id)
            return None
        if home_id not in self._homes:
            self._homes[home_id] = TibberHome(home_id, self)
        return self._homes[home_id]

    async def send_notification(self, title: str, message: str) -> bool:
        """Sends a push notification to the Tibber app on registered devices.

        :param title: The title of the push notification.
        :param message: The message of the push notification.
        """
        if not (
            res := await self.execute(
                PUSH_NOTIFICATION.format(
                    title,
                    message,
                ),
            )
        ):
            return False
        notification = res.get("sendPushNotification", {})
        successful = notification.get("successful", False)
        pushed_to_number_of_devices = notification.get("pushedToNumberOfDevices", 0)
        _LOGGER.debug(
            "send_notification: status %s, send to %s devices",
            successful,
            pushed_to_number_of_devices,
        )
        return successful

    async def fetch_consumption_data_active_homes(self) -> None:
        """Fetch consumption data for active homes."""
        await asyncio.gather(
            *[tibber_home.fetch_consumption_data() for tibber_home in self.get_homes(only_active=True)],
        )

    async def fetch_production_data_active_homes(self) -> None:
        """Fetch production data for active homes."""
        await asyncio.gather(
            *[
                tibber_home.fetch_production_data()
                for tibber_home in self.get_homes(only_active=True)
                if tibber_home.has_production
            ],
        )

    async def rt_disconnect(self) -> None:
        """Stop subscription manager."""
        for home in self._homes.values():
            home.rt_unsubscribe()
        await self.realtime.disconnect()

    async def set_access_token(self, access_token: str) -> None:
        """Set access token and reauthorize clients."""
        if access_token == self._access_token:
            return
        _LOGGER.debug("Updating access token")
        self._access_token = access_token
        self.data_api.set_access_token(access_token)
        await self.realtime.set_access_token(access_token)

    @property
    def user_id(self) -> str | None:
        """Return user id of user."""
        return self._user_id

    @property
    def name(self) -> str:
        """Return name of user."""
        return self._name

    @property
    def home_ids(self) -> list[str]:
        """Return list of home ids."""
        return self.get_home_ids(only_active=True)
