"""
core/structure.py — Marktstruktur-Erkennung (ANZEIGE, kein Signal)
==================================================================
Erkennt aus den M30-Bars die klassische Price-Action-Struktur und liefert sie
als Kontext fürs Dashboard:
  • Swing-Folge  HH / HL / LH / LL  (Pivot-Hochs/-Tiefs, jeweils vs. Vorgänger)
  • letzter BOS  (Break of Structure: Richtung + gebrochenes Level)
  • Regressionskanal  (Richtung + Position des Kurses im Kanal 0..1)
  • Gesamt-Struktur  up / down / range

REINE ANZEIGE — wie TF-Ampel/Squeeze/Bounce: KEIN Trade-Trigger, KEIN Verdict-
Gewicht. Die handelbaren Varianten sind separat gemessen & verworfen:
  BOS-Entry ≈ Momentum-Continuation (`backtest_momentum.py`, regime-abhängig),
  Kanal-/Zonen-Bounce ≈ P(break)-Level-Bounce (6× belegt: Münzwurf am Extrem).
Deshalb malt dieses Modul KEINE Richtung/Prognose — es beschreibt nur den Ist-Zustand.

Thread-sicher: refresh_market(sym) holt die Bars unter mt5_lock (~30 s gedrosselt),
snapshot() liefert den letzten Stand ohne MT5-Call.
"""
from __future__ import annotations
import threading
import time

import MetaTrader5 as mt5

from core.mt5_utils import mt5_lock
from core.logger import get_logger

log = get_logger("structure")

_TF          = mt5.TIMEFRAME_M30   # Struktur auf M30 (klare Swings, wie der Referenz-Chart)
_N_BARS      = 220
_PIVOT_K     = 3        # Swing-Pivot-Fenster (k Bars je Seite)
_REG_N       = 60       # Regressionsfenster für den Kanal (~30 h auf M30)
_SLOPE_DEAD  = 0.015    # |Steigung/Bar| < dead×ATR → Kanal "flat"
_MAX_SWINGS  = 6        # so viele letzte Swings ausgeben
_REFRESH_S   = 30.0     # Drossel (Struktur ändert sich langsam, spart Lock-Zeit)


def _atr(highs, lows, closes, p=14):
    trs = []
    for i in range(1, len(closes)):
        trs.append(max(highs[i]-lows[i], abs(highs[i]-closes[i-1]), abs(lows[i]-closes[i-1])))
    return (sum(trs[-p:]) / min(len(trs), p)) if trs else None


def _pivots(highs, lows, k):
    """Alternierende Swing-Punkte → Liste (index, price, kind) mit kind 'H'/'L'.
    Swing-High bei i: höchster Bar im Fenster [i-k .. i+k] und lokales Maximum."""
    n = len(highs)
    raw = []
    for i in range(k, n - k):
        win_hi = max(highs[i-k:i+k+1]); win_lo = min(lows[i-k:i+k+1])
        if highs[i] == win_hi and highs[i] > highs[i-1] and highs[i] >= highs[i+1]:
            raw.append((i, highs[i], "H"))
        elif lows[i] == win_lo and lows[i] < lows[i-1] and lows[i] <= lows[i+1]:
            raw.append((i, lows[i], "L"))
    # Alternierung erzwingen: zwei gleiche Typen in Folge → den extremeren behalten
    out = []
    for p in raw:
        if out and out[-1][2] == p[2]:
            if (p[2] == "H" and p[1] > out[-1][1]) or (p[2] == "L" and p[1] < out[-1][1]):
                out[-1] = p
        else:
            out.append(p)
    return out


def _classify(pivots):
    """Swing-Folge als HH/HL/LH/LL (vs. jeweils vorheriges High bzw. Low)."""
    labels = []
    last_h = last_l = None
    for idx, price, kind in pivots:
        if kind == "H":
            lab = ("HH" if (last_h is not None and price > last_h)
                   else "LH" if last_h is not None else "H")
            last_h = price
        else:
            lab = ("HL" if (last_l is not None and price > last_l)
                   else "LL" if last_l is not None else "L")
            last_l = price
        labels.append({"type": lab, "price": round(price, 3), "idx": idx})
    return labels


