"""
Strategia del RETTANGOLO - BREAKOUT + RETEST.
Logica (da video 'Diari di Trading' + conferma Mattia):
1. Range = high/low candela daily precedente (ieri, 24h)
2. MAI LONG sul TOP del range (li' ci sono venditori)
3. MAI SHORT sul BOTTOM del range (li' ci sono compratori)
4. MAI tradare nel MID del range (zona caotica)
5. Aspetta che una candela intraday chiuda OLTRE il range (breakout)
6. Aspetta che la candela successiva torni a toccare il livello rotto (retest)
7. Sul retest: pattern 2 candele (prima opposta al segnale, poi Doji/Hammer nella direzione del segnale)
8. ENTRY: limite al TOP (per SHORT) o al BOTTOM (per LONG)
9. SL: leggermente sopra il max candela Hammer (per SHORT) / sotto il min (per LONG)
10. TP: MID range (opzionale: min/max corrente per trailing)
"""
from typing import List, Dict, Optional


def is_doji(k: Dict, body_ratio: float = 0.1) -> bool:
    body = abs(k["close"] - k["open"])
    rng = k["high"] - k["low"]
    if rng == 0:
        return False
    return body / rng <= body_ratio


def has_hammer_bullish(k: Dict, body_ratio: float = 0.3) -> bool:
    body = abs(k["close"] - k["open"])
    rng = k["high"] - k["low"]
    if rng == 0:
        return False
    lower_shadow = min(k["open"], k["close"]) - k["low"]
    return (lower_shadow >= 2 * body) and (body / rng <= body_ratio) and k["close"] >= k["open"]


def has_hammer_bearish(k: Dict, body_ratio: float = 0.3) -> bool:
    body = abs(k["close"] - k["open"])
    rng = k["high"] - k["low"]
    if rng == 0:
        return False
    upper_shadow = k["high"] - max(k["open"], k["close"])
    return (upper_shadow >= 2 * body) and (body / rng <= body_ratio) and k["close"] <= k["open"]


def _is_two_candle_pattern(cur: Dict, prev: Dict, side: str) -> bool:
    """Pattern 2 candele del video 'Diari di Trading':
    - SHORT: candela verde (oppure rialzista) + candela rossa con Doji/Hammer ribassista
    - LONG:  candela rossa (oppure ribassista) + candela verde con Doji/Hammer rialzista
    Returns: True se il pattern e' confermato.
    """
    if side == "SHORT":
        # prima candela: rialzista (verde) o rialzista contenuta (close >= open)
        prev_bullish = prev["close"] >= prev["open"]
        # seconda candela: rossa (close < open) + Doji o Hammer ribassista
        cur_bearish = cur["close"] < cur["open"]
        cur_pattern = is_doji(cur) or has_hammer_bearish(cur) or cur_bearish
        return prev_bullish and cur_pattern
    else:  # LONG
        # prima candela: ribassista (close < open)
        prev_bearish = prev["close"] < prev["open"]
        # seconda candela: verde (close >= open) + Doji o Hammer rialzista
        cur_bullish = cur["close"] >= cur["open"]
        cur_pattern = is_doji(cur) or has_hammer_bullish(cur) or cur_bullish
        return prev_bearish and cur_pattern


