"""core/econ_calendar.py — Wirtschaftskalender (High-Impact-News) als Handels-Sperre.

User-Vorgabe 2026-07-30 („berücksichtige auch die Quellen aus dem Trading-Handbuch",
Kapitel 5.4 + 8.1 Regel 8: „Keine Trades vor High-Impact-News", „keine Trades 15 Min.
vor EIA").

Quelle: **Forex Factory JSON** (`nfs.faireconomy.media`) — kostenlos, kein API-Key,
strukturiert mit `impact`-Level. Enthält auch „Crude Oil Inventories" (= der
EIA-Report) und OPEC-Termine.

⚠ ABGRENZUNG zum bestehenden EIA-Blackout: Der ist **gemessen** (`backtest_events.py`:
Mi 15:30–16:30 in BEIDEN Hälften negativ) und bleibt als fester Fallback bestehen —
er funktioniert auch, wenn dieses Modul keine Daten bekommt. Der Kalender-Blackout
hier ist eine **plausible Erweiterung** (FOMC/NFP/CPI/OPEC statt nur Mittwoch), aber
NICHT separat backtestbar: Event-Zeitreihen reichen nicht weit genug zurück, und die
Ereignisse sind zu selten für eine 2-Stichproben-Aussage. Er ist damit ein
**Risiko-Schutz auf Basis der Handbuch-Regel**, kein gemessener Edge — deshalb sperrt
er nur die AUTONOMEN Entries und warnt in der Checkliste; er blockt keine manuelle
Order.

Fail-safe: Ohne Netz/Daten liefert das Modul „kein Blackout" und der Bot arbeitet
unverändert weiter (der gemessene EIA-Blackout greift trotzdem).
"""
from __future__ import annotations
import json
import threading
import time
import urllib.request
from datetime import datetime, timezone

from core.logger import get_logger

log = get_logger("calendar")

# BEIDE Wochen holen: „thisweek" endet samstags — der Markt öffnet aber Sonntag
# 23:00 (Berlin), und ein Montag-Event läge dann in einem toten Fenster.
_URLS = ("https://nfs.faireconomy.media/ff_calendar_thisweek.json",
         "https://nfs.faireconomy.media/ff_calendar_nextweek.json")
_REFRESH_S = 3600.0        # 1×/Stunde reicht — der Kalender ändert sich selten
_TIMEOUT_S = 15.0
# Währungen, die WTI real bewegen (USD dominiert; OPEC/Oil-Titel unabhängig davon)
_CCY = {"USD"}
# Titel-Stichworte, die IMMER als relevant gelten (auch bei niedrigerem Impact —
# „Crude Oil Inventories" läuft bei Forex Factory teils als Low/Medium)
_OIL_KEYS = ("crude oil", "opec", "oil inventories", "petroleum")


class EconCalendar:
    def __init__(self, before_min: int = 15, after_min: int = 15,
                 enabled: bool = True):
        self.enabled = bool(enabled)
        self.before_s = max(0, int(before_min)) * 60
        self.after_s = max(0, int(after_min)) * 60
        self._lock = threading.Lock()
        self._events: list[dict] = []      # [{ts, title, ccy, impact}]
        self._ts = 0.0
        self._busy = False
        self._fail_logged = False

    # ── Daten holen ──────────────────────────────────────────────────────────
    def _fetch(self) -> None:
        try:
            raw: list = []
            errs = 0
            for url in _URLS:
                try:
                    req = urllib.request.Request(
                        url, headers={"User-Agent": "Mozilla/5.0"})
                    with urllib.request.urlopen(req, timeout=_TIMEOUT_S) as r:
                        part = json.loads(r.read().decode())
                    if isinstance(part, list):
                        raw.extend(part)
                except Exception:
                    errs += 1        # eine Woche darf fehlen (nextweek ist oft leer)
            if errs == len(_URLS):
                raise RuntimeError("keine Woche abrufbar")
            out = []
            seen = set()
            for e in raw:
                title = (e.get("title") or "").strip()
                ccy = (e.get("country") or "").strip()
                imp = (e.get("impact") or "").strip()
                tl = title.lower()
                is_oil = any(k in tl for k in _OIL_KEYS)
                if not (is_oil or (imp == "High" and ccy in _CCY)):
                    continue
                try:
                    dt = datetime.fromisoformat(e.get("date"))
                    if dt.tzinfo is None:
                        dt = dt.replace(tzinfo=timezone.utc)
                except Exception:
                    continue
                key = (round(dt.timestamp()), title)
                if key in seen:          # Überlappung thisweek/nextweek
                    continue
                seen.add(key)
                out.append({"ts": dt.timestamp(), "title": title,
                            "ccy": ccy, "impact": ("Oil" if is_oil else imp)})
            out.sort(key=lambda x: x["ts"])
            with self._lock:
                self._events = out
                self._ts = time.time()
            self._fail_logged = False
            log.info(f"Wirtschaftskalender geladen: {len(out)} relevante Events")
        except Exception as e:
            if not self._fail_logged:
                log.info(f"Kalender nicht abrufbar ({e}) — News-Sperre inaktiv "
                         f"(der gemessene EIA-Blackout greift weiterhin)")
                self._fail_logged = True

    def maybe_refresh(self) -> None:
        """Im Trend-Loop aufrufen; self-throttled auf ~1 h.

        ⚠ Der Fetch läuft in einem **Daemon-Thread** — sonst würde ein hängender
        HTTP-Call den Trend-Loop bis zum Timeout (15 s) blockieren.
        """
        if not self.enabled or self._busy:
            return
        if time.time() - self._ts < _REFRESH_S:
            return
        self._busy = True
        self._ts = time.time()          # sofort setzen → kein Retry-Sturm bei Fehler

        def _job():
            try:
                self._fetch()
            finally:
                self._busy = False

        threading.Thread(target=_job, daemon=True,
                         name="econ-calendar").start()

    # ── Abfragen ─────────────────────────────────────────────────────────────
    def blackout(self) -> dict | None:
        """Läuft gerade ein News-Fenster? → {title, impact, minutes} sonst None."""
        if not self.enabled:
            return None
        now = time.time()
        with self._lock:
            evs = list(self._events)
        for e in evs:
            if e["ts"] - self.before_s <= now <= e["ts"] + self.after_s:
                return {"title": e["title"], "impact": e["impact"], "ccy": e["ccy"],
                        "minutes": round((e["ts"] - now) / 60.0, 1)}
        return None

    def next_event(self) -> dict | None:
        """Nächstes relevantes Event (für die Anzeige)."""
        now = time.time()
        with self._lock:
            evs = list(self._events)
        for e in evs:
            if e["ts"] >= now:
                return {"title": e["title"], "impact": e["impact"], "ccy": e["ccy"],
                        "minutes": round((e["ts"] - now) / 60.0)}
        return None

    def snapshot(self) -> dict:
        bl = self.blackout()
        return {"enabled": self.enabled, "n": len(self._events),
                "blackout": bl, "next": self.next_event(),
                "age_min": round((time.time() - self._ts) / 60.0) if self._ts else None}
