"""Library for Miele API."""

# TODO
# Should be moved to pypi.org when reasonably stable
from __future__ import annotations

import asyncio
import json
import logging
from abc import ABC, abstractmethod
from json.decoder import JSONDecodeError
from typing import Any, Callable, Coroutine

from aiohttp import ClientError, ClientResponse, ClientSession, ClientTimeout

from .const import MIELE_API, VERSION

CONTENT_TYPE = "application/json"
USER_AGENT_BASE = f"Pymiele/{VERSION}"

_LOGGER = logging.getLogger(__name__)


class AbstractAuth(ABC):
    """Abstract class to make authenticated requests."""

    def __init__(self, websession: ClientSession, host: str):
        """Initialize the auth."""
        self.websession = websession
        self.host = host

    @abstractmethod
    async def async_get_access_token(self) -> str:
        """Return a valid access token."""

    async def request(self, method, url, **kwargs) -> ClientResponse:
        """Make a request."""
        headers = kwargs.get("headers")

        if headers is None:
            headers = {}
        else:
            headers = dict(headers)
            kwargs.pop("headers")

        agent_suffix = kwargs.get("agent_suffix")
        if "agent_suffix" in kwargs:
            kwargs.pop("agent_suffix")
        user_agent = (
            USER_AGENT_BASE
            if agent_suffix is None
            else f"{USER_AGENT_BASE}; {agent_suffix}"
        )

        access_token = await self.async_get_access_token()
        headers["Authorization"] = f"Bearer {access_token}"
        headers["User-Agent"] = user_agent

        # _LOGGER.debug("Request headers: %s", headers)

        return await self.websession.request(
            method,
            f"{self.host}{url}",
            **kwargs,
            headers=headers,
        )

    async def set_target_temperature(
        self, serial: str, temperature: float, zone: int = 1
    ):
        """Set target temperature."""
        temp = round(temperature)
        async with asyncio.timeout(10):
            data = {"targetTemperature": [{"zone": zone, "value": temp}]}
            res = await self.request(
                "PUT",
                f"/devices/{serial}/actions",
                data=json.dumps(data),
                headers={
                    "Content-Type": CONTENT_TYPE,
                    "Accept": "application/json",
                },
            )
            res.raise_for_status()
        _LOGGER.debug("set_target res: %s", res.status)
        return res

    async def send_action(self, serial: str, data):
        """Send action command."""

        _LOGGER.debug("send_action serial: %s, data: %s", serial, data)
        async with asyncio.timeout(10):
            res = await self.request(
                "PUT",
                f"/devices/{serial}/actions",
                data=json.dumps(data),
                headers={
                    "Content-Type": CONTENT_TYPE,
                    "Accept": "application/json",
                },
            )
            res.raise_for_status()
        _LOGGER.debug("send_action res: %s", res.status)
        return res

    async def set_program(self, serial: str, data):
        """Send start program command."""

        _LOGGER.debug("set_program serial: %s, data: %s", serial, data)
        async with asyncio.timeout(10):
            res = await self.request(
                "PUT",
                f"/devices/{serial}/programs",
                data=json.dumps(data),
                headers={
                    "Content-Type": CONTENT_TYPE,
                    "Accept": "application/json",
                },
            )
            res.raise_for_status()
        _LOGGER.debug("set_program res: %s", res.status)
        return res

    async def listen_events(
        self,
        data_callback: Callable[[dict[str, Any]], Any] | None = None,
        actions_callback: Callable[[dict[str, Any]], Any] | None = None,
    ) -> Callable[[], Coroutine[Any, Any, None]]:
        """Listen to events, apply changes to object and call callback with event."""
        while True:
            try:
                access_token = await self.async_get_access_token()
                async with self.websession.get(
                    f"{MIELE_API}/devices/all/events",
                    timeout=ClientTimeout(total=None, sock_connect=5, sock_read=None),
                    headers={
                        "Accept": "text/event-stream; char-set=utf-8",
                        "Authorization": f"Bearer {access_token}",
                    },
                ) as resp:
                    # _LOGGER.debug("Starting listening for events: %s", resp.status)
                    while True:
                        # add 120s timeout for reading event data, ping is every 20s
                        # if ping is not received, then connection must be closed and re-initialized
                        try:
                            id_line = await asyncio.wait_for(
                                resp.content.readline(), timeout=120
                            )
                        except asyncio.exceptions.TimeoutError:
                            resp.close()
                            _LOGGER.warning(
                                "Ping timeout, closing connection and restarting"
                            )
                            break
                        data_line = await resp.content.readline()
                        await resp.content.readline()  # Empty line
                        if resp.closed:
                            _LOGGER.warning("Connection was closed, restarting")
                            break
                        event_type = bytearray(id_line).decode().strip()
                        if event_type == "event: devices":
                            data = json.loads(data_line[6:])
                            if data_callback is not None:
                                asyncio.create_task(data_callback(data))
                        elif event_type == "event: actions":
                            data = json.loads(data_line[6:])
                            if actions_callback is not None:
                                asyncio.create_task(actions_callback(data))
                        elif event_type == "event: ping":
                            # _LOGGER.debug("Ping SSE")
                            pass
                        else:
                            _LOGGER.error("Unknown event type: %s", event_type)

            except ClientError as ex:
                _LOGGER.error("SSE: %s - %s", ex.status, ex.message)
                await asyncio.sleep(5)
            except JSONDecodeError as ex:
                _LOGGER.error(
                    "JSON decode error: %s, Pos: %s, Doc: %s", ex.msg, ex.pos, ex.doc
                )
                await asyncio.sleep(5)
            except Exception as ex:
                _LOGGER.error("Listen_event: %s - %s", ex.status, ex.message)
                await asyncio.sleep(5)


class MieleException(Exception):
    """Generic miele exception."""


class MieleAuthException(MieleException):
    """Authentication failure."""
