"""Internal client for making requests and managing session with Supervisor."""

from dataclasses import dataclass, field
from http import HTTPMethod, HTTPStatus
from importlib import metadata
from typing import Any

from aiohttp import (
    ClientError,
    ClientResponse,
    ClientResponseError,
    ClientSession,
    ClientTimeout,
)
from multidict import MultiDict
from yarl import URL

from .const import DEFAULT_TIMEOUT, ResponseType
from .exceptions import (
    ERROR_KEYS,
    SupervisorAuthenticationError,
    SupervisorBadRequestError,
    SupervisorConnectionError,
    SupervisorError,
    SupervisorForbiddenError,
    SupervisorNotFoundError,
    SupervisorResponseError,
    SupervisorServiceUnavailableError,
    SupervisorTimeoutError,
)
from .models.base import Response, ResultType
from .utils.aiohttp import ChunkAsyncStreamIterator

VERSION = metadata.version(__package__)


def is_json(response: ClientResponse, *, raise_on_fail: bool = False) -> bool:
    """Check if response is json according to Content-Type."""
    content_type = response.headers.get("Content-Type", "")
    if "application/json" not in content_type:
        if raise_on_fail:
            raise SupervisorResponseError(
                "Unexpected response received from supervisor when expecting"
                f"JSON. Status: {response.status}, content type: {content_type}",
            )
        return False
    return True


@dataclass(slots=True)
class _SupervisorClient:
    """Main class for handling connections with Supervisor."""

    api_host: str
    token: str
    session: ClientSession | None = None
    _close_session: bool = field(default=False, init=False)

    async def _raise_on_status(self, response: ClientResponse) -> None:
        """Raise appropriate exception on status."""
        if response.status >= HTTPStatus.BAD_REQUEST.value:
            exc_type: type[SupervisorError] = SupervisorError
            match response.status:
                case HTTPStatus.BAD_REQUEST:
                    exc_type = SupervisorBadRequestError
                case HTTPStatus.UNAUTHORIZED:
                    exc_type = SupervisorAuthenticationError
                case HTTPStatus.FORBIDDEN:
                    exc_type = SupervisorForbiddenError
                case HTTPStatus.NOT_FOUND:
                    exc_type = SupervisorNotFoundError
                case HTTPStatus.SERVICE_UNAVAILABLE:
                    exc_type = SupervisorServiceUnavailableError

            if is_json(response):
                result = Response.from_json(await response.text())
                if result.error_key in ERROR_KEYS:
                    exc_type = ERROR_KEYS[result.error_key]
                raise exc_type(
                    result.message,
                    result.extra_fields,
                    result.job_id,
                )
            raise exc_type()

    async def _request(
        self,
        method: HTTPMethod,
        uri: str,
        *,
        params: dict[str, str] | MultiDict[str] | None,
        response_type: ResponseType,
        json: dict[str, Any] | None = None,
        data: Any = None,
        timeout: ClientTimeout | None = DEFAULT_TIMEOUT,
    ) -> Response:
        """Handle a request to Supervisor."""
        try:
            url = URL(self.api_host).joinpath(uri)
        except ValueError as err:
            raise SupervisorError from err

        # This check is to make sure the normalized URL string is the same as the URL
        # string that was passed in. If they are different, then the passed in uri
        # contained characters that were removed by the normalization
        # such as ../../../../etc/passwd
        if not url.raw_path.endswith(uri):
            raise SupervisorError(f"Invalid request {uri}")

        match response_type:
            case ResponseType.TEXT:
                accept = "text/plain, */*"
            case _:
                accept = "application/json, text/plain, */*"

        headers = {
            "User-Agent": f"AioHASupervisor/{VERSION}",
            "Accept": accept,
            "Authorization": f"Bearer {self.token}",
        }

        if self.session is None:
            self.session = ClientSession()
            self._close_session = True

        try:
            response = await self.session.request(
                method.value,
                url,
                timeout=timeout,
                headers=headers,
                params=params,
                json=json,
                data=data,
            )
            await self._raise_on_status(response)
            match response_type:
                case ResponseType.JSON:
                    is_json(response, raise_on_fail=True)
                    return Response.from_json(await response.text())
                case ResponseType.TEXT:
                    return Response(ResultType.OK, await response.text())
                case ResponseType.STREAM:
                    return Response(
                        ResultType.OK, ChunkAsyncStreamIterator(response.content)
                    )
                case _:
                    return Response(ResultType.OK)

        except (UnicodeDecodeError, ClientResponseError) as err:
            raise SupervisorResponseError(
                "Unusable response received from Supervisor, check logs",
            ) from err
        except TimeoutError as err:
            raise SupervisorTimeoutError("Timeout connecting to Supervisor") from err
        except ClientError as err:
            raise SupervisorConnectionError(
                "Error occurred connecting to supervisor",
            ) from err

    async def get(
        self,
        uri: str,
        *,
        params: dict[str, str] | MultiDict[str] | None = None,
        response_type: ResponseType = ResponseType.JSON,
        timeout: ClientTimeout | None = DEFAULT_TIMEOUT,
    ) -> Response:
        """Handle a GET request to Supervisor."""
        return await self._request(
            HTTPMethod.GET,
            uri,
            params=params,
            response_type=response_type,
            timeout=timeout,
        )

    async def post(
        self,
        uri: str,
        *,
        params: dict[str, str] | MultiDict[str] | None = None,
        response_type: ResponseType = ResponseType.NONE,
        json: dict[str, Any] | None = None,
        data: Any = None,
        timeout: ClientTimeout | None = DEFAULT_TIMEOUT,
    ) -> Response:
        """Handle a POST request to Supervisor."""
        return await self._request(
            HTTPMethod.POST,
            uri,
            params=params,
            response_type=response_type,
            json=json,
            data=data,
            timeout=timeout,
        )

    async def put(
        self,
        uri: str,
        *,
        params: dict[str, str] | MultiDict[str] | None = None,
        json: dict[str, Any] | None = None,
        timeout: ClientTimeout | None = DEFAULT_TIMEOUT,
    ) -> Response:
        """Handle a PUT request to Supervisor."""
        return await self._request(
            HTTPMethod.PUT,
            uri,
            params=params,
            response_type=ResponseType.NONE,
            json=json,
            timeout=timeout,
        )

    async def delete(
        self,
        uri: str,
        *,
        params: dict[str, str] | MultiDict[str] | None = None,
        json: dict[str, Any] | None = None,
        timeout: ClientTimeout | None = DEFAULT_TIMEOUT,
    ) -> Response:
        """Handle a DELETE request to Supervisor."""
        return await self._request(
            HTTPMethod.DELETE,
            uri,
            params=params,
            response_type=ResponseType.NONE,
            json=json,
            timeout=timeout,
        )

    async def close(self) -> None:
        """Close open client session."""
        if self.session and self._close_session:
            await self.session.close()


class _SupervisorComponentClient:
    """Common ancestor for all component clients of supervisor."""

    def __init__(self, client: _SupervisorClient) -> None:
        """Initialize sub module with client for API calls."""
        self._client = client
