"""The exceptions used by Home Assistant."""

from __future__ import annotations

from collections.abc import Callable, Generator, Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from .util.event_type import EventType

if TYPE_CHECKING:
    from .core import Context


_function_cache: dict[str, Callable[[str, str, dict[str, str] | None], str]] = {}


def import_async_get_exception_message() -> (
    Callable[[str, str, dict[str, str] | None], str]
):
    """Return a method that can fetch a translated exception message.

    Defaults to English, requires translations to already be cached.
    """

    # pylint: disable-next=import-outside-toplevel
    from .helpers.translation import (
        async_get_exception_message as async_get_exception_message_import,
    )

    return async_get_exception_message_import


class HomeAssistantError(Exception):
    """General Home Assistant exception occurred."""

    _message: str | None = None
    generate_message: bool = False

    def __init__(
        self,
        *args: object,
        translation_domain: str | None = None,
        translation_key: str | None = None,
        translation_placeholders: dict[str, str] | None = None,
    ) -> None:
        """Initialize exception."""
        if not args and translation_key and translation_domain:
            self.generate_message = True
            args = (translation_key,)

        super().__init__(*args)
        self.translation_domain = translation_domain
        self.translation_key = translation_key
        self.translation_placeholders = translation_placeholders

    def __str__(self) -> str:
        """Return exception message.

        If no message was passed to `__init__`, the exception message is generated from
        the translation_key. The message will be in English, regardless of the configured
        language.
        """

        if self._message:
            return self._message

        if not self.generate_message:
            self._message = super().__str__()
            return self._message

        if TYPE_CHECKING:
            assert self.translation_key is not None
            assert self.translation_domain is not None

        if "async_get_exception_message" not in _function_cache:
            _function_cache["async_get_exception_message"] = (
                import_async_get_exception_message()
            )

        self._message = _function_cache["async_get_exception_message"](
            self.translation_domain, self.translation_key, self.translation_placeholders
        )
        return self._message


class ConfigValidationError(HomeAssistantError, ExceptionGroup[Exception]):
    """A validation exception occurred when validating the configuration."""

    def __init__(
        self,
        message_translation_key: str,
        exceptions: list[Exception],
        translation_domain: str | None = None,
        translation_placeholders: dict[str, str] | None = None,
    ) -> None:
        """Initialize exception."""
        super().__init__(
            *(message_translation_key, exceptions),
            translation_domain=translation_domain,
            translation_key=message_translation_key,
            translation_placeholders=translation_placeholders,
        )
        self.generate_message = True


class ServiceValidationError(HomeAssistantError):
    """A validation exception occurred when calling a service."""


class InvalidEntityFormatError(HomeAssistantError):
    """When an invalid formatted entity is encountered."""


class NoEntitySpecifiedError(HomeAssistantError):
    """When no entity is specified."""


class TemplateError(HomeAssistantError):
    """Error during template rendering."""

    def __init__(self, exception: Exception | str) -> None:
        """Init the error."""
        if isinstance(exception, str):
            super().__init__(exception)
        else:
            super().__init__(f"{exception.__class__.__name__}: {exception}")


@dataclass(slots=True)
class ConditionError(HomeAssistantError):
    """Error during condition evaluation."""

    type: str

    @staticmethod
    def _indent(indent: int, message: str) -> str:
        """Return indentation."""
        return "  " * indent + message

    def output(self, indent: int) -> Generator[str]:
        """Yield an indented representation."""
        raise NotImplementedError

    def __str__(self) -> str:
        """Return string representation."""
        return "\n".join(list(self.output(indent=0)))


@dataclass(slots=True)
class ConditionErrorMessage(ConditionError):
    """Condition error message."""

    # A message describing this error
    message: str

    def output(self, indent: int) -> Generator[str]:
        """Yield an indented representation."""
        yield self._indent(indent, f"In '{self.type}' condition: {self.message}")


@dataclass(slots=True)
class ConditionErrorIndex(ConditionError):
    """Condition error with index."""

    # The zero-based index of the failed condition, for conditions with multiple parts
    index: int
    # The total number of parts in this condition, including non-failed parts
    total: int
    # The error that this error wraps
    error: ConditionError

    def output(self, indent: int) -> Generator[str]:
        """Yield an indented representation."""
        if self.total > 1:
            yield self._indent(
                indent, f"In '{self.type}' (item {self.index+1} of {self.total}):"
            )
        else:
            yield self._indent(indent, f"In '{self.type}':")

        yield from self.error.output(indent + 1)


@dataclass(slots=True)
class ConditionErrorContainer(ConditionError):
    """Condition error with subconditions."""

    # List of ConditionErrors that this error wraps
    errors: Sequence[ConditionError]

    def output(self, indent: int) -> Generator[str]:
        """Yield an indented representation."""
        for item in self.errors:
            yield from item.output(indent)


class IntegrationError(HomeAssistantError):
    """Base class for platform and config entry exceptions."""

    def __str__(self) -> str:
        """Return a human readable error."""
        return super().__str__() or str(self.__cause__)


class PlatformNotReady(IntegrationError):
    """Error to indicate that platform is not ready."""


class ConfigEntryError(IntegrationError):
    """Error to indicate that config entry setup has failed."""


class ConfigEntryNotReady(IntegrationError):
    """Error to indicate that config entry is not ready."""


class ConfigEntryAuthFailed(IntegrationError):
    """Error to indicate that config entry could not authenticate."""


class InvalidStateError(HomeAssistantError):
    """When an invalid state is encountered."""


class Unauthorized(HomeAssistantError):
    """When an action is unauthorized."""

    def __init__(
        self,
        context: Context | None = None,
        user_id: str | None = None,
        entity_id: str | None = None,
        config_entry_id: str | None = None,
        perm_category: str | None = None,
        permission: str | None = None,
    ) -> None:
        """Unauthorized error."""
        super().__init__(self.__class__.__name__)
        self.context = context

        if user_id is None and context is not None:
            user_id = context.user_id

        self.user_id = user_id
        self.entity_id = entity_id
        self.config_entry_id = config_entry_id
        # Not all actions have an ID (like adding config entry)
        # We then use this fallback to know what category was unauth
        self.perm_category = perm_category
        self.permission = permission


class UnknownUser(Unauthorized):
    """When call is made with user ID that doesn't exist."""


class ServiceNotFound(ServiceValidationError):
    """Raised when a service is not found."""

    def __init__(self, domain: str, service: str) -> None:
        """Initialize error."""
        super().__init__(
            translation_domain="homeassistant",
            translation_key="service_not_found",
            translation_placeholders={"domain": domain, "service": service},
        )
        self.domain = domain
        self.service = service
        self.generate_message = True


class MaxLengthExceeded(HomeAssistantError):
    """Raised when a property value has exceeded the max character length."""

    def __init__(
        self, value: EventType[Any] | str, property_name: str, max_length: int
    ) -> None:
        """Initialize error."""
        if TYPE_CHECKING:
            value = str(value)
        super().__init__(
            translation_domain="homeassistant",
            translation_key="max_length_exceeded",
            translation_placeholders={
                "value": value,
                "property_name": property_name,
                "max_length": str(max_length),
            },
        )
        self.value = value
        self.property_name = property_name
        self.max_length = max_length
        self.generate_message = True


class DependencyError(HomeAssistantError):
    """Raised when dependencies cannot be setup."""

    def __init__(self, failed_dependencies: list[str]) -> None:
        """Initialize error."""
        super().__init__(
            f"Could not setup dependencies: {', '.join(failed_dependencies)}",
        )
        self.failed_dependencies = failed_dependencies