"""Support for device trackers which integrates with other components."""

from collections.abc import Callable
from dataclasses import asdict, dataclass
from typing import Any, Self, override

import voluptuous as vol

from homeassistant.components import zone
from homeassistant.components.device_tracker import (
    DOMAIN as DEVICE_TRACKER_DOMAIN,
    ENTITY_ID_FORMAT,
    TrackerEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import (
    AddConfigEntryEntitiesCallback,
    AddEntitiesCallback,
)
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from . import TriggerUpdateCoordinator, validators as template_validators
from .entity import AbstractTemplateEntity
from .helpers import (
    async_setup_template_entry,
    async_setup_template_platform,
    async_setup_template_preview,
)
from .schemas import (
    TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
    make_template_entity_common_modern_schema,
)
from .template_entity import TemplateEntity
from .trigger_entity import TriggerEntity

DEFAULT_NAME = "Template Device Tracker"

CONF_IN_ZONES = "in_zones"
CONF_LOCATION_ACCURACY = "location_accuracy"


def _validate_in_zones_or_lat_and_lon(obj: dict) -> dict:
    if CONF_IN_ZONES not in obj:
        if CONF_LATITUDE not in obj or CONF_LONGITUDE not in obj:
            raise vol.Invalid(
                f"Either '{CONF_IN_ZONES}' or both '{CONF_LATITUDE}' and '{CONF_LONGITUDE}' must be specified"
            )
    elif (CONF_LATITUDE in obj and CONF_LONGITUDE not in obj) or (
        CONF_LATITUDE not in obj and CONF_LONGITUDE in obj
    ):
        raise vol.Invalid(
            f"Both '{CONF_LATITUDE}' and '{CONF_LONGITUDE}' must be specified"
        )

    return obj


def validate_in_zones(
    entity: AbstractTemplateTracker,
) -> Callable[[Any], list[str] | None]:
    """Convert the result to a list of entity_ids.

    This ensures the result is a list of zone entity_ids.
    All other values that are not lists will result in None.
    """

    def convert(result: Any) -> list[str] | None:
        if template_validators.check_result_for_none(result):
            return None

        if not isinstance(result, list):
            template_validators.log_validation_result_error(
                entity,
                CONF_IN_ZONES,
                result,
                "expected a list of zone entity_ids",
            )
            return None

        zone_entity_ids = []
        failed = []
        for v in result:
            try:
                zone_entity_ids.append(
                    vol.All(cv.entity_id, cv.entity_domain(zone.DOMAIN))(v)
                )
            except vol.Invalid:
                failed.append(v)

        if failed:
            template_validators.log_validation_result_error(
                entity,
                CONF_IN_ZONES,
                failed,
                "expected a list of zone entity_ids",
            )

        return zone_entity_ids

    return convert


TRACKER_COMMON_SCHEMA = vol.Schema(
    {
        vol.Optional(CONF_IN_ZONES): cv.template,
        vol.Optional(CONF_LATITUDE): cv.template,
        vol.Optional(CONF_LOCATION_ACCURACY): cv.template,
        vol.Optional(CONF_LONGITUDE): cv.template,
    }
)


TRACKER_YAML_SCHEMA = vol.All(
    _validate_in_zones_or_lat_and_lon,
    TRACKER_COMMON_SCHEMA.extend(
        make_template_entity_common_modern_schema(
            DEVICE_TRACKER_DOMAIN, DEFAULT_NAME
        ).schema
    ),
)

TRACKER_CONFIG_ENTRY_SCHEMA = vol.All(
    _validate_in_zones_or_lat_and_lon,
    TRACKER_COMMON_SCHEMA.extend(TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema),
)


async def async_setup_platform(
    hass: HomeAssistant,
    config: ConfigType,
    async_add_entities: AddEntitiesCallback,
    discovery_info: DiscoveryInfoType | None = None,
) -> None:
    """Set up the template device trackers."""
    await async_setup_template_platform(
        hass,
        DEVICE_TRACKER_DOMAIN,
        config,
        StateTrackerEntity,
        TriggerTrackerEntity,
        async_add_entities,
        discovery_info,
    )


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Initialize config entry."""
    await async_setup_template_entry(
        hass,
        config_entry,
        async_add_entities,
        StateTrackerEntity,
        TRACKER_CONFIG_ENTRY_SCHEMA,
    )


@callback
def async_create_preview_tracker(
    hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateTrackerEntity:
    """Create a preview device tracker."""
    return async_setup_template_preview(
        hass,
        name,
        config,
        StateTrackerEntity,
        TRACKER_CONFIG_ENTRY_SCHEMA,
    )


@dataclass(kw_only=True)
class TrackerExtraStoredData(ExtraStoredData):
    """Holds extra stored data for template tracker entities."""

    in_zones: list[str] | None
    latitude: float | None
    longitude: float | None
    location_accuracy: float

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

    @classmethod
    def from_dict(cls, restored: dict[str, Any]) -> Self | None:
        """Initialize a stored tracker state from a dict."""
        try:
            return cls(
                in_zones=restored["in_zones"],
                latitude=restored["latitude"],
                longitude=restored["longitude"],
                location_accuracy=restored["location_accuracy"],
            )
        except KeyError:
            return None


class AbstractTemplateTracker(AbstractTemplateEntity, TrackerEntity, RestoreEntity):
    """Representation of a template device tracker features."""

    _entity_id_format = ENTITY_ID_FORMAT
    _restore_state_extra_data = TrackerExtraStoredData
    _restore_state_properties = ("_attr_in_zones",)

    # The super init is not called because TemplateEntity
    # and TriggerEntity will call
    # AbstractTemplateEntity.__init__. This ensures that
    # the __init__ on AbstractTemplateEntity is not
    # called twice.
    def __init__(self) -> None:  # pylint: disable=super-init-not-called
        """Initialize the features."""

        self.setup_template(
            CONF_IN_ZONES,
            "_attr_in_zones",
            validate_in_zones(self),
        )
        self.setup_template(
            CONF_LATITUDE,
            "_attr_latitude",
            template_validators.number(self, CONF_LATITUDE, -90.0, 90.0),
        )
        self.setup_template(
            CONF_LONGITUDE,
            "_attr_longitude",
            template_validators.number(self, CONF_LONGITUDE, -180.0, 180.0),
        )
        self.setup_template(
            CONF_LOCATION_ACCURACY,
            "_attr_location_accuracy",
            on_update=self._update_location_accuracy,
            none_on_template_error=False,
        )

        self._location_accuracy_validator = template_validators.number(
            self, CONF_LOCATION_ACCURACY, 0.0
        )

    def _update_location_accuracy(self, value: float | None) -> None:
        """Update the location accuracy."""
        self._attr_location_accuracy = self._location_accuracy_validator(value) or 0.0

    @property
    @override
    def extra_restore_state_data(self) -> TrackerExtraStoredData:
        """Return tracker specific state data to be restored."""
        return TrackerExtraStoredData(
            in_zones=self._attr_in_zones,
            latitude=self._attr_latitude,
            longitude=self._attr_longitude,
            location_accuracy=self._attr_location_accuracy,
        )

    @override
    def restore_extra_data(self, extra_data: TrackerExtraStoredData) -> None:
        """Restore the extra data."""
        self._attr_in_zones = extra_data.in_zones
        self._attr_latitude = extra_data.latitude
        self._attr_longitude = extra_data.longitude
        self._attr_location_accuracy = extra_data.location_accuracy


class StateTrackerEntity(TemplateEntity, AbstractTemplateTracker):
    """Representation of a Template device tracker."""

    _attr_should_poll = False

    def __init__(
        self,
        hass: HomeAssistant,
        config: ConfigType,
        unique_id: str | None,
    ) -> None:
        """Initialize the Template device tracker."""
        TemplateEntity.__init__(self, hass, config, unique_id)
        AbstractTemplateTracker.__init__(self)


class TriggerTrackerEntity(TriggerEntity, AbstractTemplateTracker):
    """Tracker entity based on trigger data."""

    domain = DEVICE_TRACKER_DOMAIN

    def __init__(
        self,
        hass: HomeAssistant,
        coordinator: TriggerUpdateCoordinator,
        config: ConfigType,
    ) -> None:
        """Initialize the entity."""
        TriggerEntity.__init__(self, hass, coordinator, config)
        AbstractTemplateTracker.__init__(self)
