"""
Rettangolo Runner - Esecuzione live della strategia RETTANGOLO.

Loop ogni 60s:
  1. Per ogni symbol in rettangolo_assets.csv (strategy=rettangolo)
  2. Calcola compute_signal() (rettangolo_strategy.py)
  3. Se c'è segnale LONG o SHORT:
     - Safety cap MAX_OPEN_POSITIONS (10 globale, già in rettangolo_config)
     - FIX 2026-07-21 Mattia 16:35: ANTI-DOPPIA LOCALE via signal_state.json (cooldown 4h)
     - Se gia' posizione aperta su quel symbol: skip
     - Altrimenti: set leverage 3x, market order, set SL/TP via set_trading_stop
  4. Logga tutto in rettangolo_runner.log

NON tocca file di config (rettangolo_assets.csv, rettangolo_config.py).
NON modifica posizioni esistenti (time-stop lo fa Pine via webhook).
"""
import os
import sys
import time
import json
import requests
from datetime import datetime, timezone
from pathlib import Path
from zoneinfo import ZoneInfo

sys.path.insert(0, str(Path(__file__).parent))

from bybit_demo_client import BybitDemoClient
from rettangolo_strategy import compute_signal
from rettangolo_config import (
    load_assets, ORDER_VALUE_USD, LEVERAGE, MAX_OPEN_POSITIONS, max_qty_for_order_value,
)

# === CONFIG ===
LOOP_INTERVAL_SEC = 60
INTRADAY_LIMIT = 200
DAILY_LIMIT = 5
TIMEFRAME = "120"  # 2H (hardcoded, matches rettangolo_assets.csv)

# FIX 2026-07-21 Mattia 16:35: ANTI-DOPPIA LOCALE
# Bug: rettangolo_runner ha inviato 933+415=1348 webhook ETH/WIF SELL in 15 ore perche
# fetch_positions(sym) ritornava vuoto dopo errore Bybit "ab not enough" (saldo demo finito).
# Workaround: file JSON che traccia ultimo_segnale per symbol+side. Cooldown 4h.
SIGNAL_COOLDOWN_SEC = 14400  # 4 ore
SIGNAL_STATE_FILE = r"G:\AI TRADING ENGINE\live_deploy\logs\rettangolo_signal_state.json"

# === REFACTOR 2026-07-20 (Mavis) — Rettangolo passa per il webhook 5580 ===
WEBHOOK_URL = "http://127.0.0.1:5580/webhook"
WEBHOOK_SECRET = "TV_2026_MATTIA_DEMO"

LOG_FILE = r"G:\AI TRADING ENGINE\live_deploy\webhook_listener\logs\rettangolo_runner.log"
LOCAL_TZ = ZoneInfo("Europe/Rome")


def log(msg):
    ts = datetime.now(timezone.utc).astimezone(LOCAL_TZ).isoformat()
    line = f"[{ts}] {msg}"
    print(line, flush=True)
    try:
        os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
        with open(LOG_FILE, "a", encoding="utf-8") as f:
            f.write(line + "\n")
    except Exception:
        pass


def _load_signal_state():
    try:
        if os.path.exists(SIGNAL_STATE_FILE):
            with open(SIGNAL_STATE_FILE, "r", encoding="utf-8") as f:
                return json.load(f)
    except Exception as e:
        log(f"  load_signal_state err: {e}")
    return {}


def _save_signal_state(state):
    try:
        os.makedirs(os.path.dirname(SIGNAL_STATE_FILE), exist_ok=True)
        with open(SIGNAL_STATE_FILE, "w", encoding="utf-8") as f:
            json.dump(state, f, indent=2)
    except Exception as e:
        log(f"  save_signal_state err: {e}")


def _is_recent_signal(state, symbol, side, now_epoch):
    """Ritorna True se stesso symbol+side ha un segnale nelle ultime SIGNAL_COOLDOWN_SEC."""
    key = f"{symbol}_{side}"
    rec = state.get(key)
    if not rec:
        return False
    elapsed = now_epoch - float(rec.get("ts", 0))
    return elapsed < SIGNAL_COOLDOWN_SEC


def fetch_klines(client, symbol, interval, limit):
    raw = client.fetch_ohlcv(symbol, interval, limit)
    out = []
    for row in raw:
        ts_ms, o, h, l, c = int(row[0]), row[1], row[2], row[3], row[4]
        out.append({
            "ts": ts_ms,
            "date": datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc).astimezone(LOCAL_TZ).isoformat(),
            "open": o, "high": h, "low": l, "close": c,
        })
    return out


