"""
core/news.py — News-Fetcher + Übersetzer
==========================================
Lädt RSS-Feeds zu WTI-Crude/Öl-News und übersetzt sie optional auf Deutsch.

  • NewsTranslator  — deep-translator (Google + MyMemory-Fallback)
  • NewsFetcher     — 4 RSS-Feeds, Sortierung nach Datum

Cache der Übersetzungen liegt in oil_widget_translations.json
(neben dem Hauptscript) und überlebt Neustarts.
"""

from __future__ import annotations
import json
import hashlib
import threading
import time
from pathlib import Path

from core.logger import get_logger

log_trans = get_logger("trans")
log_news  = get_logger("news")


# Cache-Pfad neben dem Hauptscript
TRANSLATION_CACHE_FILE = Path(__file__).parent.parent / "oil_widget_translations.json"
TRANSLATION_CACHE_MAX  = 1000          # FIFO-Trim ab 1200
TRANSLATION_TIMEOUT_S  = 8


# ══════════════════════════════════════════════
#  NEWS-ÜBERSETZUNG
# ══════════════════════════════════════════════
class NewsTranslator:
    """
    Übersetzt englische Nachrichten-Titel via deep-translator (Google).

      • Persistenter JSON-Cache (kein doppelter API-Call für gleiche Headlines)
      • Background-Threading: UI bleibt responsiv
      • Fallback-Provider (Google → MyMemory) falls primary fehlschlägt
      • Komplett lokal abschaltbar via [translation] enabled = false
    """

    def __init__(self, enabled: bool = True,
                 target: str = "de",
                 provider: str = "google",
                 cache_file: Path = TRANSLATION_CACHE_FILE):
        self.enabled    = enabled
        self.target     = target.strip().lower() or "de"
        self.provider   = provider.strip().lower() or "google"
        self.cache_file = cache_file
        self.cache      = {}
        self._cache_dirty = False
        self._lock      = threading.Lock()
        self.error      = None
        self.api_ok     = None
        self._load_cache()

    def _load_cache(self):
        if not self.cache_file.exists():
            return
        try:
            with open(self.cache_file, "r", encoding="utf-8") as f:
                self.cache = json.load(f)
            log_trans.info(f"Cache geladen: {len(self.cache)} Einträge")
        except Exception as e:
            log_trans.error(f"Cache-Lesefehler: {e}")
            self.cache = {}

    def _save_cache(self):
        if not self._cache_dirty:
            return
        try:
            if len(self.cache) > TRANSLATION_CACHE_MAX + 200:
                items = list(self.cache.items())
                self.cache = dict(items[-TRANSLATION_CACHE_MAX:])
            with open(self.cache_file, "w", encoding="utf-8") as f:
                json.dump(self.cache, f, ensure_ascii=False, indent=1)
            self._cache_dirty = False
        except Exception as e:
            log_trans.error(f"Cache-Schreibfehler: {e}")

    @staticmethod
    def _hash(text: str) -> str:
        return hashlib.sha1(text.encode("utf-8", errors="ignore")).hexdigest()[:16]

    def _translate_one(self, text: str) -> str | None:
        try:
            from deep_translator import GoogleTranslator
        except ImportError:
            self.error = "pip install deep-translator"
            return None

        try:
            if self.provider == "google":
                return GoogleTranslator(source="auto", target=self.target).translate(text)
        except Exception as e:
            log_trans.warning(f"Google-Fehler: {e}")

        try:
            from deep_translator import MyMemoryTranslator
            src_full = "en-GB"
            tgt_full = f"{self.target}-{self.target.upper()}"
            return MyMemoryTranslator(source=src_full, target=tgt_full).translate(text)
        except Exception as e:
            log_trans.warning(f"MyMemory-Fehler: {e}")

        return None

    def translate(self, text: str) -> str:
        if not self.enabled or not text:
            return text
        h = self._hash(text)
        with self._lock:
            cached = self.cache.get(h)
        if cached:
            return cached
        result = self._translate_one(text)
        if result and result.strip() and result.lower() != text.lower():
            with self._lock:
                self.cache[h] = result
                self._cache_dirty = True
                self.api_ok = True
            return result
        if result is None:
            self.api_ok = False
        return text

    def translate_batch(self, headlines: list) -> list:
        if not self.enabled:
            return headlines
        out = []
        for h in headlines:
            new = dict(h)
            orig = h.get("title", "")
            translated = self.translate(orig)
            if translated != orig:
                new["title_original"] = orig
                new["title"]          = translated
            out.append(new)
        if self._cache_dirty:
            self._save_cache()
        return out

    def shutdown(self):
        self._save_cache()


