"""
paletti
=======

I "paletti" di Mattia: l'elenco delle regole business hard del Charter
che il bot DEVE rispettare in ogni situazione. Ogni paletto e' una
condizione che, se violata, indica un bug o un comportamento anomalo.

Convenzioni:
  - Ogni paletto ha: id, category, description, hard_rule, examples
  - "hard" significa che non c'e' eccezione: se violato, il bot deve
    abortire o rifiutare l'operazione (NO fallback, NO retry, NO override)
  - Le violazioni sono incidenti di business (NON bug di codice) e vanno
    loggate in modo speciale nel verifier

Lista paletti consolidata (fonte: charter_bot_report.html sez. 1-15 +
memory Mattia 2026-07-04):
  - No media-up (no re-entry se posizione gia' aperta)
  - No double-buy su stesso symbol
  - Fee Pine IMPLICITA = 0.06% per lato (mai 0.1% Bybit std)
  - Leva Charter = 3x per TUTTI gli asset (mai simbolo-default)
  - TP 50/50 split (TP1 50% +3%, TP2 50% +5%)
  - SL ATR 2x clamp -3% (mai meno, mai piu')
  - Regime filter EMA50+ADX>20 obbligatorio
  - Max 5 posizioni aperte (deroga 12/07 Mattia)
  - Anti-doppia candela (state.last_signal_bar check)
  - Candela [-2] SEMPRE (mai [-1] in formazione)
  - set_leverage PRIMA di create_market_order
  - Charter primary = AI Enhancer NON riduce size
  - SL nativo Bybit V5 (mark price trigger, sopravvive se bot offline)
  - no upfront leverage override (Bypass solo se gia' 3x)
"""

from enum import Enum
from typing import Optional


class PalettiCategory(str, Enum):
    """Categoria del paletto (per organizzazione e report)."""

    POSITION_SIZING = "position_sizing"
    RISK_MANAGEMENT = "risk_management"
    SIGNAL_GENERATION = "signal_generation"
    EXECUTION = "execution"
    STATE_MANAGEMENT = "state_management"
    COMPLIANCE = "compliance"


