"""
core/elliott.py — Elliott-Wave-/FVG-Heuristik
==============================================
Prinzipienbasierter (NICHT perfekter) Elliott-Wave-Motor als Analyse-Input
für den KI-Agenten. EW-Zählung ist diskretionär — dieser Motor liefert eine
*plausible* Zählung mit Validitäts-Flag, keine Gewissheit.

Was er macht:
  1. ATR-ZigZag → Swing-Pivots (H/L) und Legs.
  2. Impuls-Erkennung: 5 Legs als 1-2-3-4-5, geprüft gegen die drei harten
     EW-Regeln (W2 < Start, W3 nicht der kürzeste, W4 ohne W1-Überlappung).
  3. Stand im Zyklus: vollständiger Impuls (→ Reversal-Watch) oder laufende
     Welle 5/3 (→ Fib-Extension-Ziel projizieren).
  4. FVG-Erkennung (3-Kerzen-Imbalance) der jüngsten unfilled Gaps.
  5. Erschöpfungs-Flag, wenn der Kurs das projizierte Ziel erreicht/überschritten hat.

Output via snapshot() — wird in den Agent-Kontext gegeben, damit das LLM mit
EW-Struktur (Zielzone, Erschöpfung, FVG) argumentiert.
"""

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("elliott")

_N_BARS      = 240
_ATR_PERIOD  = 14
_ZZ_ATR      = 0.7    # Swing-Umkehr ab dieser ATR-Bewegung (etwas grober als wave_rec)
_FVG_LOOKBACK = 60    # Kerzen, in denen nach offenen FVGs gesucht wird
_STALE_S     = 180

_TF_LABELS = {
    mt5.TIMEFRAME_M1: "M1", mt5.TIMEFRAME_M5: "M5", mt5.TIMEFRAME_M15: "M15",
    mt5.TIMEFRAME_M30: "M30", mt5.TIMEFRAME_H1: "H1", mt5.TIMEFRAME_H4: "H4",
}


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


def _zigzag(highs, lows, thr):
    """ATR-ZigZag → Liste (idx, price, kind 'H'/'L'), chronologisch."""
    n = len(highs)
    pivots = []
    direction = 0
    hi_idx, hi = 0, highs[0]
    lo_idx, lo = 0, lows[0]
    for i in range(1, n):
        if highs[i] > hi:
            hi, hi_idx = highs[i], i
        if lows[i] < lo:
            lo, lo_idx = lows[i], i
        if direction >= 0 and hi - lows[i] >= thr:
            pivots.append((hi_idx, hi, "H")); direction = -1
            lo, lo_idx = lows[i], i
        elif direction <= 0 and highs[i] - lo >= thr:
            pivots.append((lo_idx, lo, "L")); direction = 1
            hi, hi_idx = highs[i], i
    return pivots


def _detect_fvg(highs, lows, lookback=_FVG_LOOKBACK):
    """Fair Value Gaps (3-Kerzen-Imbalance) der jüngsten Kerzen, noch offen.
    Bullish FVG: low[i] > high[i-2] (Lücke nach oben).
    Bearish FVG: high[i] < low[i-2] (Lücke nach unten)."""
    n = len(highs)
    cur = (highs[-1] + lows[-1]) / 2.0
    out = []
    for i in range(max(2, n - lookback), n):
        if lows[i] > highs[i - 2]:          # bullish FVG (Support unter dem Kurs)
            lo, hi = highs[i - 2], lows[i]
            if cur >= lo:                    # noch nicht nach unten durchbrochen
                out.append(("bullish", lo, hi))
        elif highs[i] < lows[i - 2]:        # bearish FVG (Widerstand über dem Kurs)
            lo, hi = highs[i], lows[i - 2]
            if cur <= hi:                    # noch nicht nach oben durchbrochen
                out.append(("bearish", lo, hi))
    if not out:
        return None
    typ, lo, hi = out[-1]                    # jüngster offener FVG
    return {"type": typ, "low": round(lo, 3), "high": round(hi, 3),
            "mid": round((lo + hi) / 2.0, 3)}


def _label_impulse(pivots):
    """Versucht, die letzten Pivots als 5-Wellen-Impuls zu labeln.
    Liefert dict mit Zählung + Validität oder None.
    Down-Impuls: H L H L H L  (W1 L, W2 H, W3 L, W4 H, W5 L)
    Up-Impuls spiegelbildlich."""
    if len(pivots) < 5:
        return None
    # bis zu 6 letzte Pivots betrachten
    p = pivots[-6:]
    prices = [x[1] for x in p]
    kinds = [x[2] for x in p]

    # Richtung aus dem Muster: beginnt mit H → Down-Impuls, mit L → Up-Impuls
    # Wir brauchen alternierende Kinds.
    if any(kinds[i] == kinds[i + 1] for i in range(len(kinds) - 1)):
        return None  # nicht sauber alternierend

    down = kinds[0] == "H"
    # Indizes der Wellen-Endpunkte (Start=p[0])
    # 5 Legs brauchen 6 Pivots; bei 5 Pivots ist W5 noch offen.
    have = len(p)

    def leg(a, b):
        return abs(prices[b] - prices[a])

    if have >= 6:
        start, w1, w2, w3, w4, w5 = prices[-6:]
        L1, L3, L5 = leg(-6, -5), leg(-4, -3), leg(-2, -1)
        # EW-Regeln
        if down:
            r2 = w2 < start          # W2-Hoch unter Start
            r4 = w4 < w1             # W4-Hoch unter W1-Tief (keine Überlappung)
        else:
            r2 = w2 > start
            r4 = w4 > w1
        r3 = L3 >= min(L1, L5) and not (L3 < L1 and L3 < L5)  # W3 nicht der kürzeste
        valid = r2 and r3 and r4
        return {"pattern": "impulse_down" if down else "impulse_up",
                "wave": "5", "complete": True, "valid": valid,
                "w5_end": round(prices[-1], 3),
                "w4_end": round(prices[-2], 3),
                "w1_len": round(L1, 3),
                "dir": "down" if down else "up"}
    else:  # 5 Pivots: W4 fertig, W5 läuft noch
        start, w1, w2, w3, w4 = prices[-5:]
        L1, L3 = leg(-5, -4), leg(-3, -2)
        if down:
            r2 = w2 < start; r4 = w4 < w1
        else:
            r2 = w2 > start; r4 = w4 > w1
        r3 = L3 >= L1 * 0.6   # W3 mindestens vergleichbar mit W1
        valid = r2 and r3 and r4
        return {"pattern": "impulse_down" if down else "impulse_up",
                "wave": "5", "complete": False, "valid": valid,
                "w4_end": round(prices[-1], 3), "w1_len": round(L1, 3),
                "dir": "down" if down else "up"}