def set_leverage_safe(client, symbol, leverage=3):
    try:
        r = client.set_leverage(symbol, leverage)
        return True, f"retCode={r.get('retCode')}"
    except Exception as e:
        msg = str(e)
        if "110043" in msg:
            return True, "leverage already at target (skip)"
        return False, f"err: {e}"


def count_open_positions(client):
    try:
        positions = client.fetch_positions()
        return sum(1 for p in positions if float(p.get("size", 0) or 0) > 0)
    except Exception:
        return -1


def get_ticker_price(client, symbol):
    try:
        d = client._request("GET", "/v5/market/tickers",
                             {"category": "linear", "symbol": symbol}, signed=False)
        lst = d.get("result", {}).get("list", [])
        if lst:
            return float(lst[0].get("lastPrice", 0) or 0)
    except Exception:
        pass
    return 0.0


def run_once(client, rettangolo_assets):
    """Processa tutti gli asset rettangolo. Ritorna numero di trade aperti in questo giro."""
    opened = 0
    signal_state = _load_signal_state()
    state_dirty = False
    now_epoch = time.time()

    for asset in rettangolo_assets:
        sym = asset["symbol"]
        tf_min = asset.get("tf_min", "120")
        try:
            tf_int = int(tf_min)
        except (TypeError, ValueError):
            tf_int = 120

        # 1. fetch candele
        try:
            daily = fetch_klines(client, sym, "D", DAILY_LIMIT)
            intraday = fetch_klines(client, sym, str(tf_int), INTRADAY_LIMIT)
        except Exception as e:
            log(f"  {sym}: fetch klines err: {e}")
            continue

        if len(daily) < 2 or len(intraday) < 2:
            log(f"  {sym}: klines insufficienti (daily={len(daily)}, intra={len(intraday)})")
            continue

        prev_daily = daily[-2]

        if len(intraday) < 3:
            log(f"  {sym}: klines insufficienti per valutare candela chiusa (intra={len(inttraday)})")
            continue
        idx_closed = len(intraday) - 2
        pattern_mode = asset.get("pattern_mode", "relaxed")
        allow_martello_spinta = (pattern_mode == "relaxed")
        log(f"  {sym}: evaluating CLOSED candle idx={idx_closed} pattern_mode={pattern_mode}")
        sig = compute_signal(prev_daily, intraday, idx_closed,
                             sl_buffer_pct=0.01,
                             require_two_candles=True,
                             allow_martello_spinta=allow_martello_spinta)
        if sig is None:
            continue

        bybit_side = "Buy" if sig["signal"] == "LONG" else "Sell"
        log(f"  {sym}: SEGNALE {sig['signal']} entry={sig['entry']:.4f} sl={sig['sl']:.4f} tp={sig['tp']:.4f}")

        # 2. FIX 2026-07-21 16:35 Mattia — ANTI-DOPPIA LOCALE
        # Se stesso symbol+side ha un segnale nelle ultime SIGNAL_COOLDOWN_SEC (4h), SKIP.
        # Workaround per fetch_positions che ritorna vuoto dopo errore Bybit "ab not enough".
        if _is_recent_signal(signal_state, sym, bybit_side, now_epoch):
            key = f"{sym}_{bybit_side}"
            elapsed_h = (now_epoch - float(signal_state[key].get("ts", 0))) / 3600
            log(f"    ANTI-DOPPIA LOCALE: {sym} {bybit_side} ultimo segnale {elapsed_h:.1f}h fa (cooldown {SIGNAL_COOLDOWN_SEC/3600:.0f}h), skip")
            continue

        # 3. safety cap
        n_open = count_open_positions(client)
        if n_open < 0:
            log(f"    err count positions, skip {sym}")
            continue
        if n_open >= MAX_OPEN_POSITIONS:
            log(f"    safety cap {n_open}/{MAX_OPEN_POSITIONS}, skip {sym}")
            continue

        # 4. verifica posizione gia' aperta su questo symbol
        try:
            existing = client.fetch_positions(sym)
            if existing:
                log(f"    posizione gia' aperta su {sym} ({existing[0]['side']} size={existing[0]['size']}), skip")
                continue
        except Exception as e:
            log(f"    err fetch position {sym}: {e}, skip")
            continue

        # 5. set leverage 3x
        ok, msg = set_leverage_safe(client, sym, LEVERAGE)
        if not ok:
            log(f"    set_leverage FAIL: {msg}, skip")
            continue
        log(f"    leverage: {msg}")

        # 6. calcola current price solo per logging (non piu' usato come prezzo ordine)
        current_price = get_ticker_price(client, sym)
        if current_price <= 0:
            log(f"    ticker fail per {sym}, skip")
            continue
        log(f"    current_price: {current_price} (solo log, ordine usa entry teorica {sig['entry']:.4f})")

        # 7. apri posizione VIA WEBHOOK
        try:
            payload = {
                "secret": WEBHOOK_SECRET,
                "symbol": sym,
                "side": bybit_side,
                "qty": 1,
                "price": float(sig["entry"]),  # FIX 1 (22/07): usa entry teorica, non current_price
                "strategy": "rettangolo",
                "leverage": LEVERAGE,
                "timeframe": asset.get("timeframe", "120"),
                "comment": f"rettangolo auto {sig['signal']}",
                "sl_price": sig.get("sl"),     # FIX 5 (22/07): era sl_ref, ora sl_price (nome atteso dal webhook)
                "tp_price": sig.get("tp"),     # FIX 5 (22/07): era tp, ora tp_price
            }
            r = requests.post(WEBHOOK_URL, json=payload, timeout=10)
            try:
                result = r.json()
            except Exception:
                result = {}
            if not (r.status_code == 200 and result.get("ok")):
                err_msg = (result.get("error") or r.text or "")[:200]
                log(f"    WEBHOOK FAIL: status={r.status_code} err={err_msg}, skip")
                continue
            req_id = (result.get("request_id") or "?")[:8]
            log(f"    WEBHOOK OK: request_id={req_id}... queued for {bybit_side} {sym}")

            # FIX 6 (22/07): polling per conferma reale fill (no solo queued).
            # Il webhook risponde "queued" immediatamente perche' il worker processa in background.
            # Senza polling, il runner segna l'apertura come riuscita anche se l'ordine verra' bloccato
            # da CHOP filter / anti-doppia / errore Bybit.
            confirmed = False
            status_url = f"{WEBHOOK_URL.replace('/webhook', '')}/webhook/status/{req_id}"
            for attempt in range(15):  # max 15s di attesa (15 * 1s)
                time.sleep(1)
                try:
                    sr = requests.get(status_url, timeout=5)
                    sresult = sr.json() if sr.status_code == 200 else {}
                except Exception:
                    sresult = {}
                st = sresult.get("status")
                if st == "completed":
                    order_id = sresult.get("order_id", "?")
                    log(f"    WEBHOOK FILL CONFERMATO: orderId={order_id} after {attempt+1}s")
                    confirmed = True
                    break
                if st == "failed":
                    err = sresult.get("error", "")[:200]
                    log(f"    WEBHOOK FAILED dopo {attempt+1}s: {err}, skip anti-doppia")
                    # Non aggiungere anti-doppia per ordine fallito, ripristina segnale precedente se esiste
                    confirmed = False
                    break
            if not confirmed:
                log(f"    WEBHOOK TIMEOUT o FAILED: req_id={req_id}, NON segno anti-doppia, NON opened")
                continue

            # Aggiorna stato segnale (anti-doppia locale) SOLO per ordine realmente fillato
            signal_state[f"{sym}_{bybit_side}"] = {
                "side": bybit_side,
                "ts": now_epoch,
                "entry": float(sig["entry"]),
                "request_id": req_id,
            }
            state_dirty = True
            opened += 1
        except Exception as e:
            log(f"    webhook exception: {e}, skip")
            continue

    # Salva stato segnali se modificato
    if state_dirty:
        _save_signal_state(signal_state)
        log(f"  signal_state saved: {len(signal_state)} entries")

    return opened


