"""
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


# ============================================================
# RETTANGOLO_SIMPLE v1.0 - regola nuova Mattia 2026-08-08
# ============================================================
# Regola (testata su ARUSDT/SOL/BTC/ETH/WIF/ZEC 15M, ultimi 30gg, studio 2026-08-08):
# 1. Range = high/low delle ultime N candele intraday PRIMA di idx (rolling, NON daily).
# 2. Rottura: candela corrente chiude OLTRE il range.
#    - SHORT: close > range_top
#    - LONG:  close < range_bot
# 3. Dopo la rottura, aspetta un punto di swing confermato (HH/HL/LH/LL lookback 5).
#    Swing detection: candela con high > tutti gli high delle N precedenti (HH) o high < tutti (LH),
#    candela con low > tutti i low delle N precedenti (HL) o low < tutti (LL).
#    Classificazione: confronto col precedente swing dello stesso tipo.
# 4. Dopo lo swing point, aspetta 2 candele consecutive UP/DOWN coerenti col lato.
#    - LONG:  close > open per 2 candele
#    - SHORT: close < open per 2 candele
# 5. La SECONDA candela deve chiudere OLTRE la base nel verso del segnale:
#    - LONG:  2nd close > range_bot (rientro sopra base inf)
#    - SHORT: 2nd close < range_top (rientro sotto base sup)
# 6. ENTER con entry = close 2a candela.
# 7. SL = low/high del punto di swing (sotto/sopra).
# 8. TP = entry +/- R:R * |entry - SL| (R:R default 1.5, configurabile).

def _find_swing_simple(candles, idx, lookback):
    """Trova swing point alla candela idx.
    Ritorna dict {type, price, kind} o None."""
    if idx < lookback:
        return None
    c = candles[idx]
    # Swing high: high > tutti gli high delle `lookback` candele precedenti
    is_sh = all(c["high"] > candles[idx - j]["high"] for j in range(1, lookback + 1))
    # Swing low: low < tutti i low delle `lookback` candele precedenti
    is_sl = all(c["low"] < candles[idx - j]["low"] for j in range(1, lookback + 1))
    if not is_sh and not is_sl:
        return None
    # Classificazione HH/HL/LH/LL vs precedente swing dello stesso tipo
    # (qui semplifichiamo: se swing high dopo uno swing high piu' basso = HH, altrimenti LH; idem low)
    # Cerco il precedente swing high/low nelle candele PRIMA di idx
    prev_sh_idx = None
    prev_sl_idx = None
    for j in range(idx - 1, lookback - 1, -1):
        if prev_sh_idx is None and all(candles[j]["high"] > candles[j - k]["high"] for k in range(1, lookback + 1) if j - k >= 0):
            prev_sh_idx = j
        if prev_sl_idx is None and all(candles[j]["low"] < candles[j - k]["low"] for k in range(1, lookback + 1) if j - k >= 0):
            prev_sl_idx = j
        if prev_sh_idx is not None and prev_sl_idx is not None:
            break
    if is_sh:
        if prev_sh_idx is None or c["high"] > candles[prev_sh_idx]["high"]:
            return {"type": "HH", "price": c["high"], "kind": "high", "idx": idx}
        return {"type": "LH", "price": c["high"], "kind": "high", "idx": idx}
    if is_sl:
        if prev_sl_idx is None or c["low"] < candles[prev_sl_idx]["low"]:
            return {"type": "LL", "price": c["low"], "kind": "low", "idx": idx}
        return {"type": "HL", "price": c["low"], "kind": "low", "idx": idx}
    return None


def _find_range_rolling(candles, idx, n_candles):
    """Range = high/low delle ultime n_candles candele PRIMA di idx."""
    start = max(0, idx - n_candles)
    window = candles[start:idx]
    if not window:
        return None
    return max(c["high"] for c in window), min(c["low"] for c in window)


def compute_signal_simple(intraday_history, idx,
                          swing_lookback=5, range_lookback=24, rr_ratio=1.5):
    """RETTANGOLO_SIMPLE v1.0 - regola nuova Mattia 2026-08-08.
    intraday_history: lista candele intraday ordinate cronologicamente.
    idx: indice candela corrente.
    Ritorna dict segnale o None."""
    if idx < range_lookback + swing_lookback + 3:
        return None
    rng = _find_range_rolling(intraday_history, idx, range_lookback)
    if rng is None:
        return None
    range_top, range_bot = rng
    cur = intraday_history[idx]
    # Rottura: candela corrente chiude OLTRE il range
    side = None
    if cur["close"] > range_top:
        side = "Sell"  # SHORT
    elif cur["close"] < range_bot:
        side = "Buy"  # LONG
    if side is None:
        return None
    # Cerca il PRIMO swing point (HH/HL/LH/LL) DOPO la rottura
    # Lookback entro le ultime 8 candele prima di idx
    swing = None
    for j in range(max(0, idx - 8), idx):
        s = _find_swing_simple(intraday_history, j, swing_lookback)
        if s is not None:
            swing = s
            break
    if swing is None:
        return None
    # Dopo lo swing, verifica 2 candele consecutive UP/DOWN coerenti col lato
    if swing["idx"] + 2 > idx:
        return None
    c1 = intraday_history[swing["idx"] + 1]
    c2 = intraday_history[swing["idx"] + 2]
    if side == "Sell":
        if not (c1["close"] < c1["open"] and c2["close"] < c2["open"]):
            return None
        if not (c2["close"] < range_top):
            return None
        # SHORT: TP sotto entry, SL sopra
        entry = c2["close"]
        sl = swing["price"] if swing["kind"] == "high" else intraday_history[swing["idx"]]["high"]
        risk = abs(entry - sl)
        tp = entry - risk * rr_ratio
        return {
            "signal": "SHORT",
            "entry": round(entry, 6),
            "sl": round(sl, 6),
            "sl_ref": round(sl, 6),
            "tp": round(tp, 6),
            "tp_alt": round(c2["high"], 6),
            "reason": f"RETTANGOLO_SIMPLE: breakout UP @ idx={idx-1} + swing={swing['type']} @ idx={swing['idx']} + 2 candele DOWN (R:R {rr_ratio})",
            "swing_type": swing["type"],
            "range_top": range_top,
            "range_bot": range_bot,
        }
    else:  # Buy / LONG
        if not (c1["close"] > c1["open"] and c2["close"] > c2["open"]):
            return None
        if not (c2["close"] > range_bot):
            return None
        entry = c2["close"]
        sl = swing["price"] if swing["kind"] == "low" else intraday_history[swing["idx"]]["low"]
        risk = abs(entry - sl)
        tp = entry + risk * rr_ratio
        return {
            "signal": "LONG",
            "entry": round(entry, 6),
            "sl": round(sl, 6),
            "sl_ref": round(sl, 6),
            "tp": round(tp, 6),
            "tp_alt": round(c2["low"], 6),
            "reason": f"RETTANGOLO_SIMPLE: breakout DOWN @ idx={idx-1} + swing={swing['type']} @ idx={swing['idx']} + 2 candele UP (R:R {rr_ratio})",
            "swing_type": swing["type"],
            "range_top": range_top,
            "range_bot": range_bot,
        }