def _is_martello_spinta(spinta: Dict, martello: Dict, side: str, body_ratio_min: float = 0.5) -> bool:
    """Pattern 'MARTELLO + SPINTA' (rilassato v3.7, Mavis handoff 2026-07-20 PATTERN_RELAXED).
    - martello: candela con hammer bullish (per LONG) / hammer bearish (per SHORT) — segnale di inversione.
    - spinta: candela successiva con body >= body_ratio_min * body martello, stessa direzione (bullish per LONG).
    Differenza vs pattern 2 candele classico: richiede martello ESPLICITO (non solo bearish) e body ratio minimo.
    Use case: ZEC rompe d1_low, scende a LL, risale con martello UP, candela di spinta → continuazione rialzista.
    Pattern classico NON scatterebbe (manca retest d1_low), ma pattern visivo e' valido.
    """
    if side == "LONG":
        if not has_hammer_bullish(martello):
            return False
        cur_bullish = spinta["close"] >= spinta["open"]
        cur_body = abs(spinta["close"] - spinta["open"])
        prev_body = abs(martello["close"] - martello["open"])
        if prev_body <= 0:
            return False
        body_ratio = cur_body / prev_body
        return cur_bullish and body_ratio >= body_ratio_min
    else:  # SHORT
        if not has_hammer_bearish(martello):
            return False
        cur_bearish = spinta["close"] < spinta["open"]
        cur_body = abs(spinta["close"] - spinta["open"])
        prev_body = abs(martello["close"] - martello["open"])
        if prev_body <= 0:
            return False
        body_ratio = cur_body / prev_body
        return cur_bearish and body_ratio >= body_ratio_min


