"""Component to allow setting text as platforms."""
from __future__ import annotations

from dataclasses import asdict, dataclass
from datetime import timedelta
from enum import StrEnum
import logging
import re
from typing import Any, final

import voluptuous as vol

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import MAX_LENGTH_STATE_STATE
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_validation import (  # noqa: F401
    PLATFORM_SCHEMA,
    PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
from homeassistant.helpers.typing import ConfigType

from .const import (
    ATTR_MAX,
    ATTR_MIN,
    ATTR_MODE,
    ATTR_PATTERN,
    ATTR_VALUE,
    DOMAIN,
    SERVICE_SET_VALUE,
)

SCAN_INTERVAL = timedelta(seconds=30)

ENTITY_ID_FORMAT = DOMAIN + ".{}"

MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)

_LOGGER = logging.getLogger(__name__)

__all__ = ["DOMAIN", "TextEntity", "TextEntityDescription", "TextMode"]


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
    """Set up Text entities."""
    component = hass.data[DOMAIN] = EntityComponent[TextEntity](
        _LOGGER, DOMAIN, hass, SCAN_INTERVAL
    )
    await component.async_setup(config)

    component.async_register_entity_service(
        SERVICE_SET_VALUE,
        {vol.Required(ATTR_VALUE): cv.string},
        _async_set_value,
    )

    return True


async def _async_set_value(entity: TextEntity, service_call: ServiceCall) -> None:
    """Service call wrapper to set a new value."""
    value = service_call.data[ATTR_VALUE]
    if len(value) < entity.min:
        raise ValueError(
            f"Value {value} for {entity.entity_id} is too short (minimum length"
            f" {entity.min})"
        )
    if len(value) > entity.max:
        raise ValueError(
            f"Value {value} for {entity.entity_id} is too long (maximum length {entity.max})"
        )
    if entity.pattern_cmp and not entity.pattern_cmp.match(value):
        raise ValueError(
            f"Value {value} for {entity.entity_id} doesn't match pattern {entity.pattern}"
        )
    await entity.async_set_value(value)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Set up a config entry."""
    component: EntityComponent[TextEntity] = hass.data[DOMAIN]
    return await component.async_setup_entry(entry)


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
    """Unload a config entry."""
    component: EntityComponent[TextEntity] = hass.data[DOMAIN]
    return await component.async_unload_entry(entry)


class TextMode(StrEnum):
    """Modes for text entities."""

    PASSWORD = "password"
    TEXT = "text"


@dataclass
class TextEntityDescription(EntityDescription):
    """A class that describes text entities."""

    native_min: int = 0
    native_max: int = MAX_LENGTH_STATE_STATE
    mode: TextMode = TextMode.TEXT
    pattern: str | None = None


class TextEntity(Entity):
    """Representation of a Text entity."""

    _entity_component_unrecorded_attributes = frozenset(
        {ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_PATTERN}
    )

    entity_description: TextEntityDescription
    _attr_mode: TextMode
    _attr_native_value: str | None
    _attr_native_min: int
    _attr_native_max: int
    _attr_pattern: str | None
    _attr_state: None = None
    __pattern_cmp: re.Pattern | None = None

    @property
    def capability_attributes(self) -> dict[str, Any]:
        """Return capability attributes."""
        return {
            ATTR_MODE: self.mode,
            ATTR_MIN: self.min,
            ATTR_MAX: self.max,
            ATTR_PATTERN: self.pattern,
        }

    @property
    @final
    def state(self) -> str | None:
        """Return the entity state."""
        if self.native_value is None:
            return None
        if len(self.native_value) < self.min:
            raise ValueError(
                f"Entity {self.entity_id} provides state {self.native_value} which is "
                f"too short (minimum length {self.min})"
            )
        if len(self.native_value) > self.max:
            raise ValueError(
                f"Entity {self.entity_id} provides state {self.native_value} which is "
                f"too long (maximum length {self.max})"
            )
        if self.pattern_cmp and not self.pattern_cmp.match(self.native_value):
            raise ValueError(
                f"Entity {self.entity_id} provides state {self.native_value} which "
                f"does not match expected pattern {self.pattern}"
            )
        return self.native_value

    @property
    def mode(self) -> TextMode:
        """Return the mode of the entity."""
        if hasattr(self, "_attr_mode"):
            return self._attr_mode
        if hasattr(self, "entity_description"):
            return self.entity_description.mode
        return TextMode.TEXT

    @property
    def native_min(self) -> int:
        """Return the minimum length of the value."""
        if hasattr(self, "_attr_native_min"):
            return self._attr_native_min
        if hasattr(self, "entity_description"):
            return self.entity_description.native_min
        return 0

    @property
    @final
    def min(self) -> int:
        """Return the minimum length of the value."""
        return max(self.native_min, 0)

    @property
    def native_max(self) -> int:
        """Return the maximum length of the value."""
        if hasattr(self, "_attr_native_max"):
            return self._attr_native_max
        if hasattr(self, "entity_description"):
            return self.entity_description.native_max
        return MAX_LENGTH_STATE_STATE

    @property
    @final
    def max(self) -> int:
        """Return the maximum length of the value."""
        return min(self.native_max, MAX_LENGTH_STATE_STATE)

    @property
    @final
    def pattern_cmp(self) -> re.Pattern | None:
        """Return a compiled pattern."""
        if self.pattern is None:
            self.__pattern_cmp = None
            return None
        if not self.__pattern_cmp or self.pattern != self.__pattern_cmp.pattern:
            self.__pattern_cmp = re.compile(self.pattern)
        return self.__pattern_cmp

    @property
    def pattern(self) -> str | None:
        """Return the regex pattern that the value must match."""
        if hasattr(self, "_attr_pattern"):
            return self._attr_pattern
        if hasattr(self, "entity_description"):
            return self.entity_description.pattern
        return None

    @property
    def native_value(self) -> str | None:
        """Return the value reported by the text."""
        return self._attr_native_value

    def set_value(self, value: str) -> None:
        """Change the value."""
        raise NotImplementedError()

    async def async_set_value(self, value: str) -> None:
        """Change the value."""
        await self.hass.async_add_executor_job(self.set_value, value)


@dataclass
class TextExtraStoredData(ExtraStoredData):
    """Object to hold extra stored data."""

    native_value: str | None
    native_min: int
    native_max: int

    def as_dict(self) -> dict[str, Any]:
        """Return a dict representation of the text data."""
        return asdict(self)

    @classmethod
    def from_dict(cls, restored: dict[str, Any]) -> TextExtraStoredData | None:
        """Initialize a stored text state from a dict."""
        try:
            return cls(
                restored["native_value"],
                restored["native_min"],
                restored["native_max"],
            )
        except KeyError:
            return None


class RestoreText(TextEntity, RestoreEntity):
    """Mixin class for restoring previous text state."""

    @property
    def extra_restore_state_data(self) -> TextExtraStoredData:
        """Return text specific state data to be restored."""
        return TextExtraStoredData(
            self.native_value,
            self.native_min,
            self.native_max,
        )

    async def async_get_last_text_data(self) -> TextExtraStoredData | None:
        """Restore attributes."""
        if (restored_last_extra_data := await self.async_get_last_extra_data()) is None:
            return None
        return TextExtraStoredData.from_dict(restored_last_extra_data.as_dict())