"""
Trailing Stop Watchdog - Mavis 2026-07-20
Monitora posizioni aperte su Bybit. Quando una posizione raggiunge +1.0% profitto
SUL MARGINE (Charter, non sul prezzo), attiva trailing stop Bybit nativo
(set_trading_stop con trailingStop). Per long: trailing segue massimo - 0.5%.
Per short: trailing segue minimo + 0.5%.

IMPORTANTE: TRIGGER e TRAIL sono in % sul MARGINE (Charter). Per attivare
Bybit li converte in % sul PREZZO usando la leva della posizione.

Parametri Charter:
  TRIGGER_PCT_MARGIN = 1.0 (% profitto MARGINE per attivazione trailing)
  TRAIL_PCT_MARGIN = 0.5 (% distanza trailing da massimo/minimo, MARGINE)
  POLL_SECONDS = 60 (ogni quanto controllare)

Backstory: Charter Mattia "lo attivi dopo che è arrivato a +1.0% o 1.5% non mi
ricordo precisamente" — scelto 1.0% come default conservativo.

=== FIX 2026-07-20 v3.3 (Mattia 17:08) — SAFETY NET Python per trailing Bybit non eseguito ===
PROBLEMA: Bybit trailing stop nativo a volte NON esegue l'ordine di chiusura anche
se il prezzo scende sotto il trailing stop (bug noto su coin meno liquide).
Esempio reale 20/07 17:05: VIRTUALUSDT Buy attivato trailing a +1% margin, prezzo
salito a +2.50% margin (max), poi sceso a -0.46% margin SENZA che Bybit eseguisse
il trailing. Posizione rimasta aperta in mini-loss.
FIX: il watchdog ora tiene traccia del MASSIMO RAGGIUNTO (long) o MINIMO (short)
dopo l'attivazione, e se il prezzo corrente va SOTTO il trailing calcolato, CHIUDE
MANUALMENTE con market order reduceOnly. Safety net ridondante: se Bybit esegue
prima, noi non interveniamo (la posizione non esiste più).
"""
import time
import logging
import sys
import os
from bybit_demo_client import BybitDemoClient
from charter_core.conversions import pct_margin_to_price, price_change_to_margin, format_pct_both

# Trigger e trail in % sul MARGINE (Charter). Conversione in % PREZZO fatta runtime
# per ogni posizione usando la sua leva.
TRIGGER_PCT_MARGIN = 1.0
TRAIL_PCT_MARGIN = 0.5
POLL_SECONDS = 60

LOG_FILE = r"/opt/charter-live/live_deploy/logs/trailing_stop_watchdog.log"
PID_FILE = r"/tmp/trailing_stop_watchdog.pid"

