"""Calendar platform for TaskMate.

Exposes one read-only ``calendar.taskmate_<child>`` entity per child. Events are
derived live from existing TaskMate data — there is no second store and nothing
to keep in sync:

  * chore occurrences come from the recurrence/assignment engine
    (``_is_chore_scheduled_for_date`` + ``_compute_active_children``), rendered
    as timed events inside the configured time-of-day window or as all-day
    events when the chore is "anytime";
  * away / unavailable blocks come from ``_is_child_on_vacation`` (#525),
    coalesced into multi-day all-day events. On an away day the child's chores
    are hidden, mirroring the rest of the integration.

Read-only for now: completing a chore from the calendar is intentionally not
supported.
"""

from __future__ import annotations

import logging
from datetime import date, datetime, timedelta

from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util

from .const import DOMAIN
from .coordinator import TaskMateCoordinator
from .entity import taskmate_device_info
from .models import Child, Chore

_LOGGER = logging.getLogger(__name__)

# How far ahead the `event` (next-up) property scans for the soonest event.
_NEXT_EVENT_HORIZON_DAYS = 60


async def async_setup_entry(
    hass: HomeAssistant,
    entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Set up one calendar entity per child, adding more as children are created."""
    coordinator: TaskMateCoordinator = hass.data[DOMAIN][entry.entry_id]
    tracked: set[str] = set()

    def _new_entities() -> list[TaskMateCalendar]:
        out: list[TaskMateCalendar] = []
        for child in coordinator.data.get("children", []):
            if child.id in tracked:
                continue
            tracked.add(child.id)
            out.append(TaskMateCalendar(coordinator, entry, child))
        return out

    async_add_entities(_new_entities())

    @callback
    def _async_add_new() -> None:
        new = _new_entities()
        if new:
            async_add_entities(new)

    coordinator.async_add_listener(_async_add_new)


def _chore_applies_to_child(coordinator: TaskMateCoordinator, chore: Chore, child_id: str, day: date) -> bool:
    """True if ``chore`` is scheduled for ``child_id`` on ``day``.

    Combines the recurrence schedule with the assignment engine so the calendar
    matches who would actually see the chore — without consulting completion
    state (the calendar projects the schedule, not today's done/not-done).
    """
    if not getattr(chore, "enabled", True):
        return False
    if getattr(chore, "assignment_mode", "everyone") == "unassigned":
        return False
    if not coordinator._is_chore_scheduled_for_date(chore, day):
        return False
    active = coordinator._compute_active_children(chore, day)
    if active:
        return child_id in active
    # Empty active set means an unrestricted "everyone" chore (no assigned_to).
    return not chore.assigned_to


def _chore_description(chore: Chore) -> str:
    """Compact one-line description for a chore event."""
    parts = ["TaskMate chore"]
    pts = getattr(chore, "points", 0) or 0
    parts.append(f"{pts} pts")
    cat = getattr(chore, "time_category", "anytime") or "anytime"
    if cat != "anytime":
        parts.append(cat)
    return " · ".join(parts)


def _as_local_dt(value: date | datetime) -> datetime:
    """Normalise a CalendarEvent start/end to an aware local datetime for sorting."""
    if isinstance(value, datetime):
        if value.tzinfo is None:
            return value.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE)
        return value
    return datetime(value.year, value.month, value.day, tzinfo=dt_util.DEFAULT_TIME_ZONE)


class TaskMateCalendar(CoordinatorEntity, CalendarEntity):
    """A read-only calendar of one child's chores and away periods."""

    _attr_icon = "mdi:calendar-account"

    def __init__(
        self,
        coordinator: TaskMateCoordinator,
        entry: ConfigEntry,
        child: Child,
    ) -> None:
        super().__init__(coordinator)
        self._entry = entry
        self._child_id = child.id
        self._attr_unique_id = f"{entry.entry_id}_{child.id}_calendar"
        self._attr_name = f"TaskMate {child.name}"

    @property
    def device_info(self) -> DeviceInfo:
        return taskmate_device_info(self._entry.entry_id)

    @property
    def _child(self) -> Child | None:
        return self.coordinator.storage.get_child(self._child_id)

    @property
    def available(self) -> bool:
        return self._child is not None and super().available

    @property
    def event(self) -> CalendarEvent | None:
        """The current or next upcoming event (HA shows this as the entity state)."""
        child = self._child
        if not child:
            return None
        now = dt_util.now()
        today = now.date()
        events = self._build_events(child, today, today + timedelta(days=_NEXT_EVENT_HORIZON_DAYS))
        upcoming = [e for e in events if _as_local_dt(e.end) > now]
        upcoming.sort(key=lambda e: _as_local_dt(e.start))
        return upcoming[0] if upcoming else None

    async def async_get_events(
        self, hass: HomeAssistant, start_date: datetime, end_date: datetime
    ) -> list[CalendarEvent]:
        """Return all events overlapping the requested range."""
        child = self._child
        if not child:
            return []
        return self._build_events(child, start_date.date(), end_date.date())

    def _build_events(self, child: Child, start_day: date, end_day: date) -> list[CalendarEvent]:
        coord = self.coordinator
        events: list[CalendarEvent] = []

        # ---- away blocks: coalesce consecutive away days into one all-day event ----
        run_start: date | None = None
        run_label: str | None = None
        day = start_day
        while day <= end_day:
            if coord._is_child_on_vacation(child, day):
                if run_start is None:
                    period = coord.active_vacation(day)
                    run_label = (period or {}).get("name") or ""
                    run_start = day
            elif run_start is not None:
                events.append(_away_event(run_start, day, run_label))
                run_start = None
            day += timedelta(days=1)
        if run_start is not None:
            events.append(_away_event(run_start, end_day + timedelta(days=1), run_label))

        # ---- chore occurrences (skipped on away days) ----
        chores = coord.storage.get_chores()
        tz = dt_util.DEFAULT_TIME_ZONE
        day = start_day
        while day <= end_day:
            if not coord._is_child_on_vacation(child, day):
                for chore in chores:
                    if not _chore_applies_to_child(coord, chore, child.id, day):
                        continue
                    window = coord._time_category_window(getattr(chore, "time_category", "anytime"), day)
                    desc = _chore_description(chore)
                    if window is None:
                        events.append(
                            CalendarEvent(
                                start=day,
                                end=day + timedelta(days=1),
                                summary=chore.name,
                                description=desc,
                            )
                        )
                    else:
                        start_dt, end_dt = window
                        events.append(
                            CalendarEvent(
                                start=start_dt.replace(tzinfo=tz),
                                end=end_dt.replace(tzinfo=tz),
                                summary=chore.name,
                                description=desc,
                            )
                        )
            day += timedelta(days=1)

        return events


def _away_event(start_day: date, end_day_exclusive: date, label: str | None) -> CalendarEvent:
    """Build a coalesced all-day 'away' event over [start, end)."""
    summary = f"Away — {label}" if label else "Away"
    return CalendarEvent(
        start=start_day,
        end=end_day_exclusive,
        summary=summary,
        description="Unavailable — streak paused and chores hidden.",
    )