def _trend_state(labels):
    recent = [l["type"] for l in labels[-4:]]
    ups = sum(1 for t in recent if t in ("HH", "HL"))
    dns = sum(1 for t in recent if t in ("LH", "LL"))
    if ups >= 3 and ups > dns:
        return "up"
    if dns >= 3 and dns > ups:
        return "down"
    return "range"


def _last_bos(labels, n_bars):
    """Letzter Break of Structure: jüngstes HH (bullisch, Vorlauf-Hoch gebrochen)
    bzw. LL (bärisch). Level = das gebrochene vorige Extrem; bars_ago aus dem Index."""
    prev_h = prev_l = None
    bos = None
    for l in labels:
        if l["type"] in ("HH", "LH"):
            if l["type"] == "HH" and prev_h is not None:
                bos = {"dir": "up", "level": prev_h, "idx": l["idx"]}
            prev_h = l["price"]
        else:
            if l["type"] == "LL" and prev_l is not None:
                bos = {"dir": "down", "level": prev_l, "idx": l["idx"]}
            prev_l = l["price"]
    if bos:
        bos["bars_ago"] = max(0, (n_bars - 1) - bos.pop("idx"))
        bos["level"] = round(bos["level"], 3)
    return bos


def _channel(closes, atr):
    N = min(_REG_N, len(closes))
    if N < 5:
        return None
    ys = closes[-N:]
    mx = (N - 1) / 2.0
    my = sum(ys) / N
    sxx = sum((x - mx) ** 2 for x in range(N))
    sxy = sum((x - mx) * (ys[x] - my) for x in range(N))
    slope = sxy / sxx if sxx else 0.0
    intercept = my - slope * mx
    resid = [ys[x] - (slope * x + intercept) for x in range(N)]
    up_off, lo_off = max(resid), min(resid)
    last_x = N - 1
    mid = slope * last_x + intercept
    upper, lower = mid + up_off, mid + lo_off
    width = upper - lower
    pos = (ys[-1] - lower) / width if width > 0 else 0.5
    if atr and abs(slope) < _SLOPE_DEAD * atr:
        d = "flat"
    else:
        d = "up" if slope > 0 else "down"
    return {"dir": d, "pos": round(max(0.0, min(1.0, pos)), 2),
            "upper": round(upper, 3), "lower": round(lower, 3), "mid": round(mid, 3),
            "slope_atr": round(slope / atr, 3) if atr else None}


def _channel_anchors(closes, times, atr):
    """Kanal als 2 Ankerpunkte je Linie (Fensterstart + letzter abgeschl. Bar) mit
    BROKER-Zeiten — für die MQL5-Bridge (OBJ_TREND, nach rechts verlängert).
    closes/times = abgeschlossene Bars (gleich lang). Gibt {t1,t2,dir,upper,mid,lower}."""
    N = min(_REG_N, len(closes))
    if N < 5 or len(times) < N:
        return None
    seg = closes[-N:]; tt = times[-N:]
    mx = (N - 1) / 2.0; my = sum(seg) / N
    sxx = sum((x - mx) ** 2 for x in range(N))
    sxy = sum((x - mx) * (seg[x] - my) for x in range(N))
    b = sxy / sxx if sxx else 0.0
    a = my - b * mx
    resid = [seg[x] - (a + b * x) for x in range(N)]
    up_off, lo_off = max(resid), min(resid)
    m1 = a; m2 = a + b * (N - 1)
    d = "flat" if (atr and abs(b) < _SLOPE_DEAD * atr) else ("up" if b > 0 else "down")
    return {"t1": int(tt[0]), "t2": int(tt[-1]), "dir": d,
            "upper": [round(m1 + up_off, 3), round(m2 + up_off, 3)],
            "mid":   [round(m1, 3),          round(m2, 3)],
            "lower": [round(m1 + lo_off, 3), round(m2 + lo_off, 3)]}