logging.basicConfig(
    filename=LOG_FILE,
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger()
log.info("=== TRAILING STOP WATCHDOG STARTED (Mavis 2026-07-20) ===")
log.info("Charter params: TRIGGER_PCT_MARGIN=%.2f%% TRAIL_PCT_MARGIN=%.2f%% POLL=%ds",
         TRIGGER_PCT_MARGIN, TRAIL_PCT_MARGIN, POLL_SECONDS)
log.info("Conversione %%MARGINE <-> %%PREZZO via charter_core.conversions (leva per posizione)")

# Scrivi PID
with open(PID_FILE, "w") as f:
    f.write(str(os.getpid()))

bybit = BybitDemoClient()


def is_trailing_active(pos: dict) -> bool:
    """Verifica se trailing stop è già attivo su una posizione Bybit."""
    tsl = pos.get("trailingStop")
    return tsl is not None and str(tsl) not in ("0", "0.0", "")


# === FIX v3.3 SAFETY NET: traccia massimo/minimo raggiunto per posizione ===
# Dizionario {(symbol, side): max_or_min_price}. Persiste finche' il processo vive.
# Quando trailing attivo, aggiorniamo ad ogni CHECK. Se current < trailing calcolato,
# CHIUDE MANUALMENTE (safety net per bug Bybit che non esegue trailing nativo).
trailing_peak: dict = {}


def calc_trailing_stop_price(peak: float, side: str, trail_pct_price: float) -> float:
    """Calcola il trailing stop price dato il peak (max per long, min per short)
    e il trail in % sul PREZZO. NOTA: trail_pct_price è in PERCENTUALE (es. 0.1667 = 0.1667%),
    coerente con il parametro Bybit `trailingStop` (es. "0.1666" = 0.1666% prezzo).
    FIX v3.5 Mavis 17:18: bug precedente trattava trail come decimale (peak * 0.833 invece di peak * 0.99833)."""
    if side.lower() in ("buy", "long"):
        return peak * (1 - trail_pct_price / 100.0)
    else:
        return peak * (1 + trail_pct_price / 100.0)


def main_loop():
    while True:
        try:
            positions = bybit.fetch_positions()
            for p in positions:
                sym = p.get("symbol")
                side = p.get("side")
                size = float(p.get("size") or 0)
                if size <= 0:
                    continue
                entry = float(p.get("avgPrice") or 0)
                if entry <= 0:
                    continue
                leverage = float(p.get("leverage") or 1)
                if leverage <= 0:
                    leverage = 1.0
                # Leggi prezzo corrente
                current = float(bybit.get_last_price(sym))
                # Calcola % MARGINE Charter usando helper
                pct_margin = price_change_to_margin(entry, current, leverage, side)
                pct_price = pct_margin_to_price(TRIGGER_PCT_MARGIN, leverage, side)  # solo per log
                tsl_active = is_trailing_active(p)
                key = (sym, side)
                log.info("CHECK %s %s entry=%.6f current=%.6f PnL=%s lev=%.0fx tsl_active=%s peak=%s",
                         sym, side, entry, current, format_pct_both(pct_margin, leverage),
                         leverage, tsl_active, trailing_peak.get(key, "n/a"))
                # === FIX v3.3 SAFETY NET: inizializza peak LAZY per posizioni con trailing GIA' attivo
                # (succede al primo avvio watchdog o se la posizione era gia' aperta con trailing Bybit).
                # Recupero il massimo (long) / minimo (short) raggiunto dalla candela in cui
                # il PnL ha raggiunto il trigger. Se non riesco, fallback peak = current.
                if tsl_active and key not in trailing_peak:
                    try:
                        klines = bybit.fetch_ohlcv(sym, "1", 120)  # ultime 2h candele 1m
                        if klines:
                            peak_init = None
                            for k in klines:
                                h = float(k[2])
                                l = float(k[3])
                                if side.lower() in ("buy", "long"):
                                    test_price = h
                                else:
                                    test_price = l
                                _pnl = price_change_to_margin(entry, test_price, leverage, side)
                                if _pnl >= TRIGGER_PCT_MARGIN:
                                    # Trovata candela di trigger. Peak = max/min da qui in poi.
                                    subsequent = [float(x[2]) for x in klines if float(x[0]) >= k[0]]
                                    if subsequent:
                                        if side.lower() in ("buy", "long"):
                                            peak_init = max(subsequent)
                                        else:
                                            lows = [float(x[3]) for x in klines if float(x[0]) >= k[0]]
                                            peak_init = min(lows) if lows else test_price
                                    break
                            if peak_init is not None:
                                trailing_peak[key] = peak_init
                                log.info("PEAK INIT LAZY %s %s from history: peak=%.6f (recuperato da candele passate)",
                                         sym, side, peak_init)
                            else:
                                # Nessuna candela con PnL >= trigger (strano se tsl_active=True)
                                # Fallback: usa current
                                trailing_peak[key] = current
                                log.warning("PEAK INIT LAZY FALLBACK %s %s: nessuna candela con PnL >= trigger, peak=current=%.6f",
                                            sym, side, current)
                    except Exception as e:
                        # Fallback in caso di errore
                        trailing_peak[key] = current
                        log.warning("PEAK INIT LAZY ERROR %s: %s — fallback peak=current=%.6f", sym, e, current)
                # Attiva trailing se profit MARGINE >= trigger Charter e NON già attivo
                if pct_margin >= TRIGGER_PCT_MARGIN and not tsl_active:
                    # Conversione trail MARGINE → PREZZO per Bybit
                    trail_pct_price = pct_margin_to_price(TRAIL_PCT_MARGIN, leverage)
                    # Inizializza peak con entry per long, o con current se short
                    if side.lower() in ("buy", "long"):
                        trailing_peak[key] = current
                    else:
                        trailing_peak[key] = current
                    log.info("ACTIVATING TRAILING %s %s: trigger=%s trail=%s (price equiv=%.4f%%) NO activePrice (attivo subito al mark %.6f) [v3.3 peak init=%.6f]",
                             sym, side,
                             format_pct_both(TRIGGER_PCT_MARGIN, leverage),
                             format_pct_both(TRAIL_PCT_MARGIN, leverage),
                             trail_pct_price, current, current)
                    try:
                        bybit.set_trading_stop(
                            symbol=sym,
                            trailing_stop=trail_pct_price,
                        )
                        log.info("TRAILING ATTIVATO: %s %s trail_price=%.4f%% dal mark=%.6f",
                                 sym, side, trail_pct_price, current)
                    except Exception as e:
                        log.error("set_trading_stop FAILED for %s: %s", sym, e)
                # === FIX v3.3 SAFETY NET: aggiorna peak e verifica se CHIUDERE manualmente ===
                if tsl_active and key in trailing_peak:
                    # Aggiorna peak (max per long, min per short)
                    if side.lower() in ("buy", "long"):
                        if current > trailing_peak[key]:
                            trailing_peak[key] = current
                            log.info("PEAK UPDATE %s %s long: new peak=%.6f", sym, side, current)
                    else:
                        if current < trailing_peak[key]:
                            trailing_peak[key] = current
                            log.info("PEAK UPDATE %s %s short: new peak=%.6f", sym, side, current)
                    # Calcola trailing stop price atteso
                    trail_pct_price = pct_margin_to_price(TRAIL_PCT_MARGIN, leverage)
                    expected_tsl_price = calc_trailing_stop_price(trailing_peak[key], side, trail_pct_price)
                    # SAFETY NET: se current è SOTTO (long) o SOPRA (short) il trailing calcolato,
                    # Bybit DOVREBBE aver triggerato. Se non l'ha fatto, chiudiamo NOI manualmente.
                    triggered = False
                    if side.lower() in ("buy", "long") and current <= expected_tsl_price:
                        triggered = True
                    elif side.lower() in ("sell", "short") and current >= expected_tsl_price:
                        triggered = True
                    if triggered:
                        log.warning("SAFETY NET v3.3: %s %s current=%.6f ha SUPERATO il trailing calcolato %.6f (peak=%.6f trail=%.4f%%) "
                                    "ma Bybit non ha eseguito. CHIUSURA MANUALE market reduceOnly.",
                                    sym, side, current, expected_tsl_price, trailing_peak[key], trail_pct_price)
                        try:
                            close_side = "Sell" if side.lower() in ("buy", "long") else "Buy"
                            order = bybit._request("POST", "/v5/order/create", {
                                "category": "linear", "symbol": sym, "side": close_side,
                                "orderType": "Market", "qty": str(size),
                                "timeInForce": "GTC", "reduceOnly": True
                            }, signed=True)
                            order_id = (order or {}).get("orderId") or (order or {}).get("result", {}).get("orderId")
                            log.warning("SAFETY NET v3.3 CHIUSA: %s %s qty=%s orderId=%s @ market (Bybit trailing non eseguito)",
                                        sym, close_side, size, order_id)
                            # Rimuovi da peak tracker
                            trailing_peak.pop(key, None)
                        except Exception as e:
                            log.error("SAFETY NET v3.3 CHIUSURA FAILED for %s: %s", sym, e)
                    else:
                        log.info("SAFETY NET v3.3 OK: %s %s current=%.6f NON ha superato trailing=%.6f (peak=%.6f)",
                                 sym, side, current, expected_tsl_price, trailing_peak[key])
        except Exception as e:
            log.error("watchdog iteration error: %s", e)
        time.sleep(POLL_SECONDS)


if __name__ == "__main__":
    try:
        main_loop()
    except KeyboardInterrupt:
        log.info("WATCHDOG STOPPED (KeyboardInterrupt)")
        sys.exit(0)