# Ogni paletto e' un dict con:
#   id: str univoco
#   category: PalettiCategory
#   description: spiegazione human-readable
#   hard_rule: funzione di validazione (None = solo documentale)
PALETTI = [
    {
        "id": "P001-NO_MEDIA_UP",
        "category": PalettiCategory.POSITION_SIZING,
        "description": (
            "No media-up: se esiste GIA' una posizione aperta su Bybit "
            "per il simbolo, NON aprire una nuova entry. NON toccare i "
            "TP/SL esistenti (Mattia potrebbe averli messi a mano). "
            "Bot in MONITOR mode read-only su quella posizione."
        ),
        "file_ref": "live_engine.py:391-402",
        "bug_origin": "BUG post 12/07 (no double-buy hard rule)",
    },
    {
        "id": "P002-NO_DOUBLE_BUY",
        "category": PalettiCategory.POSITION_SIZING,
        "description": (
            "No double-buy: non possono esistere 2 entry live attive "
            "sullo stesso simbolo via il bot. Anti-doppia candela + "
            "check Bybit pre-exec obbligatori."
        ),
        "file_ref": "live_engine.py:414-424",
    },
    {
        "id": "P003-FEE_PINE_IMPLICITA",
        "category": PalettiCategory.RISK_MANAGEMENT,
        "description": (
            "Fee Pine IMPLICITA = 0.06% per lato (0.0006 in decimale). "
            "NON usare 0.1% Bybit standard, NON 0.075% taker ridotto. "
            "Fonte: scoperta BUG_003 chiusura vera 16:20 (gap PF 1.4% "
            "replica Pine fedele)."
        ),
        "file_ref": "web_solver_v6.py:34 (commission_pct=0.0006)",
        "bug_origin": "BUG_003 (Pine<>Solver replica gap)",
    },
    {
        "id": "P004-LEVA_3X_CHARTER",
        "category": PalettiCategory.POSITION_SIZING,
        "description": (
            "Leva Charter = 3x per TUTTI gli asset, sempre. Mai usare "
            "la leva di default del simbolo (es. ZEC default 3x OK, "
            "ma AERO default 1x VIOLA Charter). set_leverage(3) PRIMA "
            "di create_market_order, con bypass solo se gia' a 3x."
        ),
        "file_ref": "live_engine.py:297 + bybit_demo_client.py set_leverage bypass",
        "bug_origin": "BUG_004 (ordine senza leva)",
    },
    {
        "id": "P005-TP_50_50",
        "category": PalettiCategory.RISK_MANAGEMENT,
        "description": (
            "TP 50/50 split: TP1 a +3% (50% size), TP2 a +5% (50% size). "
            "Safety: se divisione perde troppo (qty=1, step=1), fallback "
            "su TP singolo a TP1 (qty_tp2=0). Mai TP singolo a TP2."
        ),
        "file_ref": "live_engine.py:237-337 (place_entry_with_tpsl)",
    },
    {
        "id": "P006-SL_ATR_2X_CLAMP",
        "category": PalettiCategory.RISK_MANAGEMENT,
        "description": (
            "SL ATR 2x clampato a -3% (sl_clamp_min == sl_clamp_max == -0.03). "
            "Formula: sl_pct = max(-sl_atr_mult * atr / entry_price, sl_clamp_min), "
            "poi min(sl_pct, sl_clamp_max). Hard: mai meno del -3%, mai piu' del -3%."
        ),
        "file_ref": "live_engine.py:237-280 + SETUPS Charter sl_clamp",
    },
    {
        "id": "P007-REGIME_FILTER_EMA50_ADX",
        "category": PalettiCategory.SIGNAL_GENERATION,
        "description": (
            "Regime filter Pine-faithful obbligatorio: ADX > 20 AND "
            "(LONG richiede close[-2] > EMA50[-2]) AND (SHORT richiede "
            "close[-2] < EMA50[-2]). SKIP-REGIME se fallisce. "
            "Rivalidazione pre-exec al CHECK 5 (riga 427)."
        ),
        "file_ref": "live_engine.py:215-233 + 406 + 427",
    },
    {
        "id": "P008-MAX_5_POSITIONS",
        "category": PalettiCategory.POSITION_SIZING,
        "description": (
            "MAX_OPEN_TRADES_PORTFOLIO = 5 (deroga Mattia 12/07, regola "
            "base era 3). Oltre = SKIP-MAX-OPEN. Charter primary hanno "
            "priorita', secondary sono skippate prima."
        ),
        "file_ref": "live_engine.py MAX_OPEN_TRADES_PORTFOLIO = 5",
        "bug_origin": "Deroga Mattia 2026-07-12 10:20",
    },
    {
        "id": "P009-ANTI_DOPPIA_CANDELA",
        "category": PalettiCategory.STATE_MANAGEMENT,
        "description": (
            "Anti-doppia candela: prima di processare un setup, confronta "
            "state.last_signal_bar[setup_name] con timestamp candela [-2]. "
            "Se uguale -> SKIP-DUP (gia' processata, no nuova entry)."
        ),
        "file_ref": "live_engine.py:354-359",
    },
    {
        "id": "P010-CANDELA_-2",
        "category": PalettiCategory.SIGNAL_GENERATION,
        "description": (
            "Segnale sempre su candela [-2] (chiusa), MAI [-1] (in "
            "formazione). Pine-faithful, evita look-ahead bias."
        ),
        "file_ref": "live_engine.py:151-189 compute_indicators + detect_signal",
    },
    {
        "id": "P011-SET_LEVERAGE_PRIMA",
        "category": PalettiCategory.EXECUTION,
        "description": (
            "set_leverage(symbol, leverage, side) SEMPRE PRIMA di "
            "create_market_order. Charter target 3x. Bypass solo se "
            "gia' a 3x (ritorna {retCode: 0, retMsg: 'bypassed ...'}). "
            "Se fallisce -> NON procedere con entry."
        ),
        "file_ref": "live_engine.py:297 (ordine set_leverage prima di create_market_order)",
        "bug_origin": "BUG_004 fix",
    },
    {
        "id": "P012-CHARTER_PRIMARY_NO_AI_REDUCE",
        "category": PalettiCategory.POSITION_SIZING,
        "description": (
            "Setup Charter primary (is_primary=True): AI Signal Enhancer "
            "NON viene chiamato e NON riduce size. Size sempre piena "
            "(500/250/250 USDT). Solo secondary hanno MIL dynamic sizing "
            "+ AI verdict (GO/CAUTELA 50%/SKIP)."
        ),
        "file_ref": "live_engine.py:433-435 + 447",
    },
    {
        "id": "P013-SL_NATIVO_BYBIT",
        "category": PalettiCategory.EXECUTION,
        "description": (
            "SL nativo Bybit V5 via set_trading_stop (mark price trigger), "
            "eseguito dall'exchange anche se bot offline. Mai SL Python "
            "locale (sparisce se bot muore). Fix parziale BUG_005 in "
            "bybit_demo_client.py:129-159. Fix completo pending: usare "
            "set_trading_stop anche per primo TP invece di limit order."
        ),
        "file_ref": "bybit_demo_client.py:129-159 set_trading_stop",
        "bug_origin": "BUG_005 (Charter TP/SL non Bybit V5 nativi, OPEN 18:26)",
    },
    {
        "id": "P014-NO_FRONT_LEVERAGE_OVERRIDE",
        "category": PalettiCategory.EXECUTION,
        "description": (
            "set_leverage bypass: solo se l'asset e' GIA' a 3x su Bybit. "
            "Bybit ritorna errore 110043 'leverage not modified' se bypass "
            "e' False e leva e' gia' 3x (gestito come OK = non procedere). "
            "Mai sovrascrivere leva 3x con un altro valore runtime."
        ),
        "file_ref": "bybit_demo_client.py set_leverage con bypass flag",
    },
]