def main_loop():
    client = BybitDemoClient()
    log(f"=== RETTANGOLO RUNNER AVVIATO (loop {LOOP_INTERVAL_SEC}s, MANIFESTO nozionale={ORDER_VALUE_USD}, leva={LEVERAGE}, cap={MAX_OPEN_POSITIONS}, ANTI_DUP_COOLDOWN={SIGNAL_COOLDOWN_SEC}s) ===")
    while True:
        try:
            assets = [a for a in load_assets() if a.get("strategy", "rettangolo") == "rettangolo" and a.get("enabled", True)]
            if not assets:
                log("nessun asset rettangolo enabled nel CSV, skip")
            else:
                log(f"processo {len(assets)} asset rettangolo: {','.join(a['symbol'] for a in assets)}")
                run_once(client, assets)
        except Exception as e:
            log(f"loop exception: {e}")
        time.sleep(LOOP_INTERVAL_SEC)


if __name__ == "__main__":
    if "--once" in sys.argv:
        client = BybitDemoClient()
        assets = [a for a in load_assets() if a.get("strategy", "rettangolo") == "rettangolo" and a.get("enabled", True)]
        log(f"=== RETTANGOLO RUNNER --once ({len(assets)} asset) ===")
        run_once(client, assets)
    else:
        main_loop()
