"""Archive & auto-delete retention policy (v2.10.0).

Two parts:

* **Pure decisions** — :func:`should_auto_archive` / :func:`should_auto_delete`
  reason over a single (merged) task dict + the global day thresholds + an
  injected ``today``. No Home Assistant imports, so every branch is unit-testable
  with plain dicts.
* **The sweep** — :func:`async_run_retention_sweep` walks every object entry once
  (wired to a daily timer in ``__init__.async_setup``), applies the archives in
  one ConfigEntry write per entry, deletes the eligible tasks via the shared
  ``websocket.tasks.async_delete_task`` helper, and reloads each touched entry so
  its entities reflect the new state.

Policy (locked design):

* Auto-archive applies to **completed one-off tasks only** — recurring / sensor
  tasks never reach a terminal "done" state, so they are archived manually only.
* Auto-delete applies to **auto-archived** one-offs only (``archived_reason ==
  "auto"``). A manually archived item is never auto-deleted — deleting a manual
  archive stays an explicit user action.
* Both thresholds are "0 = disabled" (archive) / "0 = never" (delete).
"""

from __future__ import annotations

import logging
from datetime import date
from typing import Any

from homeassistant.core import HomeAssistant
from homeassistant.util import dt as dt_util

from ..const import ARCHIVE_REASON_AUTO

_LOGGER = logging.getLogger(__name__)


def _to_date(value: Any) -> date | None:
    """Coerce a stored date / ISO timestamp string to a ``date`` (or None).

    Accepts both ``"2026-06-24"`` (last_performed) and a full ISO timestamp
    ``"2026-06-24T12:00:00+00:00"`` (archived_at) — the leading ``YYYY-MM-DD``
    is all the day-granular policy needs.
    """
    if not isinstance(value, str) or len(value) < 10:
        return None
    try:
        return date.fromisoformat(value[:10])
    except ValueError:
        return None


def is_completed_oneoff(task: dict[str, Any]) -> bool:
    """True iff ``task`` is a one-off (``one_time`` recurrence) that's been done."""
    # Local import keeps this module HA-free for the pure-function tests.
    from .schedule import KIND_ONE_TIME, Schedule

    return Schedule.parse(task).kind == KIND_ONE_TIME and bool(task.get("last_performed"))


def should_auto_archive(task: dict[str, Any], *, archive_days: int, today: date) -> bool:
    """Decide whether a (merged) task should be auto-archived now.

    True only for an active (not-yet-archived) completed one-off whose completion
    is at least ``archive_days`` days in the past. ``archive_days <= 0`` disables
    auto-archive entirely.
    """
    if archive_days <= 0:
        return False
    if task.get("archived_at") is not None:
        return False
    if not is_completed_oneoff(task):
        return False
    last_performed = _to_date(task.get("last_performed"))
    if last_performed is None:
        return False
    return (today - last_performed).days >= archive_days


def should_auto_delete(task: dict[str, Any], *, delete_days: int, today: date) -> bool:
    """Decide whether an auto-archived task should be auto-deleted now.

    True only for a task archived **automatically** (``archived_reason == "auto"``
    — which, by construction, is always a completed one-off) at least
    ``delete_days`` days ago. ``delete_days <= 0`` means "never delete". Manual /
    object-cascade archives are deliberately excluded.
    """
    if delete_days <= 0:
        return False
    if task.get("archived_at") is None:
        return False
    if task.get("archived_reason") != ARCHIVE_REASON_AUTO:
        return False
    archived_on = _to_date(task.get("archived_at"))
    if archived_on is None:
        return False
    return (today - archived_on).days >= delete_days


def _coerce_int(value: Any, default: int) -> int:
    """Best-effort int coercion (settings come through the WS as int already)."""
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


def _global_options(hass: HomeAssistant) -> dict[str, Any]:
    """Return the global entry's options (or data), or {} when absent."""
    from .global_options import get_global_options

    return dict(get_global_options(hass))


def _merged_tasks(entry: Any) -> dict[str, Any]:
    """Static (ConfigEntry) + dynamic (Store) task data for an object entry."""

    from .aggregate import merged_tasks

    return merged_tasks(entry)


async def async_run_retention_sweep(hass: HomeAssistant) -> None:
    """Auto-archive overdue-done one-offs and auto-delete aged auto-archives.

    Idempotent and cheap: only writes / reloads an entry that actually has work
    this pass. Safe to call from a daily timer or directly from a test.
    """
    from ..const import (
        ARCHIVE_REASON_AUTO as _REASON_AUTO,
    )
    from ..const import (
        CONF_ARCHIVE_ONEOFF_DAYS,
        CONF_DELETE_ARCHIVED_ONEOFF_DAYS,
        CONF_TASKS,
        DEFAULT_ARCHIVE_ONEOFF_DAYS,
        DEFAULT_DELETE_ARCHIVED_ONEOFF_DAYS,
    )
    from .aggregate import get_object_entries

    opts = _global_options(hass)
    archive_days = _coerce_int(
        opts.get(CONF_ARCHIVE_ONEOFF_DAYS, DEFAULT_ARCHIVE_ONEOFF_DAYS),
        DEFAULT_ARCHIVE_ONEOFF_DAYS,
    )
    delete_days = _coerce_int(
        opts.get(CONF_DELETE_ARCHIVED_ONEOFF_DAYS, DEFAULT_DELETE_ARCHIVED_ONEOFF_DAYS),
        DEFAULT_DELETE_ARCHIVED_ONEOFF_DAYS,
    )
    if archive_days <= 0 and delete_days <= 0:
        return

    today = dt_util.now().date()
    now_iso = dt_util.now().isoformat()

    for entry in get_object_entries(hass):
        merged = _merged_tasks(entry)
        # The two sets are disjoint by construction: archive needs archived_at
        # None; delete needs archived_at set — a task can't be both this pass.
        to_archive = [tid for tid, td in merged.items() if should_auto_archive(td, archive_days=archive_days, today=today)]
        to_delete = [tid for tid, td in merged.items() if should_auto_delete(td, delete_days=delete_days, today=today)]
        if not to_archive and not to_delete:
            continue

        if to_archive:
            from .entry_tasks import write_tasks

            existing = entry.data.get(CONF_TASKS, {})
            updates: dict[str, dict[str, Any]] = {}
            for tid in to_archive:
                if tid not in existing:
                    continue
                td = dict(existing[tid])
                td["archived_at"] = now_iso
                td["archived_reason"] = _REASON_AUTO
                updates[tid] = td
            write_tasks(hass, entry, updates)
            _LOGGER.info(
                "Auto-archived %d completed one-off task(s) in %s",
                len(to_archive),
                entry.title,
            )

        if to_delete:
            from ..websocket.tasks import async_delete_task

            deleted = 0
            for tid in to_delete:
                if await async_delete_task(hass, entry, tid):
                    deleted += 1
            if deleted:
                _LOGGER.info(
                    "Auto-deleted %d archived one-off task(s) in %s",
                    deleted,
                    entry.title,
                )

        # Reload once so entities reflect the archive (inert) / delete (gone).
        await hass.config_entries.async_reload(entry.entry_id)