# ══════════════════════════════════════════════
#  NEWS-FETCHER
# ══════════════════════════════════════════════
class NewsFetcher:
    """
    Holt aktuelle WTI-Crude-News sowie geopolitische / makroökonomische News
    aus öffentlichen RSS-Feeds.

    Energie-Feeds (WTI / Rohstoff-spezifisch):
      OilPrice, EIA Press, Rigzone, ShaleMag, Offshore Energy,
      Oil&Gas 360, Guardian Oil

    Makro- / Geopolitik-Feeds (marktrelevant für WTI):
      Al Jazeera, BBC Business, BBC World, MarketWatch, The Hill Energy,
      DW World, FT Commodities, Middle East Eye, Hellenic Shipping News
    """

    # ── Energie / Rohstoffe — WTI-fokussiert ─────────────────────────────
    FEEDS_ENERGY = [
        ("OilPrice",        "https://oilprice.com/rss/main"),
        ("EIA Press",       "https://www.eia.gov/rss/press_rss.xml"),
        ("Rigzone",         "https://www.rigzone.com/news/rss/rigzone_latest.aspx"),
        ("ShaleMag",        "https://shalemag.com/feed/"),
        ("Offshore Energy", "https://www.offshore-energy.biz/feed/"),
        ("Oil&Gas 360",     "https://www.oilandgas360.com/feed/"),
        ("Guardian Oil",    "https://www.theguardian.com/business/oil/rss"),
    ]

    # ── Makro / Geopolitik (marktrelevant für WTI) ───────────────────────
    # Reuters-Feed wurde 2020 abgeschaltet — nicht mehr verwenden.
    FEEDS_GEO = [
        ("Al Jazeera",    "https://www.aljazeera.com/xml/rss/all.xml"),
        ("BBC Business",  "https://feeds.bbci.co.uk/news/business/rss.xml"),
        ("BBC World",     "https://feeds.bbci.co.uk/news/world/rss.xml"),
        ("MarketWatch",   "https://feeds.marketwatch.com/marketwatch/marketpulse/"),
        ("The Hill",      "https://thehill.com/policy/energy-environment/feed/"),
        ("DW World",      "https://rss.dw.com/rdf/rss-en-world"),
        ("FT Commodities","https://www.ft.com/commodities?format=rss"),
        ("Middle East Eye","https://www.middleeasteye.net/rss"),
        ("Hellenic Ship.", "https://www.hellenicshippingnews.com/feed/"),
    ]

    # Kombination beider Feed-Gruppen
    FEEDS = FEEDS_ENERGY + FEEDS_GEO

    # Schlüsselwörter zur Relevanz-Filterung (Geo-Feeds)
    GEO_KEYWORDS = {
        # Öl/Energie allgemein
        "oil", "crude", "wti", "brent", "petroleum", "energy",
        "opec", "iea", "eia", "barrel", "supply", "demand",
        "refin", "pipeline", "tanker", "stockpile", "inventory", "stocks",
        "spr", "gas", "fuel", "commodity", "commodities", "lng",
        # Schifffahrt / Tankerrouten — direkt preisrelevant
        "shipping", "vessel", "suez", "canal", "chokepoint",
        "bab-el-mandeb", "vlcc", "freight", "cargo",
        # WTI-spezifisch: US-Produktion & Infrastruktur
        "cushing", "permian", "bakken", "eagle ford", "marcellus",
        "keystone", "gulf of mexico", "nymex", "shale", "fracking",
        "us oil", "us energy", "us crude", "us production",
        "refinery capacity", "hurricane",
        # Sanktionen / Geopolitik
        "iran", "sanction", "embargo", "strait", "hormuz", "gulf",
        # Akteure
        "saudi", "russia", "moscow", "putin", "iraq",
        "venezuela", "libya", "nigeria", "kuwait", "uae", "qatar",
        "ukraine", "hamas", "houthi", "yemen", "israel", "hezbollah",
        "lebanon", "syria", "china", "beijing", "iran",
        # Ereignisse
        "war", "attack", "ceasefire", "truce", "drone", "missile",
        "embargo", "blockade", "strike", "shutdown", "outage",
        "disruption", "force majeure", "summit", "deal", "agreement",
        "tariff", "trade war", "recession", "inflation", "gdp",
    }

    # ── Vollständige Feed-Pools inkl. Alternativen (Reihenfolge = Priorität) ──
    # validate_feeds() testet alle Feeds parallel und ersetzt ausgefallene
    # primäre Feeds automatisch mit dem nächsten funktionierenden Pool-Eintrag.
    _ENERGY_POOL = [
        # Primär
        ("OilPrice",         "https://oilprice.com/rss/main"),
        ("EIA Press",        "https://www.eia.gov/rss/press_rss.xml"),
        ("Rigzone",          "https://www.rigzone.com/news/rss/rigzone_latest.aspx"),
        ("ShaleMag",         "https://shalemag.com/feed/"),
        ("Offshore Energy",  "https://www.offshore-energy.biz/feed/"),
        ("Oil&Gas 360",      "https://www.oilandgas360.com/feed/"),
        ("Guardian Oil",     "https://www.theguardian.com/business/oil/rss"),
        # Alternativen (automatischer Fallback)
        ("Energy Monitor",   "https://www.energymonitor.ai/feed/"),
        ("Natural Gas Int.", "https://www.naturalgasintel.com/feed/"),
    ]

    _GEO_POOL = [
        # Primär
        ("Al Jazeera",       "https://www.aljazeera.com/xml/rss/all.xml"),
        ("BBC Business",     "https://feeds.bbci.co.uk/news/business/rss.xml"),
        ("BBC World",        "https://feeds.bbci.co.uk/news/world/rss.xml"),
        ("MarketWatch",      "https://feeds.marketwatch.com/marketwatch/marketpulse/"),
        ("The Hill",         "https://thehill.com/policy/energy-environment/feed/"),
        ("DW World",         "https://rss.dw.com/rdf/rss-en-world"),
        ("FT Commodities",   "https://www.ft.com/commodities?format=rss"),
        ("Middle East Eye",  "https://www.middleeasteye.net/rss"),
        ("Hellenic Ship.",   "https://www.hellenicshippingnews.com/feed/"),
        # Alternativen (automatischer Fallback)
        ("New Arab",         "https://www.newarab.com/rss.xml"),
    ]

    # WTI-Direktseite (HTML-Scraper, keine RSS)
    OILPRICE_WTI_URL    = "https://oilprice.com/futures/wti/"
    # Globale Öl-Preistabelle (Breadth + WTI/Brent Spot)
    OILPRICE_CHARTS_URL = "https://oilprice.com/oil-price-charts/"

    # HTTP-Timeout pro Feed in Sekunden — verhindert dass langsame/tote
    # Feeds den ganzen Fetch-Thread blockieren.
    FETCH_TIMEOUT_S = 8

    def __init__(self, translator: 'NewsTranslator | None' = None):
        self.headlines    = []
        self.last_fetch   = None
        self.error        = None
        self.translator   = translator
        self.sentiment    = {"score": 0.0, "n_bull": 0, "n_bear": 0, "samples": []}
        self.price_data:  dict | None = None   # OilPrice Charts Breadth + Spot
        # Pro-Feed Status: {source: {"ok": bool, "n": int, "msg": str}}
        self.feed_status: dict = {}
        # Validierte, aktive Feed-Listen (None = noch nicht validiert → Klassenvariable)
        self._active_energy: list | None = None
        self._active_geo:    list | None = None
        self._lock        = threading.Lock()

    def _is_geo_relevant(self, title: str) -> bool:
        """Gibt True zurück, wenn ein Geo-Feed-Titel marktrelevante Keywords enthält."""
        low = title.lower()
        return any(kw in low for kw in self.GEO_KEYWORDS)

    # ══════════════════════════════════════════════
    #  FEED-VALIDIERUNG (einmalig beim Start)
    # ══════════════════════════════════════════════

    def _test_url(self, source: str, url: str) -> tuple[bool, int, str]:
        """Schneller Feed-Test ohne feed_status zu verändern. Gibt (ok, n_entries, msg) zurück."""
        try:
            import feedparser, requests
        except ImportError:
            return False, 0, "missing: feedparser/requests"
        try:
            resp = requests.get(
                url,
                timeout=(2, 6),  # kurze Timeouts für schnelle Validierung
                headers={
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                                  "AppleWebKit/537.36 OilWidget/1.0",
                    "Accept": "application/rss+xml, application/xml, text/xml, */*",
                },
            )
            if resp.status_code != 200:
                return False, 0, f"HTTP {resp.status_code}"
            feed = feedparser.parse(resp.content)
            n = len(feed.entries)
            if n == 0:
                return False, 0, "0 Einträge"
            return True, n, "ok"
        except Exception as e:
            return False, 0, str(e)[:60]

    def validate_feeds(self) -> dict:
        """
        Testet alle Feeds aus _ENERGY_POOL / _GEO_POOL parallel.
        Ausgefallene Primär-Feeds werden automatisch durch den nächsten
        funktionierenden Pool-Eintrag ersetzt.
        Ergebnis wird in self._active_energy / self._active_geo gespeichert.

        Aufruf: einmalig beim Start in einem Daemon-Thread.
        """
        import concurrent.futures

        all_feeds = list({u: (s, u) for s, u in
                          self._ENERGY_POOL + self._GEO_POOL}.values())

        log_news.info(f"Feed-Validierung: teste {len(all_feeds)} Feeds …")

        # Parallel testen
        test_results: dict[str, tuple[bool, int, str]] = {}  # url → (ok, n, msg)
        with concurrent.futures.ThreadPoolExecutor(max_workers=12) as ex:
            fut_map = {ex.submit(self._test_url, s, u): (s, u) for s, u in all_feeds}
            try:
                for fut in concurrent.futures.as_completed(fut_map, timeout=25):
                    src, url = fut_map[fut]
                    try:
                        ok, n, msg = fut.result()
                    except Exception as e:
                        ok, n, msg = False, 0, str(e)[:50]
                    test_results[url] = (ok, n, msg)
                    status = "OK" if ok else "FAIL"
                    log_news.debug(f"  [{status}] {src}: {msg} ({n} Einträge)")
            except concurrent.futures.TimeoutError:
                log_news.warning("Feed-Validierung: Timeout nach 25 s")

        def build_active(primary: list, pool: list) -> tuple[list, list]:
            """
            Gibt (aktive_feeds, ersetzungen) zurück.
            Primäre Feeds haben Vorrang; ausgefallene werden mit dem
            nächsten noch nicht verwendeten Pool-Feed ersetzt.
            """
            primary_urls = {u for _, u in primary}
            alternatives = [(s, u) for s, u in pool if u not in primary_urls]

            active = []
            used_urls: set[str] = set()
            replacements = []

            for src, url in primary:
                ok, n, msg = test_results.get(url, (False, 0, "nicht getestet"))
                if ok and n > 0:
                    active.append((src, url))
                    used_urls.add(url)
                else:
                    log_news.warning(f"Feed ausgefallen: {src}  [{msg}]  → suche Ersatz")
                    replaced = False
                    for alt_src, alt_url in alternatives:
                        if alt_url in used_urls:
                            continue
                        alt_ok, alt_n, alt_msg = test_results.get(alt_url, (False, 0, "nicht getestet"))
                        if alt_ok and alt_n > 0:
                            active.append((alt_src, alt_url))
                            used_urls.add(alt_url)
                            replacements.append((src, alt_src))
                            log_news.info(f"  → Ersetzt: {src} → {alt_src}")
                            replaced = True
                            break
                    if not replaced:
                        log_news.warning(f"  → Kein Ersatz verfügbar für {src}")

            return active, replacements

        active_e, rep_e = build_active(self.FEEDS_ENERGY, self._ENERGY_POOL)
        active_g, rep_g = build_active(self.FEEDS_GEO,    self._GEO_POOL)

        with self._lock:
            self._active_energy = active_e
            self._active_geo    = active_g

        total_ok   = sum(1 for ok, _, _ in test_results.values() if ok)
        total_fail = len(test_results) - total_ok
        log_news.info(
            f"Feed-Validierung abgeschlossen: {total_ok} OK / {total_fail} ausgefallen  "
            f"| Energie: {len(active_e)} aktiv ({len(rep_e)} ersetzt)  "
            f"| Geo: {len(active_g)} aktiv ({len(rep_g)} ersetzt)"
        )
        return {
            "energy_active":  active_e,
            "geo_active":     active_g,
            "replacements":   rep_e + rep_g,
            "n_ok":           total_ok,
            "n_fail":         total_fail,
        }

    def _fetch_oilprice_wti_scrape(self) -> list:
        """
        Scrapet https://oilprice.com/futures/wti/ nach WTI-spezifischen Headlines.
        Nutzt BeautifulSoup wenn verfügbar, sonst Regex-Fallback.
        Gibt bis zu 8 Einträge im selben Format wie RSS-Feeds zurück.
        """
        try:
            import requests
        except ImportError:
            return []

        try:
            resp = requests.get(
                self.OILPRICE_WTI_URL,
                timeout=(3, self.FETCH_TIMEOUT_S),
                headers={
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                                  "AppleWebKit/537.36 Chrome/120.0 OilWidget/1.0",
                    "Accept":     "text/html,application/xhtml+xml,*/*",
                    "Accept-Language": "en-US,en;q=0.9",
                },
            )
        except Exception as e:
            log_news.warning(f"OilPrice WTI scrape: {e}")
            self.feed_status["OilPrice WTI"] = {"ok": False, "n": 0, "msg": str(e)[:60]}
            return []

        if resp.status_code != 200:
            log_news.warning(f"OilPrice WTI: HTTP {resp.status_code}")
            self.feed_status["OilPrice WTI"] = {
                "ok": False, "n": 0, "msg": f"HTTP {resp.status_code}"}
            return []

        items = []
        now   = time.time()
        seen  = set()

        # Artikel-Pfade auf OilPrice.com: /Energy/... und /Latest-Energy-News/...
        _ARTICLE_PATHS = ("/Energy/", "/Latest-Energy-News/")

        try:
            from bs4 import BeautifulSoup
            soup = BeautifulSoup(resp.content, "html.parser")

            for tag in soup.find_all("a", href=True):
                href  = tag.get("href", "")
                title = tag.get_text(strip=True)
                # Nur Artikel-Pfade, keine Navigation/Werbung
                if not any(p in href for p in _ARTICLE_PATHS):
                    continue
                if len(title) < 25 or title in seen:
                    continue
                if not href.startswith("http"):
                    href = ("https://oilprice.com" + href
                            if href.startswith("/") else "")
                if not href:
                    continue
                seen.add(title)
                items.append({
                    "title":    title,
                    "link":     href,
                    "source":   "OilPrice WTI",
                    "ts":       now,
                    "category": "energy",
                })
                if len(items) >= 8:
                    break

        except ImportError:
            # Regex-Fallback: kein beautifulsoup4 installiert
            import re
            pat = re.compile(
                r'<a[^>]+href="(https?://oilprice\.com/'
                r'(?:Energy|Latest-Energy-News)/[^"#?]{10,})"[^>]*>'
                r'\s*([^<\n]{25,200})\s*</a>',
                re.IGNORECASE | re.DOTALL,
            )
            for href, raw in pat.findall(resp.text):
                title = re.sub(r'\s+', ' ', raw).strip()
                if not title or title in seen:
                    continue
                seen.add(title)
                items.append({
                    "title":    title,
                    "link":     href,
                    "source":   "OilPrice WTI",
                    "ts":       now,
                    "category": "energy",
                })
                if len(items) >= 8:
                    break
            if not items:
                log_news.info("OilPrice WTI: BeautifulSoup nicht installiert "
                              "(pip install beautifulsoup4 lxml) — Regex-Fallback aktiv")

        self.feed_status["OilPrice WTI"] = {
            "ok": len(items) > 0,
            "n":  len(items),
            "msg": "scrape ok" if items else "0 Headlines",
        }
        log_news.info(f"OilPrice WTI scrape: {len(items)} Headlines")
        return items

    def _fetch_oilprice_charts(self) -> dict | None:
        """
        Scrapet https://oilprice.com/oil-price-charts/ und extrahiert:
          • WTI Crude und Brent Crude: Preis, absolute Änderung, %Änderung
          • Market Breadth aus Tabelle 'Futures & Indexes':
            Anteil der Öl-Benchmarks mit positivem Tageschange
          • breadth_signal: -1.0 (alle runter) … +1.0 (alle rauf)

        Breadth > +0.5  → bullisher Marktkontext
        Breadth < -0.5  → bärischer Marktkontext
        """
        try:
            import requests
        except ImportError:
            return None

        try:
            resp = requests.get(
                self.OILPRICE_CHARTS_URL,
                timeout=(3, self.FETCH_TIMEOUT_S),
                headers={
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                                  "AppleWebKit/537.36 Chrome/120.0 OilWidget/1.0",
                    "Accept":     "text/html,application/xhtml+xml,*/*",
                    "Accept-Language": "en-US,en;q=0.9",
                },
            )
        except Exception as e:
            log_news.warning(f"OilPrice Charts fetch: {e}")
            return None

        if resp.status_code != 200:
            log_news.warning(f"OilPrice Charts: HTTP {resp.status_code}")
            return None

        try:
            from bs4 import BeautifulSoup
        except ImportError:
            log_news.warning("OilPrice Charts: BeautifulSoup fehlt (pip install beautifulsoup4)")
            return None

        try:
            soup   = BeautifulSoup(resp.content, "html.parser")
            tables = soup.find_all("table", class_="oilprices__table")
            if not tables:
                log_news.warning("OilPrice Charts: Tabelle nicht gefunden")
                return None

            t0     = tables[0]   # "Futures & Indexes"
            n_up   = n_down = 0
            wti    = brent  = None

            for row in t0.find_all("tr"):
                cells = row.find_all(["th", "td"])
                if len(cells) < 4:
                    continue
                name_cell   = cells[1]
                price_cell  = cells[2]
                change_cell = cells[3]
                pct_cell    = cells[4] if len(cells) > 4 else None

                name = name_cell.get_text(strip=True)
                try:
                    price = float(price_cell.get_text(strip=True).replace(",", ""))
                except ValueError:
                    continue

                change_text    = change_cell.get_text(strip=True)
                change_classes = change_cell.get("class", [])
                try:
                    change = float(change_text.replace(",", ""))
                except ValueError:
                    change = 0.0

                is_up   = any("up"   in c for c in change_classes)
                is_down = any("down" in c for c in change_classes)
                if is_up:
                    n_up   += 1
                elif is_down:
                    n_down += 1

                if name in ("WTI Crude", "Brent Crude"):
                    pct = 0.0
                    if pct_cell:
                        raw_pct = pct_cell.get_text(strip=True).split("(")[0]
                        try:
                            pct = float(raw_pct.replace("%", "").replace(",", ""))
                        except ValueError:
                            pass
                    entry = {"price": price, "change": change, "pct": pct}
                    if name == "WTI Crude":
                        wti   = entry
                    else:
                        brent = entry

            n_total = n_up + n_down
            breadth_pct    = round(n_up / n_total * 100, 1) if n_total else 50.0
            breadth_signal = round((n_up - n_down) / n_total, 3) if n_total else 0.0

            result = {
                "wti":             wti,
                "brent":           brent,
                "n_up":            n_up,
                "n_down":          n_down,
                "n_total":         n_total,
                "breadth_pct":     breadth_pct,     # % der Futures die gestiegen sind
                "breadth_signal":  breadth_signal,  # -1..+1
            }

            wti_str   = f"WTI={wti['price']:.2f} ({wti['pct']:+.2f}%)"  if wti   else "WTI=?"
            brent_str = f"Brent={brent['price']:.2f} ({brent['pct']:+.2f}%)" if brent else "Brent=?"
            log_news.info(
                f"OilPrice Charts: {wti_str}  {brent_str}  "
                f"Breadth {breadth_pct:.0f}% ({n_up}up/{n_down}dn) signal={breadth_signal:+.2f}"
            )
            return result

        except Exception as e:
            log_news.warning(f"OilPrice Charts parse: {e}", exc_info=True)
            return None

    def price_data_snapshot(self) -> dict | None:
        """Thread-safe Kopie der letzten Preisdaten (OilPrice Charts)."""
        with self._lock:
            return dict(self.price_data) if self.price_data else None

    def _load_feed(self, source: str, url: str):
        """
        Lädt eine einzelne RSS-URL mit hardem HTTP-Timeout.
        Liefert die feedparser-Feed oder None bei Fehler.
        Setzt feed_status[source] für spätere Diagnose.
        """
        try:
            import feedparser
            import requests
        except ImportError as e:
            self.feed_status[source] = {"ok": False, "n": 0, "msg": f"missing: {e}"}
            return None

        try:
            resp = requests.get(
                url,
                timeout=(3, self.FETCH_TIMEOUT_S),
                headers={
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                                  "AppleWebKit/537.36 OilWidget/1.0",
                    "Accept": "application/rss+xml, application/xml, text/xml, */*",
                },
            )
            if resp.status_code != 200:
                self.feed_status[source] = {
                    "ok": False, "n": 0,
                    "msg": f"HTTP {resp.status_code}",
                }
                log_news.warning(f"{source}: HTTP {resp.status_code}")
                return None
            feed = feedparser.parse(resp.content)
            if feed.bozo and not feed.entries:
                # bozo=1 mit Entries ist meist nur "namespace warning" → OK
                msg = str(feed.bozo_exception)[:80] if hasattr(feed, "bozo_exception") else "parse error"
                self.feed_status[source] = {"ok": False, "n": 0, "msg": msg}
                log_news.warning(f"{source}: {msg}")
                return None
            n = len(feed.entries)
            self.feed_status[source] = {"ok": True, "n": n, "msg": "ok"}
            return feed
        except Exception as e:
            self.feed_status[source] = {"ok": False, "n": 0, "msg": str(e)[:80]}
            log_news.warning(f"{source}: {e}")
            return None

    def fetch(self):
        try:
            import feedparser  # nur für Verfügbarkeits-Check
        except ImportError:
            with self._lock:
                self.error = "pip install feedparser requests"
            return

        # Per-Feed Status für diesen Run leeren
        self.feed_status = {}
        energy_items = []
        geo_items    = []

        # ── OilPrice WTI-Direktseite (Scraper, höchste Priorität) ──
        wti_items  = self._fetch_oilprice_wti_scrape()
        energy_items.extend(wti_items)

        # ── OilPrice Charts — Preis-Breadth ──
        charts_data = self._fetch_oilprice_charts()

        # Validierte Listen verwenden (nach validate_feeds()), sonst Klassen-Defaults
        with self._lock:
            energy_feeds = self._active_energy if self._active_energy is not None else self.FEEDS_ENERGY
            geo_feeds    = self._active_geo    if self._active_geo    is not None else self.FEEDS_GEO

        # ── Energie-Feeds: bis zu 5 Einträge je Feed, keine Filterung ──
        for source, url in energy_feeds:
            feed = self._load_feed(source, url)
            if feed is None:
                continue
            for entry in feed.entries[:5]:
                pub = entry.get("published_parsed") or time.gmtime()
                energy_items.append({
                    "title":    entry.get("title", "(no title)").strip(),
                    "link":     entry.get("link", ""),
                    "source":   source,
                    "ts":       time.mktime(pub),
                    "category": "energy",
                })

        # ── Geo-Feeds: bis zu 3 Einträge je Feed, nur relevante Titel ──
        for source, url in geo_feeds:
            feed = self._load_feed(source, url)
            if feed is None:
                continue
            count = 0
            for entry in feed.entries:
                if count >= 3:
                    break
                title = entry.get("title", "").strip()
                if not self._is_geo_relevant(title):
                    continue
                pub = entry.get("published_parsed") or time.gmtime()
                geo_items.append({
                    "title":    title,
                    "link":     entry.get("link", ""),
                    "source":   source,
                    "ts":       time.mktime(pub),
                    "category": "geo",
                })
                count += 1

        # Diagnose-Zeilen ins Log
        ok_count = sum(1 for s in self.feed_status.values() if s["ok"])
        fail_count = len(self.feed_status) - ok_count
        log_news.info(
            f"Feed-Status: {ok_count} OK, {fail_count} fehlgeschlagen"
        )
        for source, st in self.feed_status.items():
            if st["ok"]:
                log_news.debug(f"  ✓ {source}: {st['n']} Einträge")
            else:
                log_news.info(f"  ✗ {source}: {st['msg']}")

        # ── Zusammenführen: WTI-Scraper + RSS Energie + Geo ──
        # WTI-Scraper-Items haben ts=now → sortieren immer an erste Stelle wenn aktuell
        energy_items.sort(key=lambda x: x["ts"], reverse=True)
        geo_items.sort(key=lambda x: x["ts"], reverse=True)

        # WTI-Scraper bekommt bis zu 4 Slots im Energy-Bucket (von 7 gesamt)
        wti_out    = [i for i in energy_items if i.get("source") == "OilPrice WTI"][:4]
        rss_energy = [i for i in energy_items if i.get("source") != "OilPrice WTI"][:3]
        items = wti_out + rss_energy + geo_items[:4]
        items.sort(key=lambda x: x["ts"], reverse=True)
        items = items[:10]

        log_news.info(
            f"→ WTI-Scraper:{len(wti_out)} + Energie-RSS:{len(rss_energy)} + "
            f"Geo:{len(geo_items[:4])} = {len(items)} Headlines"
        )

        if self.translator and self.translator.enabled:
            try:
                items = self.translator.translate_batch(items)
            except Exception as e:
                log_news.warning(f"Übersetzung fehlgeschlagen: {e}")

        # Sentiment auf den (ggf. übersetzten) Headlines berechnen.
        # calc_news_sentiment() greift bevorzugt auf title_original (englisch) zurück.
        try:
            from core.analysis import calc_news_sentiment
            sentiment = calc_news_sentiment(items)
        except Exception as e:
            log_news.warning(f"Sentiment-Berechnung fehlgeschlagen: {e}")
            sentiment = {"score": 0.0, "n_bull": 0, "n_bear": 0, "samples": []}

        # ── Breadth-Signal in Sentiment einblenden (Gewicht 25%) ──────────────
        # Breadth misst ob die Mehrheit aller Öl-Benchmarks steigt/fällt.
        # Nur bei starkem Signal (|breadth| > 0.4) und ohne starke News-Gegenmeinung.
        if charts_data:
            bs = charts_data.get("breadth_signal", 0.0)
            if abs(bs) > 0.4:
                raw_score = sentiment["score"]
                blended   = round(raw_score * 0.75 + bs * 0.25, 3)
                blended   = max(-1.0, min(1.0, blended))
                sentiment  = dict(sentiment)
                sentiment["score"]          = blended
                sentiment["breadth_signal"] = bs
                sentiment["breadth_pct"]    = charts_data.get("breadth_pct", 50.0)
                log_news.info(
                    f"Breadth {bs:+.2f} → Sentiment {raw_score:+.2f} → {blended:+.2f}"
                )

        with self._lock:
            self.headlines  = items
            self.last_fetch = time.time()
            self.error      = None if items else "Keine News-Feeds erreichbar"
            self.sentiment  = sentiment
            self.price_data = charts_data

        log_news.info(f"News-Sentiment: {sentiment['score']:+.2f} "
                      f"(bull={sentiment['n_bull']}, bear={sentiment['n_bear']})")

    def snapshot(self):
        with self._lock:
            return list(self.headlines), self.last_fetch, self.error

    def sentiment_snapshot(self) -> dict:
        """Thread-safe Kopie des letzten Sentiment-Snapshots."""
        with self._lock:
            return dict(self.sentiment)

    def storm_state(self) -> dict:
        """News-Sturm-Indikator (REINE ANZEIGE, kein Signal): Headline-Rate der
        letzten Stunde vs. Basisrate des vorhandenen Feed-Fensters. Viel frische
        Öl-/Geo-Schlagzeilen auf einmal = Ereignis läuft → Vorsicht (Vola).
        normal · elevated (≥3 frisch & ≥2× Basis) · storm (≥5 frisch & ≥3× Basis)."""
        with self._lock:
            items = [h for h in self.headlines if h.get("ts")]
        now = time.time()
        if not items:
            return {"level": "normal", "fresh": 0, "base_per_h": 0.0}
        fresh = sum(1 for h in items if now - h["ts"] <= 3600)
        # Basis-Fenster min. 6 h verankern — sonst bläht ein frischer Burst die
        # Basisrate selbst auf und der Sturm erkennt sich nicht (Selbst-Normierung).
        span_h = min(48.0, max(6.0, (now - min(h["ts"] for h in items)) / 3600))
        base = len(items) / span_h                     # Ø Headlines/Stunde im Fenster
        level = "normal"
        if fresh >= 5 and fresh >= 3 * base:
            level = "storm"
        elif fresh >= 3 and fresh >= 2 * base:
            level = "elevated"
        return {"level": level, "fresh": fresh, "base_per_h": round(base, 1)}