class ElliottAnalyzer:
    def __init__(self, timeframe: int = mt5.TIMEFRAME_M15):
        self._lock = threading.Lock()
        self._tf = timeframe
        self._snap: dict = {}
        self._ts: float = 0.0
        self._error: str | None = None

    def set_timeframe(self, tf: int):
        with self._lock:
            self._tf = tf
            self._snap = {}
            self._ts = 0.0

    def refresh_market(self, sym: str):
        with self._lock:
            tf = self._tf
        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) < _ATR_PERIOD + 10:
            with self._lock:
                self._error = "keine Bars"
            return
        highs = [float(b["high"]) for b in bars]
        lows  = [float(b["low"])  for b in bars]
        closes = [float(b["close"]) for b in bars]
        atr = _atr(highs, lows, closes)
        if not atr or atr <= 0:
            with self._lock:
                self._error = "ATR=0"
            return
        cur = closes[-1]
        pivots = _zigzag(highs, lows, _ZZ_ATR * atr)
        fvg = _detect_fvg(highs, lows)
        snap = self._build(pivots, cur, atr, fvg, tf)
        with self._lock:
            self._snap = snap
            self._ts = time.time()
            self._error = None
        # Nur bei ÄNDERUNG loggen — lief vorher je Tick (~6k identische Zeilen
        # pro 5-MB-Logrotation) und flutete das Log.
        line = (f"{sym} {snap.get('pattern')}/{snap.get('wave')} "
                f"target={snap.get('target')} exhausted={snap.get('exhaustion')}")
        if line != getattr(self, "_last_log_line", None):
            self._last_log_line = line
            log.info(line)

    def _build(self, pivots, cur, atr, fvg, tf):
        tf_lbl = _TF_LABELS.get(tf, str(tf))
        out = {"tf": tf_lbl, "pattern": "unclear", "wave": "?",
               "dir": None, "target": None, "target_label": None,
               "exhaustion": False, "invalidation": None,
               "valid": False, "fvg": fvg, "n_pivots": len(pivots),
               "note": ""}
        imp = _label_impulse(pivots)
        if not imp:
            out["note"] = "kein sauberer Impuls erkennbar"
            return out
        out.update(pattern=imp["pattern"], dir=imp["dir"], valid=imp["valid"])
        down = imp["dir"] == "down"

        if not imp["complete"]:
            # Welle 5 läuft → Ziel = W4-Ende ∓ (1.0 / 1.618) × W1-Länge
            w4 = imp["w4_end"]; l1 = imp["w1_len"]
            t100 = w4 - l1 if down else w4 + l1
            t162 = w4 - 1.618 * l1 if down else w4 + 1.618 * l1
            out["wave"] = "5 (laufend)"
            out["target"] = round(t162, 3)
            out["target_label"] = "1.618 W5"
            out["invalidation"] = round(w4, 3)   # über/unter W4 = Zählung fraglich
            # Erschöpfung, wenn Kurs das 1.0-Ziel erreicht/überschritten hat
            reached = (cur <= t100) if down else (cur >= t100)
            out["exhaustion"] = reached
            out["note"] = (f"Welle 5 {'abwärts' if down else 'aufwärts'} läuft, "
                           f"Ziel ~{out['target']} (1.0 bei ~{round(t100,3)})"
                           + ("  — Ziel erreicht, Reversal-Risiko" if reached else ""))
        else:
            # Impuls vollständig → Reversal in Gegenrichtung wahrscheinlich
            w5 = imp["w5_end"]; l1 = imp["w1_len"]
            out["wave"] = "5 (vollendet)"
            out["dir"] = imp["dir"]
            # Reversal-Ziele = Fib-Retracement des Gesamtimpulses (grob via W1-Länge)
            out["target"] = round((w5 + l1) if down else (w5 - l1), 3)
            out["target_label"] = "Reversal ~0.382–0.618"
            out["invalidation"] = round(w5, 3)
            # nach Vollendung gilt der Impuls als erschöpft
            out["exhaustion"] = True
            out["note"] = (f"Impuls {'abwärts' if down else 'aufwärts'} vollendet bei "
                           f"{w5} → Reversal {'aufwärts' if down else 'abwärts'} wahrscheinlich")
        return out

    def snapshot(self) -> dict:
        with self._lock:
            d = dict(self._snap)
            d["error"] = self._error
            d["last_update"] = self._ts
            d["stale"] = (not self._ts) or (time.time() - self._ts > _STALE_S)
            return d