def compute_signal(prev_daily: Dict, intraday_history: List[Dict], idx: int,
                   sl_buffer_pct: float = 0.01, require_two_candles: bool = True,
                   allow_martello_spinta: bool = True) -> Optional[Dict]:
    """
    Calcola il segnale per la candela intraday corrente (idx in intraday_history).
    prev_daily: candela daily precedente (range di riferimento)
    intraday_history: lista di candele intraday (es. 2H) ordinate cronologicamente
    idx: indice della candela corrente da valutare
    sl_buffer_pct: buffer SL sopra/sotto max/min candela Hammer (default 0.5% se None)
    require_two_candles: True = pattern 2 candele (default video), False = 1 candela (legacy)
    allow_martello_spinta: True = accetta pattern MARTELLO+SPINTA rilassato (v3.7), False = solo pattern classico

    Ritorna None se nessun segnale, altrimenti dict con {signal, sl, tp, entry, reason, sl_ref}.
    """
    if idx < 1 or idx >= len(intraday_history):
        return None
    rng_top = prev_daily["high"]
    rng_bot = prev_daily["low"]
    rng_mid = (rng_top + rng_bot) / 2
    rng_size = rng_top - rng_bot
    if rng_size <= 0:
        return None
    # MAI LONG sul TOP, MAI SHORT sul BOTTOM (gia' implicito nella logica breakout+retest)
    # cerca breakout nelle ultime N candele PRIMA di idx
    lookback = min(10, idx)
    breakout_dir = None
    breakout_idx = None
    for j in range(idx - lookback, idx):
        bar = intraday_history[j]
        if bar["close"] > rng_top:
            breakout_dir = "up"
            breakout_idx = j
            break
        if bar["close"] < rng_bot:
            breakout_dir = "down"
            breakout_idx = j
            break
    if breakout_dir is None:
        return None
    # candela corrente: deve toccare il livello rotto (retest)
    cur = intraday_history[idx]
    prev_bar = intraday_history[idx - 1]

    # === FIX 2026-07-20 v3.7 (Mavis handoff PATTERN_RELAXED) ===
    # Pattern rilassato MARTELLO + SPINTA per long/short in area mid.
    # Scatterà anche SENZA retest formale di rng_top/rng_bot, purché ci sia:
    # 1) breakout nelle ultime N candele (gia' identificato)
    # 2) candela MARTELLO bullish/bearish DOPO il breakout (segnale inversione)
    # 3) candela di SPINTA successiva al martello (body >= 50% del body martello, stessa direzione)
    # Use case: ZEC rompe d1_low, scende a LL, risale con martello UP, candela spinta → continuazione.
    # Viene tentato PRIMA del pattern classico 2 candele (che richiede retest d1_low, spesso assente in area mid).
    if allow_martello_spinta and breakout_dir == "down":
        # Cerca martello bullish DOPO il breakout_idx
        for m in range(breakout_idx + 1, idx):
            cand_martello = intraday_history[m]
            if not has_hammer_bullish(cand_martello):
                continue
            # Il martello deve essere il nuovo minimo DOPO il breakout (close < breakout close)
            if cand_martello["low"] > intraday_history[breakout_idx]["low"]:
                continue  # non è il nuovo minimo
            # candela di spinta (successiva al martello)
            if m + 1 >= idx:
                continue
            cand_spinta = intraday_history[m + 1]
            if _is_martello_spinta(cand_spinta, cand_martello, "LONG"):
                # PATTERN VALIDO: martello + spinta dopo breakout down
                entry = cand_martello["low"]
                sl_ref = cand_martello["low"]
                sl = sl_ref * (1 - sl_buffer_pct)
                tp = rng_mid
                return {
                    "signal": "LONG",
                    "entry": round(entry, 6),
                    "sl": round(sl, 6),
                    "sl_ref": round(sl_ref, 6),
                    "tp": round(tp, 6),
                    "tp_alt": round(cand_martello["high"], 6),
                    "reason": f"breakout DOWN @ ts={intraday_history[breakout_idx]['ts']} + MARTELLO bullish @ ts={cand_martello['ts']} + SPINTA @ ts={cand_spinta['ts']} (pattern rilassato v3.7)"
                }
    if allow_martello_spinta and breakout_dir == "up":
        for m in range(breakout_idx + 1, idx):
            cand_martello = intraday_history[m]
            if not has_hammer_bearish(cand_martello):
                continue
            if cand_martello["high"] < intraday_history[breakout_idx]["high"]:
                continue  # non è il nuovo massimo
            if m + 1 >= idx:
                continue
            cand_spinta = intraday_history[m + 1]
            if _is_martello_spinta(cand_spinta, cand_martello, "SHORT"):
                entry = cand_martello["high"]
                sl_ref = cand_martello["high"]
                sl = sl_ref * (1 + sl_buffer_pct)
                tp = rng_mid
                return {
                    "signal": "SHORT",
                    "entry": round(entry, 6),
                    "sl": round(sl, 6),
                    "sl_ref": round(sl_ref, 6),
                    "tp": round(tp, 6),
                    "tp_alt": round(cand_martello["low"], 6),
                    "reason": f"breakout UP @ ts={intraday_history[breakout_idx]['ts']} + MARTELLO bearish @ ts={cand_martello['ts']} + SPINTA @ ts={cand_spinta['ts']} (pattern rilassato v3.7)"
                }
    # === FINE FIX v3.7 ===

    if breakout_dir == "up":
        # retest del TOP da sopra (prezzo scende a toccare TOP)
        if cur["low"] <= rng_top and cur["close"] <= rng_top:
            # pattern inversione ribassista
            pattern_ok = False
            if require_two_candles:
                # Pattern 2 candele del video: verde (precedente) + rossa/Hammer (corrente)
                pattern_ok = _is_two_candle_pattern(cur, prev_bar, "SHORT")
            else:
                # legacy 1 candela
                pattern_ok = is_doji(cur) or has_hammer_bearish(cur) or cur["close"] < cur["open"]
            if pattern_ok:
                entry = rng_top
                # SL: leggermente sopra il max della candela corrente (Hammer) — video
                sl_ref = cur["high"]
                sl = sl_ref * (1 + sl_buffer_pct)
                # TP: MID range (priorita') oppure minimo corrente (opzionale trailing)
                tp = rng_mid
                return {
                    "signal": "SHORT",
                    "entry": entry,
                    "sl": round(sl, 6),
                    "sl_ref": round(sl_ref, 6),  # max candela Hammer (per trailing)
                    "tp": round(tp, 6),
                    "tp_alt": round(cur["low"], 6),  # min corrente (opzionale trailing)
                    "reason": "breakout UP @ ts=" + str(intraday_history[breakout_idx]["ts"]) +
                              ", retest su TOP, pattern " + ("2 candele" if require_two_candles else "1 candela")
                }
    else:  # breakout down
        # retest del BOTTOM da sotto (prezzo sale a toccare BOTTOM)
        if cur["high"] >= rng_bot and cur["close"] >= rng_bot:
            pattern_ok = False
            if require_two_candles:
                pattern_ok = _is_two_candle_pattern(cur, prev_bar, "LONG")
            else:
                pattern_ok = is_doji(cur) or has_hammer_bullish(cur) or cur["close"] > cur["open"]
            if pattern_ok:
                entry = rng_bot
                # SL: leggermente sotto il min della candela corrente (Hammer) — video
                sl_ref = cur["low"]
                sl = sl_ref * (1 - sl_buffer_pct)
                tp = rng_mid
                return {
                    "signal": "LONG",
                    "entry": entry,
                    "sl": round(sl, 6),
                    "sl_ref": round(sl_ref, 6),  # min candela Hammer (per trailing)
                    "tp": round(tp, 6),
                    "tp_alt": round(cur["high"], 6),  # max corrente (opzionale trailing)
                    "reason": "breakout DOWN @ ts=" + str(intraday_history[breakout_idx]["ts"]) +
                              ", retest su BOTTOM, pattern " + ("2 candele" if require_two_candles else "1 candela")
                }
    return None


