"""core/hl_walls.py — ECHTE Liquiditäts-Wände von Hyperliquid für den MT5-Chart.

Holt vom HL-Dashboard (Port 8001, `GET /api/walls`) das größte L2-Orderbuch-Level je
Seite — **basis-korrigiert auf das Pepperstone-Preisniveau** — und stellt es dem
Level-Export (`engine._write_levels_file` → `sr_levels.csv` → `SR_Levels.mq5`) bereit.

WARUM (User-Wunsch 2026-07-30): Alle S/R-Linien im Projekt sind aus **Pivots
geschätzt** („wo war mal ein Docht"). Das hier ist die einzige Quelle mit
**tatsächlich ruhenden Orders** — Pepperstone ist Market Maker und liefert kein
Orderbuch, Hyperliquid schon (20 Levels je Seite).

⚠ Zwei Dinge im Blick behalten:
  1. **Basis-Differenz**: HL `xyz:CL` notiert ~0,45 $ unter dem CFD (anderer
     Kontrakt/Contango). Die Umrechnung passiert SERVERSEITIG im HL-Projekt
     (Median über die letzten 60 Snapshots) — hier kommt schon `bid_mt5`/`ask_mt5`
     an. Fehlt die Basis (zu wenige Snapshots), wird NICHT gezeichnet.
  2. **Anzeige, KEIN Signal.** Ob HL-Orderbuch-Signale auf den CFD übertragbar sind,
     ist eine offene Frage und wird gerade gemessen (`analyze_bookflow.py` im
     HL-Projekt). Bis dahin fließt hier nichts in Empfehlung/Trade ein.

Fail-safe: kurzer Timeout, Ergebnis gecacht; ist der HL-Server aus, liefert
`walls()` einfach None und der Chart bleibt unverändert.
"""
from __future__ import annotations
import json
import threading
import time
import urllib.request

from core.logger import get_logger

log = get_logger("hlwalls")

_TIMEOUT_S = 2.0     # der MT5-Loop darf nicht am HTTP hängen
_TTL_S = 10.0        # Cache — die Wände wandern langsamer als der Export-Takt (~5 s)
_MARKET_TTL_S = 300.0   # Funding/OI aendern sich stuendlich — 5 min genuegt
# ⚠ EIGENER, laengerer Timeout: `/api/market` fragt die HL-API live nach ALLEN
# Oel-Maerkten (4 Stueck) und braucht deutlich laenger als der Waende-Endpunkt,
# der aus einer lokalen DB liest. Mit dem kurzen Waende-Timeout lief der Aufruf
# immer ins Leere — und der stille `except` machte daraus ein wortloses None.
_MARKET_TIMEOUT_S = 12.0
_MAX_AGE_S = 60.0    # älter = HL-Feed steht → nicht mehr zeichnen
_STALE_GRACE_S = 120.0  # nach einem Fehlschlag den letzten guten Stand so lange
                        # weiterreichen (Blink-Fix — s. Kommentar in `walls()`)