def total_paletti_count() -> int:
    """Ritorna il numero totale di paletti documentati."""
    return len(PALETTI)


def is_paletto_violated(paletto_id: str, context: dict) -> bool:
    """
    Stub di validazione paletto. NB: la logica di validazione vera
    vive nei sub-repo (live_engine, bybit_demo_client) perche' dipende
    dallo state runtime. Questa funzione ritorna sempre False
    (placeholder) e serve solo come hook per tooling futuro.

    Args:
        paletto_id: ID paletto (es. 'P001-NO_MEDIA_UP')
        context: dict con chiavi dipendenti dal paletto

    Returns:
        True se il paletto e' violato, False altrimenti.
    """
    # Placeholder: la validazione reale e' demandata ai sub-repo per
    # evitare dipendenze circolari charter_core <-> live_engine
    return False


def format_paletto_report() -> str:
    """Ritorna un report markdown con l'elenco completo dei paletti."""
    lines = ["# Paletti Charter - Report\n"]
    lines.append(f"Totale paletti: **{total_paletti_count()}**\n")
    lines.append("\n## Per categoria\n")
    by_cat: dict = {}
    for p in PALETTI:
        by_cat.setdefault(p["category"].value, []).append(p)
    for cat, items in sorted(by_cat.items()):
        lines.append(f"\n### {cat} ({len(items)} paletti)\n")
        for p in items:
            lines.append(f"- **{p['id']}**: {p['description'][:80]}...")
            if "file_ref" in p:
                lines.append(f"  - File: `{p['file_ref']}`")
            if "bug_origin" in p:
                lines.append(f"  - Origine: {p['bug_origin']}")
    return "\n".join(lines)