def scan_signals(prev_daily: Dict, intraday_history: List[Dict], sl_buffer_pct: float = 0.01) -> list:
    """Scansiona tutte le candele intraday e ritorna la lista di segnali storici."""
    signals = []
    for i in range(1, len(intraday_history)):
        sig = compute_signal(prev_daily, intraday_history, i, sl_buffer_pct)
        if sig:
            sig["date"] = intraday_history[i]["date"]
            sig["entry"] = sig["entry"]  # entry price = TOP/BOTTOM
            signals.append(sig)
    return signals


def backtest(klines_daily: List[Dict], klines_intraday: List[Dict],
             sl_buffer_pct: float = 0.01) -> List[Dict]:
    """
    Backtest della strategia: per ogni candela daily 'ieri', calcola segnali
    sulle candele intraday fino alla prossima daily.
    """
    trades = []
    # mappa: per ogni candela daily, identifica l'intervallo intraday successivo
    for d in range(1, len(klines_daily)):
        prev_day = klines_daily[d - 1]
        cur_day = klines_daily[d]
        # filtra intraday: solo candele il giorno 'cur_day' e fino alla sua chiusura
        day_start = cur_day["ts"]
        day_end = day_start + 24 * 3600 * 1000
        day_intra = [k for k in klines_intraday if day_start <= k["ts"] < day_end]
        if len(day_intra) < 2:
            continue
        # mappa indice in intraday_history completo
        # Per semplicità, uso l'indice locale (in day_intra)
        for i in range(1, len(day_intra)):
            sig = compute_signal(prev_day, day_intra, i, sl_buffer_pct)
            if not sig:
                continue
            entry_bar = day_intra[i + 1] if i + 1 < len(day_intra) else day_intra[i]
            entry = sig["entry"]  # limit al TOP/BOTTOM
            sl = sig["sl"]
            tp = sig["tp"]
            outcome = "OPEN"
            exit_price = day_intra[-1]["close"]
            for j in range(i + 1, len(day_intra)):
                bar = day_intra[j]
                if sig["signal"] == "LONG":
                    if bar["low"] <= sl:
                        outcome = "SL"; exit_price = sl; break
                    if bar["high"] >= tp:
                        outcome = "TP"; exit_price = tp; break
                else:
                    if bar["high"] >= sl:
                        outcome = "SL"; exit_price = sl; break
                    if bar["low"] <= tp:
                        outcome = "TP"; exit_price = tp; break
            if sig["signal"] == "LONG":
                pnl_pct = (exit_price - entry) / entry * 100
            else:
                pnl_pct = (entry - exit_price) / entry * 100
            trades.append({
                "date": day_intra[i]["date"],
                "signal": sig["signal"],
                "entry": entry,
                "sl": sl,
                "tp": tp,
                "outcome": outcome,
                "exit": exit_price,
                "pnl_pct": round(pnl_pct, 2),
            })
    return trades
