"""Homeassistant Client."""

from __future__ import annotations


import asyncio
import json
from dataclasses import dataclass
from importlib import metadata
from typing import TYPE_CHECKING, Any, Self

from aiohttp import ClientSession, ClientConnectionError, InvalidUrlClientError
from aiohttp.hdrs import METH_GET, METH_POST, METH_PUT, METH_DELETE
from mashumaro.codecs.orjson import ORJSONDecoder
from yarl import URL

from aiomealie.exceptions import (
    MealieConnectionError,
    MealieError,
    MealieAuthenticationError,
    MealieValidationError,
    MealieNotFoundError,
    MealieBadRequestError,
)
from aiomealie.models import (
    About,
    GroupSummary,
    Mealplan,
    MealplanResponse,
    OrderDirection,
    RecipesResponse,
    ShoppingListsResponse,
    MutateShoppingItem,
    ShoppingItemsOrderBy,
    ShoppingItemsResponse,
    StartupInfo,
    Theme,
    UserInfo,
    Recipe,
    Statistics,
    MealplanEntryType,
)

if TYPE_CHECKING:
    from datetime import date

VERSION = metadata.version(__package__)


@dataclass
class MealieClient:
    """Main class for handling connections with Mealie."""

    api_host: str
    token: str | None = None
    session: ClientSession | None = None
    request_timeout: int = 10
    _close_session: bool = False
    _version: str | None = None

    async def _request(
        self,
        method: str,
        uri: str,
        *,
        data: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> str:
        """Handle a request to Mealie."""
        url = URL(self.api_host).joinpath(uri)

        headers = {
            "User-Agent": f"AioMealie/{VERSION}",
            "Accept": "application/json, text/plain, */*",
        }
        if self.token:
            headers["Authorization"] = f"Bearer {self.token}"

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

        try:
            async with asyncio.timeout(self.request_timeout):
                response = await self.session.request(
                    method, url, headers=headers, params=params, json=data
                )
        except asyncio.TimeoutError as exception:
            msg = "Timeout occurred while connecting to Mealie"
            raise MealieConnectionError(msg) from exception
        except ClientConnectionError as exception:
            msg = "Client connection error while connecting to Mealie"
            raise MealieConnectionError(msg) from exception
        except InvalidUrlClientError as exception:
            msg = "Invalid URL found while connecting to Mealie"
            raise MealieConnectionError(msg) from exception

        if response.status == 400:
            msg = "Bad request to Mealie"
            raise MealieBadRequestError(
                msg,
            )

        if response.status == 401:
            msg = "Unauthorized access to Mealie"
            raise MealieAuthenticationError(msg)

        if response.status == 422:
            msg = "Mealie validation error"
            raise MealieValidationError(
                msg,
            )

        if response.status == 404:
            msg = "Object not found in Mealie"
            raise MealieNotFoundError(
                msg,
            )

        content_type = response.headers.get("Content-Type", "")

        if "application/json" not in content_type:
            msg = "Unexpected response from Mealie"
            raise MealieError(
                msg,
                {"Content-Type": content_type},
            )

        return await response.text()

    async def _get(self, uri: str, params: dict[str, Any] | None = None) -> str:
        """Handle a GET request to Mealie."""
        return await self._request(METH_GET, uri, params=params)

    async def _post(
        self,
        uri: str,
        data: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> str:
        """Handle a POST request to Mealie."""
        return await self._request(METH_POST, uri, data=data, params=params)

    async def _put(
        self,
        uri: str,
        data: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> str:
        """Handle a PUT request to Mealie."""
        return await self._request(METH_PUT, uri, data=data, params=params)

    async def _delete(
        self,
        uri: str,
        data: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> str:
        """Handle a DELETE request to Mealie."""
        return await self._request(METH_DELETE, uri, data=data, params=params)

    @property
    async def version(self) -> str:
        """Return the version, retrieve from get_about if not stored."""
        if not self._version:
            about = await self.get_about()
            self._version = about.version
        return self._version

    async def get_startup_info(self) -> StartupInfo:
        """Get startup info."""
        response = await self._get("api/app/about/startup-info")
        return StartupInfo.from_json(response)

    async def get_about(self) -> About:
        """Get about info."""
        response = await self._get("api/app/about")
        return About.from_json(response)

    async def get_user_info(self) -> UserInfo:
        """Get user info."""
        response = await self._get("api/users/self")
        return UserInfo.from_json(response)

    async def get_groups_self(self) -> GroupSummary:
        """Get groups self."""
        response = await self._get("api/groups/self")
        return GroupSummary.from_json(response)

    async def get_theme(self) -> Theme:
        """Get theme."""
        response = await self._get("api/app/about/theme")
        return Theme.from_json(response)

    async def get_recipes(
        self, search: str | None = None, per_page: int = 50
    ) -> RecipesResponse:
        """Get recipes."""
        params: dict[str, Any] = {}
        params["perPage"] = per_page
        if search:
            params["search"] = search
        response = await self._get("api/recipes", params=params)
        return RecipesResponse.from_json(response)

    async def get_recipe(self, recipe_id_or_slug: str) -> Recipe:
        """Get recipe."""
        response = await self._get(f"api/recipes/{recipe_id_or_slug}")
        return Recipe.from_json(response)

    async def import_recipe(self, url: str, include_tags: bool = False) -> Recipe:
        """Import a recipe."""
        data = {"url": url, "include_tags": include_tags}
        response = await self._post("api/recipes/create/url", data)
        return await self.get_recipe(json.loads(response))

    async def get_mealplan_today(self) -> list[Mealplan]:
        """Get mealplan."""
        response = await self._get("api/households/mealplans/today")
        return ORJSONDecoder(list[Mealplan]).decode(response)

    async def get_mealplans(
        self,
        start_date: date | None = None,
        end_date: date | None = None,
    ) -> MealplanResponse:
        """Get mealplans."""
        params: dict[str, Any] = {}
        if start_date:
            params["start_date"] = start_date.isoformat()
        if end_date:
            params["end_date"] = end_date.isoformat()
        params["perPage"] = -1
        response = await self._get("api/households/mealplans", params)
        return MealplanResponse.from_json(response)

    async def get_shopping_lists(self) -> ShoppingListsResponse:
        """Get shopping lists."""
        params: dict[str, Any] = {}
        params["perPage"] = -1
        response = await self._get("api/households/shopping/lists", params)
        return ShoppingListsResponse.from_json(response)

    async def get_shopping_items(
        self,
        shopping_list_id: str,
    ) -> ShoppingItemsResponse:
        """Get shopping items."""
        params: dict[str, Any] = {}
        params["queryFilter"] = f"shoppingListId={shopping_list_id}"
        params["orderBy"] = ShoppingItemsOrderBy.POSITION
        params["orderDirection"] = OrderDirection.ASCENDING
        params["perPage"] = -1
        response = await self._get("api/households/shopping/items", params)
        return ShoppingItemsResponse.from_json(response)

    async def add_shopping_item(
        self,
        item: MutateShoppingItem,
    ) -> None:
        """Add a shopping item."""

        await self._post(
            "api/households/shopping/items", data=item.to_dict(omit_none=True)
        )

    async def update_shopping_item(
        self, item_id: str, item: MutateShoppingItem
    ) -> None:
        """Update a shopping item."""

        await self._put(
            f"api/households/shopping/items/{item_id}",
            data=item.to_dict(omit_none=True),
        )

    async def delete_shopping_item(self, item_id: str) -> None:
        """Delete shopping item."""

        await self._delete(
            f"api/households/shopping/items/{item_id}",
        )

    async def get_statistics(self) -> Statistics:
        """Get statistics."""

        response = await self._get("api/households/statistics")
        return Statistics.from_json(response)

    async def random_mealplan(
        self, at: date, entry_type: MealplanEntryType
    ) -> Mealplan:
        """Set a random mealplan for a specific date."""
        response = await self._post(
            "api/households/mealplans/random",
            {
                "date": at.isoformat(),
                "entryType": entry_type.value,
            },
        )
        return Mealplan.from_json(response)

    async def set_mealplan(
        self,
        at: date,
        entry_type: MealplanEntryType,
        *,
        recipe_id: str | None = None,
        note_title: str | None = None,
        note_text: str | None = None,
    ) -> Mealplan:
        """Set a mealplan for a specific date."""
        data = {
            "date": at.isoformat(),
            "entryType": entry_type.value,
        }
        if recipe_id:
            data["recipeId"] = recipe_id
        if note_title:
            data["title"] = note_title
            if note_text:
                data["text"] = note_text
        response = await self._post("api/households/mealplans", data)
        return Mealplan.from_json(response)

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

    async def __aenter__(self) -> Self:
        """Async enter.

        Returns
        -------
            The MealieClient object.
        """
        return self

    async def __aexit__(self, *_exc_info: object) -> None:
        """Async exit.

        Args:
        ----
            _exc_info: Exec type.
        """
        await self.close()