def channel_series(closes, atr, k):
    """Regressionskanal (mid/upper/lower) als Arrays der LETZTEN k Bars fürs
    Chart-Overlay — Regression über die letzten _REG_N ABGESCHLOSSENEN Bars,
    linear über alle k Bars extrapoliert (volle Chart-Breite). closes = alle
    Closes (letzter = offener Bar). Gibt {dir, mid[], upper[], lower[]} zurück
    (jeweils Länge k, deckungsgleich mit den zurückgelieferten Bars) oder None."""
    if k < 2 or len(closes) < 6:
        return None
    cc = closes[:-1]                      # nur abgeschlossene Bars (wie die Struktur)
    N = min(_REG_N, len(cc))
    if N < 5:
        return None
    seg = cc[-N:]
    mx = (N - 1) / 2.0
    my = sum(seg) / N
    sxx = sum((x - mx) ** 2 for x in range(N))
    sxy = sum((x - mx) * (seg[x] - my) for x in range(N))
    b = sxy / sxx if sxx else 0.0
    a = my - b * mx                        # Preis bei x=0 (Fensterstart)
    resid = [seg[x] - (a + b * x) for x in range(N)]
    up_off, lo_off = max(resid), min(resid)
    x0 = len(cc) - N                       # cc-Index von x=0
    base = len(closes) - k                 # closes-Index des ersten Ausgabe-Bars
    mid, up, lo = [], [], []
    for j in range(k):
        x = (base + j) - x0                # x relativ zum Fensterstart (extrapoliert)
        m = a + b * x
        mid.append(round(m, 3)); up.append(round(m + up_off, 3)); lo.append(round(m + lo_off, 3))
    d = "flat" if (atr and abs(b) < _SLOPE_DEAD * atr) else ("up" if b > 0 else "down")
    return {"dir": d, "mid": mid, "upper": up, "lower": lo}


class MarketStructure:
    def __init__(self):
        self._snap: dict = {"trend": None, "swings": [], "last_swing": None,
                            "bos": None, "channel": None, "tf": "M30", "error": None}
        self._last_refresh = 0.0
        self._lock = threading.Lock()

    def refresh_market(self, sym: str):
        now = time.time()
        if now - self._last_refresh < _REFRESH_S:
            return
        try:
            with mt5_lock(timeout=2) as got:
                if not got:
                    return
                bars = mt5.copy_rates_from_pos(sym, _TF, 0, _N_BARS)
            if bars is None or len(bars) < _REG_N + 5:
                return
            # letzte (offene) Kerze weglassen → nur abgeschlossene Struktur
            highs  = [float(b["high"])  for b in bars[:-1]]
            lows   = [float(b["low"])   for b in bars[:-1]]
            closes = [float(b["close"]) for b in bars[:-1]]
            times  = [int(b["time"])    for b in bars[:-1]]   # Broker-Zeit (MQL5-Anker)
            n = len(closes)
            atr = _atr(highs, lows, closes)
            piv = _pivots(highs, lows, _PIVOT_K)
            labels = _classify(piv)
            snap = {
                "trend":        _trend_state(labels) if labels else "range",
                "swings":       [{"type": l["type"], "price": l["price"]}
                                 for l in labels[-_MAX_SWINGS:]],
                "last_swing":   labels[-1]["type"] if labels else None,
                "bos":          _last_bos(labels, n),
                "channel":      _channel(closes, atr),
                "channel_line": _channel_anchors(closes, times, atr),
                "tf":           "M30",
                "error":        None,
            }
            with self._lock:
                self._snap = snap
            self._last_refresh = now
        except Exception as e:
            with self._lock:
                self._snap["error"] = str(e)[:120]
            log.warning(f"MarketStructure.refresh: {e}")

    def snapshot(self) -> dict:
        with self._lock:
            return dict(self._snap)
