"""
Erinnerungen & Routinen (Lucy-Proaktivität, Faden A3).

„Lucy, erinner mich morgen um 9 an …" — der Hermes-Agent legt per Tool (mcp_mc.py:
reminder_create) einen Eintrag an; der Wecker-Loop hier feuert ihn zur Zeit in den
Melde-Briefkasten (announce.py → Lucy spricht) und parallel auf Telegram.

Bewusst OHNE Hermes-cron: der Wecker läuft deterministisch im MC2-Backend (systemd,
überlebt Gateway-Ausfälle) und bleibt in UNSEREN Schichten (Hermes-Quellcode/Config
bleibt Auto-Update-Kanal, Verdikt Faden 11c).

Zeiten: ISO-8601. Ohne Offset gilt die Zeitzone des Commanders (MC_LOCAL_TZ,
Default Europe/Berlin) — die Box selbst läuft auf UTC, naive Zeiten dürfen daher
NIE als Box-Lokalzeit interpretiert werden. Wiederholung: daily | weekdays | weekly.
"""

import json
import logging
import os
import threading
import time
from datetime import datetime, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo

from config import MODELS_DIR
from services import announce

log = logging.getLogger(__name__)

STORE_PATH = Path(os.environ.get("MC_REMINDERS_STORE", str(MODELS_DIR / "mc2-reminders.json")))
LOCAL_TZ = ZoneInfo(os.environ.get("MC_LOCAL_TZ", "Europe/Berlin"))
INTERVAL = int(os.environ.get("MC_REMINDERS_INTERVAL", "20"))   # Wecker-Tick (Sekunden)
MAX_ITEMS = int(os.environ.get("MC_REMINDERS_MAX", "100"))
REPEATS = ("", "daily", "weekdays", "weekly")
LATE_NOTE_S = 600   # feuert >10 min zu spät (Box war aus) → ehrlich dazusagen

_lock = threading.Lock()
_state: dict | None = None   # {"next_id": int, "items": [...]}


def _load() -> dict:
    global _state
    if _state is None:
        try:
            _state = json.loads(STORE_PATH.read_text(encoding="utf-8"))
            assert isinstance(_state.get("next_id"), int) and isinstance(_state.get("items"), list)
        except Exception:
            _state = {"next_id": 1, "items": []}
    return _state


def _save(state: dict) -> None:
    try:
        tmp = STORE_PATH.with_suffix(".tmp")
        tmp.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
        tmp.replace(STORE_PATH)
    except OSError:
        log.warning("reminders: Store %s nicht schreibbar", STORE_PATH, exc_info=True)


def _parse_when(when: str) -> datetime:
    """ISO-8601 → bewusste Zeit. Naive Angaben = Commander-Zeitzone (NICHT Box-UTC)."""
    try:
        dt = datetime.fromisoformat(when.strip())
    except ValueError:
        raise ValueError("Zeit nicht lesbar — bitte ISO-8601 mit Datum UND Uhrzeit, z.B. 2026-07-04T09:00.")
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=LOCAL_TZ)
    return dt


def _fmt(ts: float) -> str:
    """Menschlich lesbare Commander-Lokalzeit (fürs Tool-Echo/Listing)."""
    return datetime.fromtimestamp(ts, LOCAL_TZ).strftime("%a %d.%m.%Y %H:%M")


def _advance(ts: float, repeat: str) -> float:
    """Nächste Wiederholung NACH jetzt (holt verpasste Termine ohne Mehrfach-Feuern auf)."""
    dt = datetime.fromtimestamp(ts, LOCAL_TZ)
    now = time.time()
    while dt.timestamp() <= now:
        dt += timedelta(days=7 if repeat == "weekly" else 1)
        if repeat == "weekdays":
            while dt.weekday() > 4:   # Sa/So überspringen
                dt += timedelta(days=1)
    return dt.timestamp()


def create(text: str, when: str, repeat: str = "") -> dict:
    text = (text or "").strip()
    if not text:
        raise ValueError("Leerer Erinnerungs-Text.")
    repeat = (repeat or "").strip().lower()
    if repeat == "once":
        repeat = ""
    if repeat not in REPEATS:
        raise ValueError("repeat muss leer, daily, weekdays oder weekly sein.")
    ts = _parse_when(when).timestamp()
    if repeat:
        if ts <= time.time():   # Startpunkt vorbei → auf die nächste Wiederholung rücken (Uhrzeit bleibt exakt)
            ts = _advance(ts, repeat)
    elif ts <= time.time() - 60:
        raise ValueError(f"Die Zeit liegt in der Vergangenheit ({_fmt(ts)}) — bitte neu umrechnen "
                         f"(jetzt ist {_fmt(time.time())}).")
    with _lock:
        state = _load()
        if len(state["items"]) >= MAX_ITEMS:
            raise ValueError(f"Zu viele offene Erinnerungen (max {MAX_ITEMS}).")
        item = {"id": state["next_id"], "text": text[:500], "next_ts": ts,
                "repeat": repeat, "created_ts": time.time()}
        state["next_id"] += 1
        state["items"].append(item)
        _save(state)
    log.info("reminder #%s angelegt: %s → %s%s", item["id"], _fmt(ts), text[:60],
             f" (Routine {repeat})" if repeat else "")
    return {**item, "when_local": _fmt(ts)}


def list_all() -> list[dict]:
    with _lock:
        items = sorted(_load()["items"], key=lambda i: i["next_ts"])
        return [{**i, "when_local": _fmt(i["next_ts"])} for i in items]


def delete(reminder_id: int) -> dict:
    with _lock:
        state = _load()
        for i, item in enumerate(state["items"]):
            if item["id"] == reminder_id:
                state["items"].pop(i)
                _save(state)
                return {**item, "when_local": _fmt(item["next_ts"])}
    raise KeyError(f"Erinnerung {reminder_id} nicht gefunden.")


def _fire_due() -> None:
    now = time.time()
    with _lock:
        state = _load()
        due = [i for i in state["items"] if i["next_ts"] <= now]
        if not due:
            return
        for item in due:
            if item["repeat"]:
                item["next_ts"] = _advance(item["next_ts"], item["repeat"])
            else:
                state["items"].remove(item)
        _save(state)
    for item in due:
        late = now - item["next_ts"] > LATE_NOTE_S if not item["repeat"] else False
        text = f"Erinnerung, Commander: {item['text']}"
        if late:
            text += f" (Eigentlich fällig um {_fmt(item['next_ts'])} — die Box war wohl aus.)"
        announce.add(text, subject="[Erinnerung]", source="reminder")
        announce.notify_telegram("[Erinnerung]", item["text"])
        log.info("reminder #%s gefeuert%s", item["id"], " (Routine)" if item["repeat"] else "")


async def reminders_loop() -> None:
    """Wecker (Hintergrund-Task im MC2-Lifespan). Verpasste einmalige Erinnerungen
    (Box war aus) feuern beim nächsten Tick nach — ehrlich als verspätet markiert."""
    import asyncio
    log.info("reminders: Wecker aktiv (Tick %ss, Zeitzone %s)", INTERVAL, LOCAL_TZ)
    while True:
        try:
            await asyncio.to_thread(_fire_due)
        except Exception:
            log.debug("reminders: Tick fehlgeschlagen", exc_info=True)
        await asyncio.sleep(INTERVAL)