class HLWalls:
    def __init__(self, url: str = "http://127.0.0.1:8001/api/walls",
                 enabled: bool = False):
        self.url = url
        self.enabled = bool(enabled)
        self._lock = threading.Lock()
        self._cache: dict | None = None
        self._cache_ts = 0.0
        self._fail_logged = False
        self._mkt_cache: dict | None = None
        self._mkt_ts = 0.0

    def market(self) -> dict | None:
        """HL-KONTEXT je Oel-Markt: Funding, Open Interest, Oracle-Premium.

        ⚠⚠ REINE ANZEIGE — kein Verdict-Gewicht, kein Trade-Trigger. Gemessen
        (`analyze_hl_funding.py`, 2026-08-06, 5.095 Funding-Stundenwerte ueber
        7 Monate gegen den Pepperstone-CFD): **entueberlappt haelt KEIN Bucket**
        die 2-Stichproben-Regel. Der erste Lauf sah nach mehreren robusten
        Effekten aus — das war Autokorrelation (stuendliche Beobachtungen,
        4/12/24-h-Fenster teilen sich fast ihre ganze Laenge).
        Deshalb steht das hier als Kontext in den Meldungen und NICHT in der
        Empfehlung.

        Quelle ist das LOKALE Dashboard (Port 8001), nicht die HL-API direkt —
        keine neue externe Abhaengigkeit im Trading-Bot. Eigene, laengere
        Drosselung (5 min): Funding aendert sich stuendlich, das braucht nicht
        den 5-s-Takt der Waende. Fail-safe: bei Fehlern None.
        """
        if not self.enabled:
            return None
        now = time.time()
        with self._lock:
            if (self._mkt_cache is not None
                    and now - self._mkt_ts < _MARKET_TTL_S):
                return self._mkt_cache
        aus = None
        try:
            url = self.url.replace("/api/walls", "/api/market")
            with urllib.request.urlopen(url, timeout=_MARKET_TIMEOUT_S) as r:
                d = json.loads(r.read().decode())
            if d.get("ok"):
                for m in (d.get("markets") or []):
                    # Nur der Markt, aus dem auch die Waende kommen.
                    if m.get("market") != "xyz:CL":
                        continue
                    mark, orac = m.get("mark_px"), m.get("oracle_px")
                    aus = {"funding": m.get("funding"),
                           "open_interest": m.get("open_interest"),
                           "mark": mark, "oracle": orac,
                           "premium": (float(mark) - float(orac))
                                      if (mark and orac) else None,
                           "day_volume": m.get("day_volume"),
                           "network": d.get("network")}
                    break
        except Exception:
            aus = None                      # still — Kontext darf nie stoeren
        with self._lock:
            if aus is not None:
                self._mkt_cache, self._mkt_ts = aus, now
            return self._mkt_cache

    def walls(self) -> dict | None:
        """{'bid','bid_sz','ask','ask_sz','basis','imb5'} auf MT5-Preisniveau —
        oder None (aus / HL nicht erreichbar / Basis unbekannt / Daten zu alt)."""
        if not self.enabled:
            return None
        now = time.time()
        with self._lock:
            if self._cache is not None and now - self._cache_ts < _TTL_S:
                return self._cache
        data = None
        try:
            with urllib.request.urlopen(self.url, timeout=_TIMEOUT_S) as r:
                d = json.loads(r.read().decode())
            if d.get("ok") and d.get("bid_mt5") and d.get("ask_mt5"):
                # Feed-Alter prüfen: ein stehengebliebenes Buch darf nicht als
                # aktuelle Liquidität gezeichnet werden.
                age = now - float(d.get("ts") or 0)
                if age <= _MAX_AGE_S:
                    data = {"bid": float(d["bid_mt5"]), "ask": float(d["ask_mt5"]),
                            "bid_sz": float(d.get("bid_sz") or 0),
                            "ask_sz": float(d.get("ask_sz") or 0),
                            "basis": d.get("basis"), "imb5": d.get("imb5"),
                            # Wirkrichtung je Wand (Pfeil im Chart) + größere Wand.
                            # ⚠ Lesart „die Wand hält" — INTERPRETATION, nicht gemessen.
                            "arrow_bid": d.get("arrow_bid") or "up",
                            "arrow_ask": d.get("arrow_ask") or "down",
                            "dominant": d.get("dominant"),
                            # Liquiditäts-Trendlinie (Regression über den volumen-
                            # gewichteten Wand-Schwerpunkt): {t1,p1,t2,p2,dir,slope_per_h}
                            # — Zeiten sind LOKALE Epoch, der Aufrufer rechnet sie in
                            # Broker-Zeit um (MT5-Charts brauchen Broker-Zeit).
                            "trend": d.get("trend"),
                            # ── Ergänzt 2026-08-01 (User: „blende den Hyperliquid-
                            # Kurs neben den Pepperstone-Kurs ein"): der HL-MITTELKURS
                            # auf MT5-Niveau. HL `xyz:CL` notiert wegen anderem
                            # Kontrakt/Contango ~0,45 $ unter dem CFD — `basis` ist die
                            # serverseitig gemessene Differenz (Median über 60
                            # Snapshots), `mid_mt5` also direkt mit dem Broker-Kurs
                            # vergleichbar. Nützlich vor allem am Wochenende: HL
                            # handelt 24/7, Pepperstone nicht.
                            "hl_mid": (float(d["hl_mid"])
                                       if d.get("hl_mid") is not None else None),
                            "mid_mt5": (round(float(d["hl_mid"])
                                              + float(d.get("basis") or 0.0), 3)
                                        if d.get("hl_mid") is not None else None),
                            # ⚠ `basis_stale` (2026-08-02): der Broker tickt nicht mehr,
                            # die Basis ist auf dem letzten Stand mit LEBENDEM Markt
                            # eingefroren. Vorher rechnete die Gegenseite die Basis
                            # blind aus den letzten 60 Snapshots — bei stehendem Broker
                            # sog sie den kompletten HL-Wochenend-Move auf und
                            # `hl_mid + basis` ergab wieder den EINGEFRORENEN Broker-
                            # Kurs (real 86,41 statt 81,89, Differenz zum Broker-Bid
                            # nur 0,081 = zirkulär). Alles, was daraus abgeleitet wird,
                            # ist bei `basis_stale` eine SCHÄTZUNG und wird so markiert.
                            "basis_stale": bool(d.get("basis_stale")),
                            "mt5_still_s": d.get("mt5_still_s"),
                            "age_s": round(age, 1)}
            self._fail_logged = False
        except Exception as e:
            if not self._fail_logged:      # nur 1× melden, nicht jeden Tick
                log.info(f"HL-Wände nicht abrufbar ({e}) — Chart bleibt ohne "
                         f"Liquiditätslinien (HL-Dashboard auf 8001 läuft nicht?)")
                self._fail_logged = True
        with self._lock:
            if data is not None:
                self._last_good = data
                self._last_good_ts = now
            elif (getattr(self, "_last_good", None) is not None
                  and now - getattr(self, "_last_good_ts", 0.0) < _STALE_GRACE_S):
                # ⚠ BLINK-FIX (2026-07-30, User „die Liquidationspfeile blinken nur
                # manchmal auf"): ein einzelner Fehlschlag (HTTP-Timeout, HL-Server
                # kurz busy, Snapshot minimal zu alt) darf die Pfeile NICHT löschen.
                # Ohne den letzten guten Wert fehlen die LQ-Zeilen in der CSV → der
                # Indikator löscht beim Redraw alle Objekte und zeichnet sie nicht neu
                # → Pfeil weg, beim nächsten Erfolg wieder da = sichtbares Blinken.
                # Innerhalb der Grace wird der letzte gute Stand weitergereicht;
                # erst danach verschwinden die Pfeile wirklich (echter Ausfall).
                data = self._last_good
            self._cache = data
            self._cache_ts = now
        return data
