"""Persistent registry of delivery targets that are confirmed unreachable.

When a messaging platform reports that a target chat is permanently gone — a
deleted group (``Forbidden: the group chat was deleted``), a bot kicked/blocked,
or a deactivated user — re-sending to it on every cron tick or every fan-out
delivery wastes a send attempt against the platform's flood-control envelope and
spams the logs.  This registry lets the delivery layer short-circuit a target it
has already proven dead, while staying self-healing: any successful send to that
target clears the flag, so a user who re-adds the bot (or restores the chat)
recovers automatically with no manual cleanup.

Scope is deliberately narrow.  Only *whole-chat* deaths are recorded — the
``forbidden`` and chat-level ``not_found`` (``chat not found``) error kinds.
Thread/topic-level ``not_found`` is NOT recorded here: the adapters already
self-heal that by retrying without ``reply_to`` (see the Telegram adapter's
reply-target-deleted path), and a deleted topic does not mean the parent chat is
dead.

The store is a small JSON file under the active profile's HERMES_HOME so each
profile keeps its own dead set.  Reads/writes are best-effort: a corrupt or
unwritable file degrades to an in-memory-only registry rather than raising on
the delivery path.
"""

from __future__ import annotations

import json
import logging
import threading
import time
from pathlib import Path
from typing import Dict, Optional

from hermes_cli.config import get_hermes_home

logger = logging.getLogger(__name__)

# Error kinds (from gateway.platforms.base.classify_send_error) that mean the
# *whole chat* is unreachable, not a transient or thread-level problem.
_DEAD_ERROR_KINDS = frozenset({"forbidden", "not_found"})


def _normalize(platform: str, chat_id: str) -> str:
    """Canonical key for a (platform, chat_id) pair."""
    return f"{str(platform).strip().lower()}:{str(chat_id).strip()}"


class DeadTargetRegistry:
    """Thread-safe, persistent set of confirmed-dead delivery targets.

    Keyed on ``platform:chat_id``.  Stores the reason and a timestamp for
    observability.  Self-healing: :meth:`clear` (called on a successful send)
    removes the flag.
    """

    def __init__(self, path: Optional[Path] = None) -> None:
        self._lock = threading.RLock()
        self._dead: Dict[str, Dict[str, object]] = {}
        if path is not None:
            self._path = path
        else:
            self._path = get_hermes_home() / "gateway" / "dead_targets.json"
        self._load()

    # -- persistence -------------------------------------------------------

    def _load(self) -> None:
        try:
            if self._path.exists():
                raw = json.loads(self._path.read_text())
                if isinstance(raw, dict):
                    # Only keep well-shaped entries.
                    self._dead = {
                        k: v for k, v in raw.items() if isinstance(v, dict)
                    }
        except (OSError, ValueError) as exc:
            logger.debug("dead_targets: could not load %s (%s) — starting empty",
                         self._path, exc)
            self._dead = {}

    def _flush_locked(self) -> None:
        try:
            self._path.parent.mkdir(parents=True, exist_ok=True)
            tmp = self._path.with_suffix(self._path.suffix + ".tmp")
            tmp.write_text(json.dumps(self._dead, indent=2))
            tmp.replace(self._path)
        except OSError as exc:
            # Best-effort: keep the in-memory state, don't break delivery.
            logger.debug("dead_targets: could not persist %s (%s)", self._path, exc)

    # -- public API --------------------------------------------------------

    @staticmethod
    def is_dead_error_kind(error_kind: Optional[str]) -> bool:
        """Return True when ``error_kind`` denotes a permanent whole-chat death."""
        return bool(error_kind) and error_kind in _DEAD_ERROR_KINDS

    def is_dead(self, platform: str, chat_id: Optional[str]) -> bool:
        if not chat_id:
            return False
        with self._lock:
            return _normalize(platform, chat_id) in self._dead

    def mark_dead(self, platform: str, chat_id: Optional[str],
                  reason: str = "") -> bool:
        """Record a target as confirmed-dead.  Returns True if newly added."""
        if not chat_id:
            return False
        key = _normalize(platform, chat_id)
        with self._lock:
            existed = key in self._dead
            self._dead[key] = {
                "platform": str(platform).strip().lower(),
                "chat_id": str(chat_id),
                "reason": str(reason)[:200],
                "marked_at": time.time(),
            }
            self._flush_locked()
        if not existed:
            logger.info(
                "dead_targets: marked %s as unreachable (%s) — future deliveries "
                "to this target will be skipped until a send succeeds",
                key, reason or "no reason given",
            )
        return not existed

    def clear(self, platform: str, chat_id: Optional[str]) -> bool:
        """Remove a target's dead flag (self-healing).  Returns True if it was set."""
        if not chat_id:
            return False
        key = _normalize(platform, chat_id)
        with self._lock:
            if key in self._dead:
                del self._dead[key]
                self._flush_locked()
                logger.info("dead_targets: cleared %s (delivery succeeded again)", key)
                return True
        return False

    def all_dead(self) -> Dict[str, Dict[str, object]]:
        """Snapshot of the current dead set (for diagnostics / `hermes` CLI)."""
        with self._lock:
            return {k: dict(v) for k, v in self._dead.items()}
