"""Use of this source code is governed by the MIT license found in the LICENSE file.

Plugwise Smile communication protocol helpers.
"""

from __future__ import annotations

from plugwise.constants import LOGGER
from plugwise.exceptions import (
    ConnectionFailedError,
    InvalidAuthentication,
    InvalidXMLError,
    ResponseError,
)
from plugwise.util import escape_illegal_xml_characters

# This way of importing aiohttp is because of patch/mocking in testing (aiohttp timeouts)
from aiohttp import (
    ClientError,
    ClientResponse,
    ClientSession,
    ClientTimeout,
    encode_basic_auth,
)
from defusedxml import ElementTree as etree


class SmileComm:
    """The SmileComm class."""

    def __init__(
        self,
        host: str,
        password: str,
        port: int,
        timeout: int,
        *,
        username: str,
        websession: ClientSession | None,
    ) -> None:
        """Set the constructor for this class."""
        if not websession:
            aio_timeout = ClientTimeout(total=timeout)
            self._websession = ClientSession(timeout=aio_timeout)
        else:
            self._websession = websession

        # Quickfix IPv6 formatting, not covering
        if host.count(":") > 2:  # pragma: no cover
            host = f"[{host}]"

        self._base_header = {
            "Authorization": encode_basic_auth(username, password=password)
        }
        self._endpoint = f"http://{host}:{str(port)}"  # Sensitive

    async def _request(
        self,
        command: str,
        retry: int = 3,
        method: str = "get",
        data: str | None = None,
    ) -> etree.Element:
        """Get/put/delete data from a give URL."""
        resp: ClientResponse
        url = f"{self._endpoint}{command}"
        try:
            match method:
                case "delete":
                    resp = await self._websession.delete(url, headers=self._base_header)
                case "get":
                    # Work-around for Stretchv2, should not hurt the other smiles
                    headers = {**self._base_header, "Accept-Encoding": "gzip"}
                    resp = await self._websession.get(url, headers=headers)
                case "post":
                    headers = {**self._base_header, "Content-type": "text/xml"}
                    resp = await self._websession.post(
                        url,
                        headers=headers,
                        data=data,
                    )
                case "put":
                    headers = {**self._base_header, "Content-type": "text/xml"}
                    resp = await self._websession.put(
                        url,
                        headers=headers,
                        data=data,
                    )
        except (
            ClientError
        ) as exc:  # ClientError is an ancestor class of ServerTimeoutError
            if retry < 1:
                LOGGER.warning(
                    "Failed sending %s %s to Plugwise Smile, error: %s",
                    method,
                    command,
                    exc,
                )
                raise ConnectionFailedError from exc
            return await self._request(command, retry - 1)

        if resp.status == 504:
            if retry < 1:
                LOGGER.warning(
                    "Failed sending %s %s to Plugwise Smile, error: %s",
                    method,
                    command,
                    "504 Gateway Timeout",
                )
                raise ConnectionFailedError
            return await self._request(command, retry - 1)

        return await self._request_validate(resp, method)

    async def _request_validate(
        self, resp: ClientResponse, method: str
    ) -> etree.Element:
        """Helper-function for _request(): validate the returned data."""
        match resp.status:
            case 200:
                # Cornercases for server not responding with 202
                if method in ("post", "put"):
                    return
            case 202:
                # Command accepted gives empty body with status 202
                return
            case 401:
                msg = (
                    "Invalid Plugwise login, please retry with the correct credentials."
                )
                LOGGER.error("%s", msg)
                raise InvalidAuthentication
            case 405:
                msg = "405 Method not allowed."
                LOGGER.error("%s", msg)
                raise ConnectionFailedError

        if not (result := await resp.text()) or (
            "<error>" in result and "Not started" not in result
        ):
            LOGGER.warning("Smile response empty or error in %s", result)
            raise ResponseError

        try:
            # Encode to ensure utf8 parsing
            xml = etree.XML(escape_illegal_xml_characters(result).encode())
        except etree.ParseError as exc:
            LOGGER.warning("Smile returns invalid XML for %s", self._endpoint)
            raise InvalidXMLError from exc

        return xml

    async def close_connection(self) -> None:
        """Close the Plugwise connection."""
        await self._websession.close()
