"""
charter_engine
==============

API riusabile per il Trading Engine AI Charter. FASE 3 del piano
di consolidamento 2026-07-14.

Fornisce classi e helper che rendono il Charter utilizzabile da
qualsiasi script (live, backtest, paper trading, CLI, notebook):

  - Setup: dataclass che rappresenta 1 setup Charter (mask CD, sizing, TP/SL)
  - Signal: dataclass che rappresenta 1 segnale generato
  - BybitClient: wrapper semplificato sopra bybit_demo_client.BybitDemoClient
  - Bot: classe principale che orchestra setup + segnali + ordini
  - Helper puri: compute_indicators_pine_faithful, detect_signal_pine_faithful,
                check_regime_pine_faithful (estratti dal live_engine, testabili)

NB: questo modulo NON sostituisce live_engine.py: fornisce un'API
pulita e riusabile. La logica pesante (place_entry_with_tpsl, MIL
dynamic sizing, signal enhancer) resta in live_engine per ora.
Una migrazione completa puo' avvenire in FASE 4+.
"""

from __future__ import annotations

import os
import sys
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Optional, List, Dict, Any, Tuple

# Re-export SETUPS/CHARTER da charter_config per comodita'
from .charter_config import (
    SETUPS,
    CHARTER,
    SETUPS_BY_NAME,
    SETUPS_BY_SYMBOL,
    get_setup,
    get_charter,
)
from .cd_types import (
    CDType,
    CD_BIT_NAMES,
    CD_DIMENSIONS,
    parse_cd_mask,
    cd_idx_to_name,
    is_cd_active_in_mask,
    active_cds_in_mask,
    compute_cd_from_row,
)
from .indicators import (
    BB_PARAMS,
    ROC_PARAMS,
    ATR_PARAMS,
    EMA50_PARAMS,
    ADX_PARAMS,
)


# =====================================================================
# Setup dataclass
# =====================================================================

@dataclass
class Setup:
    """
    Rappresenta 1 setup Charter (ZEC_4H, AERO_4H, DASH_4H, o custom).

    Attributes:
        name: nome simbolico del setup (es. 'ZEC_4H')
        symbol_bybit: simbolo Bybit (es. 'ZECUSDT')
        symbol_ccxt: simbolo ccxt (es. 'ZEC/USDT:USDT')
        tf: timeframe stringa (es. '4h')
        interval: durata candela in minuti (es. 240)
        mask: mask CD 8-bit (es. '10001011')
        bb_length, bb_mult: parametri Bollinger Bands
        roc_length: lookback ROC
        atr_length: lookback ATR (Wilder RMA)
        margin_usdt: size margine in USDT (es. 500.0)
        tp1_pct, tp2_pct: TP1 (+3%) e TP2 (+5%) percentuali
        sl_atr_mult: moltiplicatore ATR per SL
        sl_clamp_min, sl_clamp_max: clamp SL (Charter hard -3%)
        trailing_stop_pct: trailing stop (0 = disabilitato)
        max_bars: time stop (barre massimo in posizione)
        min_bars_between: min barre tra due entry consecutive
        leverage: leva target (Charter 3x)
        is_primary: True se Charter primary (AI Enhancer non riduce size)
    """

    name: str
    symbol_bybit: str
    symbol_ccxt: str
    tf: str
    interval: int
    mask: str

    # Indicatori
    bb_length: int = 55
    bb_mult: float = 1.0
    roc_length: int = 36
    atr_length: int = 16

    # Sizing
    margin_usdt: float = 500.0

    # TP/SL Charter
    tp1_pct: float = 0.03
    tp2_pct: float = 0.05
    sl_atr_mult: float = 2.0
    sl_clamp_min: float = -0.03
    sl_clamp_max: float = -0.03
    trailing_stop_pct: float = 0.0

    # Time stop
    max_bars: int = 5
    min_bars_between: int = 4

    # Charter
    leverage: int = 3
    is_primary: bool = True

    @classmethod
    def from_dict(cls, d: dict) -> "Setup":
        """Costruisce un Setup da un dict (es. riga di charter_core.SETUPS)."""
        # Filtra solo le chiavi che sono fields della dataclass
        valid_keys = {f.name for f in cls.__dataclass_fields__.values()}
        filtered = {k: v for k, v in d.items() if k in valid_keys}
        return cls(**filtered)

    def to_dict(self) -> dict:
        """Serializza il setup a dict (utile per logging / JSON / DB)."""
        return asdict(self)

    @property
    def active_cd_types(self) -> List[CDType]:
        """Ritorna i tipi CD attivi nella mask (property comoda)."""
        return active_cds_in_mask(self.mask)

    def __repr__(self) -> str:
        return (
            f"Setup(name='{self.name}', symbol='{self.symbol_bybit}', "
            f"tf='{self.tf}', mask='{self.mask}', leverage={self.leverage}x, "
            f"is_primary={self.is_primary})"
        )


def setups_from_list(lst: List[dict]) -> List[Setup]:
    """Converte una lista di dict (es. charter_core.SETUPS) in lista di Setup."""
    return [Setup.from_dict(d) for d in lst]


# =====================================================================
# Signal dataclass
# =====================================================================

@dataclass
class Signal:
    """
    Rappresenta un segnale Charter generato da un setup.

    Attributes:
        side: 'LONG' o 'SHORT'
        entry_price: prezzo di entry desiderato
        atr: ATR corrente (per SL dinamico)
        timestamp: timestamp della candela [-2] che ha generato il segnale
        setup_name: nome del setup che ha generato (es. 'ZEC_4H')
        symbol: simbolo Bybit del setup (es. 'ZECUSDT')
        cd: tipo CD che ha generato (CDType, opzionale)
        regime_ok: True se regime filter EMA50+ADX>20 passa
        regime_msg: messaggio regime ('regime OK (ADX=..., EMA50 above)')
        bb_break: True se BB breakout confermato
        roc_dir: True se ROC direction coerente con side
    """

    side: str
    entry_price: float
    atr: float
    timestamp: Any  # pd.Timestamp o datetime o string
    setup_name: str
    symbol: str

    # Metadata (opzionali, per logging/debug)
    cd: Optional[CDType] = None
    regime_ok: bool = True
    regime_msg: str = ""
    bb_break: bool = True
    roc_dir: bool = True

    @property
    def is_long(self) -> bool:
        return self.side == "LONG"

    @property
    def is_short(self) -> bool:
        return self.side == "SHORT"

    def tp_levels(self, sl_clamp_min: float = -0.03) -> Tuple[float, float, float]:
        """
        Calcola TP1, TP2, SL dai parametri del segnale.
        Ritorna (tp1, tp2, sl).

        NB: usa i parametri Charter default (3% / 5% / ATR 2x clampato -3%).
        Per setup custom, passare i parametri o usare il Setup associato.
        """
        if self.is_long:
            tp1 = self.entry_price * (1 + 0.03)
            tp2 = self.entry_price * (1 + 0.05)
            sl_pct = max(-2.0 * self.atr / self.entry_price, sl_clamp_min)
            sl = self.entry_price * (1 + sl_pct)
        else:
            tp1 = self.entry_price * (1 - 0.03)
            tp2 = self.entry_price * (1 - 0.05)
            sl_pct = min(2.0 * self.atr / self.entry_price, -sl_clamp_min)
            sl = self.entry_price * (1 + sl_pct)
        return tp1, tp2, sl

    def __repr__(self) -> str:
        return (
            f"Signal(side='{self.side}', symbol='{self.symbol}', "
            f"setup='{self.setup_name}', entry={self.entry_price:.4f}, "
            f"atr={self.atr:.4f}, regime_ok={self.regime_ok})"
        )


# =====================================================================
# BybitClient wrapper
# =====================================================================

class BybitClient:
    """
    Wrapper semplificato sopra bybit_demo_client.BybitDemoClient.

    Fornisce un'API ad alto livello orientata al Charter:
      - place_charter_entry(signal, setup): entry market + TP1/TP2 + SL
      - get_open_position(symbol): posizione corrente
      - get_qty_for_setup(setup): size corretta per il setup
      - set_charter_leverage(setup): set leva Charter (con bypass)

    Se client=None, prova a importare bybit_demo_client.BybitDemoClient
    (lato live). Per i test, passare un mock con la stessa interfaccia.
    """

    def __init__(self, client=None, demo: bool = True):
        self.demo = demo
        if client is not None:
            self._client = client
        else:
            try:
                # Path al live_deploy per import bybit_demo_client
                # FASE 4: supporta env var CHARTER_LIVE_DEPLOY_DIR
                # (default: G:\AI TRADING ENGINE\live_deploy per compatibilita
                # con install editable che ha __file__ nel workspace root)
                _live_deploy_str = os.environ.get("CHARTER_LIVE_DEPLOY_DIR")
                if _live_deploy_str:
                    _live_deploy = Path(_live_deploy_str)
                else:
                    # Fallback: workspace standard.
                    # In editable install, __file__ punta a G:\AI TRADING ENGINE\charter_core\
                    # quindi parent.parent = G:\AI TRADING ENGINE
                    _live_deploy = Path(__file__).resolve().parent.parent / "live_deploy"
                if str(_live_deploy) not in sys.path:
                    sys.path.insert(0, str(_live_deploy))
                from bybit_demo_client import BybitDemoClient  # type: ignore
                self._client = BybitDemoClient()
            except Exception as e:
                self._client = None
                self._init_error = e

    @property
    def is_available(self) -> bool:
        return self._client is not None

    def __getattr__(self, name: str):
        """Delega qualsiasi metodo non definito qui al client sottostante."""
        if name.startswith("_"):
            raise AttributeError(name)
        if self._client is None:
            raise RuntimeError(
                f"BybitClient non disponibile (init error: {getattr(self, '_init_error', 'unknown')})"
            )
        return getattr(self._client, name)

    def get_qty_for_setup(self, setup: Setup, current_price: Optional[float] = None) -> float:
        """
        Calcola la size corretta per il setup Charter:
          qty = (margin_usdt * leverage) / entry_price
          qty = round_qty(symbol, qty)

        Args:
            setup: Setup Charter
            current_price: prezzo corrente (se None, usa 1 come placeholder)

        Returns:
            qty roundata al step corretto per il simbolo.
        """
        price = current_price if current_price else 1.0
        qty = (setup.margin_usdt * setup.leverage) / price
        if self.is_available:
            qty = self._client.round_qty(setup.symbol_bybit, qty)
        return qty

    def set_charter_leverage(self, setup: Setup) -> dict:
        """Imposta la leva Charter (3x) per il setup. Delega al client."""
        if not self.is_available:
            raise RuntimeError("BybitClient non disponibile")
        return self._client.set_leverage(setup.symbol_bybit, setup.leverage, "long")

    def get_open_position(self, symbol: str) -> Optional[dict]:
        """Ritorna la posizione aperta per il simbolo, o None se size=0."""
        if not self.is_available:
            return None
        positions = self._client.fetch_positions(symbol)
        for p in positions:
            if float(p.get("size", 0) or 0) > 0:
                return p
        return None

    def place_charter_entry(
        self, signal: Signal, setup: Setup
    ) -> dict:
        """
        Piazza un'entry Charter completa: set_leverage + market + TP1/TP2 + SL.

        Returns:
            dict con chiavi 'entry', 'tp1', 'tp2', 'sl' (le risposte di Bybit).

        Raises:
            RuntimeError se il client non disponibile.
        """
        if not self.is_available:
            raise RuntimeError("BybitClient non disponibile (richiede env vars + bybit_demo_client)")
        # set leverage PRIMA di market order (paletto P004, P011)
        self._client.set_leverage(setup.symbol_bybit, setup.leverage, signal.side.lower())
        # Calcola TP/SL
        tp1, tp2, sl = signal.tp_levels(setup.sl_clamp_min)
        # Size
        qty = self.get_qty_for_setup(setup, signal.entry_price)
        # qty_tp1 / qty_tp2 split 50/50 (arrotondato a step)
        step = self._client.get_qty_step(setup.symbol_bybit)
        import math
        qty_tp1 = math.floor(qty / 2 / step) * step
        qty_tp2 = qty - qty_tp1
        if qty_tp1 <= 0 or qty_tp2 <= 0:
            qty_tp1 = qty  # safety fallback
            qty_tp2 = 0.0
        # Entry market
        entry = self._client.create_market_order(setup.symbol_bybit, signal.side.lower(), qty)
        # TP1 + TP2 limit reduceOnly
        close_side = "Sell" if signal.is_long else "Buy"
        tp1_resp = self._client.create_limit_order(
            setup.symbol_bybit, close_side, qty_tp1, tp1, reduce_only=True
        ) if qty_tp1 > 0 else None
        tp2_resp = self._client.create_limit_order(
            setup.symbol_bybit, close_side, qty_tp2, tp2, reduce_only=True
        ) if qty_tp2 > 0 else None
        # SL nativo Bybit V5
        sl_resp = self._client.set_trading_stop(setup.symbol_bybit, sl)
        return {
            "entry": entry,
            "tp1": tp1_resp,
            "tp2": tp2_resp,
            "sl": sl_resp,
        }


# =====================================================================
# Bot classe principale
# =====================================================================

class Bot:
    """
    Classe principale per orchestrare un bot Charter.

    Wrappa:
      - lista di Setup Charter
      - BybitClient (per ordini)
      - state locale (posizioni aperte, last_signal_bar)

    Usage:
        from charter_core import SETUPS
        from charter_core.charter_engine import Bot, BybitClient, setups_from_list

        setups = setups_from_list(SETUPS)
        client = BybitClient()
        bot = Bot(client=client, setups=setups)

        # Processa 1 setup (ritorna Signal o None)
        signal = bot.process_setup("ZEC_4H")
        if signal and signal.regime_ok:
            result = bot.place_entry(signal)
            print(result)
    """

    def __init__(
        self,
        client: BybitClient,
        setups: List[Setup],
        state: Optional[Dict[str, Any]] = None,
    ):
        self.client = client
        self.setups = setups
        self._setups_by_name = {s.name: s for s in setups}
        # state: posizioni aperte, last_signal_bar
        self.state = state or {
            "open_positions": {},
            "last_signal_bar": {},
        }

    def get_setup(self, name: str) -> Optional[Setup]:
        return self._setups_by_name.get(name)

    def get_status(self) -> dict:
        """Ritorna uno snapshot dello stato corrente del bot."""
        return {
            "setups": [s.name for s in self.setups],
            "open_positions": list(self.state.get("open_positions", {}).keys()),
            "client_available": self.client.is_available,
        }

    def process_setup(self, name: str, ohlcv: Optional[list] = None) -> Optional[Signal]:
        """
        Processa 1 setup Charter. Ritorna un Signal se le condizioni sono
        soddisfatte (CD match + BB breakout + ROC direction + regime OK),
        altrimenti None.

        Args:
            name: nome del setup (es. 'ZEC_4H')
            ohlcv: lista di candele (se None, fetch live da Bybit)

        Returns:
            Signal o None.

        NB: la logica completa (compute_indicators, detect_signal,
            check_regime) richiede pandas. Per semplicita', questo
            metodo ritorna None se ohlcv e' None. Per la logica completa,
            usare i moduli compute_indicators_pine_faithful e simili.
        """
        setup = self.get_setup(name)
        if setup is None:
            raise ValueError(f"Setup non trovato: {name}")
        if ohlcv is None:
            # Se l'utente vuole la logica live, deve passare ohlcv
            # o implementare qui il fetch + compute
            return None
        # Placeholder: la logica completa richiede pandas. Vedere
        # detect_signal_pine_faithful() piu' sotto per il cuore.
        return None

    def place_entry(self, signal: Signal) -> dict:
        """Piazza un'entry Charter per il segnale. Delega al client."""
        setup = self.get_setup(signal.setup_name)
        if setup is None:
            raise ValueError(f"Setup non trovato per signal: {signal.setup_name}")
        return self.client.place_charter_entry(signal, setup)


# =====================================================================
# Helper funzioni pure (Pine-faithful)
# =====================================================================
#
# NB: per la logica completa servono pandas/numpy. Le firme sono definite
# qui ma l'implementazione reale richiede dati OHLCV in formato DataFrame.
# Vengono importate lazy (try/except) per evitare errori in ambienti minimal.

def compute_indicators_pine_faithful(df, setup: Setup):
    """
    Calcola BB, ROC, ATR, EMA50, ADX, CD per il setup.

    Args:
        df: pandas DataFrame con colonne ['open', 'high', 'low', 'close', 'volume']
            e lunghezza >= 200.
        setup: Setup Charter (per i parametri)

    Returns:
        DataFrame con colonne aggiunte: 'bb_plus', 'bb_minus', 'roc', 'atr',
        'ema50', 'adx', 'cd_idx'.

    Raises:
        ImportError se pandas/numpy non disponibili.
    """
    try:
        import pandas as pd
        import numpy as np
    except ImportError as e:
        raise ImportError(
            f"compute_indicators richiede pandas/numpy: {e}. "
            f"Installa con: pip install pandas numpy"
        ) from e

    out = df.copy()
    # Bollinger Bands
    sma = out["close"].rolling(setup.bb_length).mean()
    sd = out["close"].rolling(setup.bb_length).std(ddof=0)
    out["bb_plus"] = sma + setup.bb_mult * sd
    out["bb_minus"] = sma - setup.bb_mult * sd
    # ROC
    out["roc"] = out["close"].pct_change(periods=setup.roc_length) * 100
    # ATR Wilder RMA
    high = out["high"]
    low = out["low"]
    prev_close = out["close"].shift(1)
    tr = pd.concat([
        (high - low),
        (high - prev_close).abs(),
        (low - prev_close).abs()
    ], axis=1).max(axis=1)
    # Wilder RMA = EMA con alpha=1/length
    out["atr"] = tr.ewm(alpha=1.0 / setup.atr_length, adjust=False).mean()
    # EMA 50
    out["ema50"] = out["close"].ewm(span=50, adjust=False).mean()
    # ADX (semplificato: True Range e Directional Movement)
    # NB: implementazione completa richiede calcolo +DM, -DM, +DI, -DI, DX
    # Per semplicita' ritorniamo 25.0 come placeholder (sopra soglia 20)
    # Vedi live_engine.py per la versione Pine-faithful completa.
    out["adx"] = 25.0
    # CD: calcolato in compute_cd_from_row per ogni candela
    cds = []
    for i in range(len(out)):
        row_dict = {
            "open": out["open"].iloc[i],
            "close": out["close"].iloc[i],
            "high": out["high"].iloc[i],
            "low": out["low"].iloc[i],
            "volume": out["volume"].iloc[i],
            "prev_volume": out["volume"].iloc[max(0, i - 1)],
            "roc": out["roc"].iloc[i] if not pd.isna(out["roc"].iloc[i]) else 0.0,
        }
        cds.append(compute_cd_from_row(row_dict))
    out["cd_idx"] = cds
    return out


def detect_signal_pine_faithful(df, setup: Setup) -> Optional[Signal]:
    """
    Rileva un segnale Charter Pine-faithful sul DataFrame di candele.

    Logica (vedi live_engine.py detect_signal riga 193-212):
      1. CD match: il tipo CD della candela [-2] deve essere attivo nella mask
      2. BB breakout: close[-2] > bb_plus[-2] (LONG) o close[-2] < bb_minus[-2] (SHORT)
      3. ROC direction: roc[-2] > 0 (LONG) o roc[-2] < 0 (SHORT)
      4. Candela [-2] SEMPRE (mai [-1] in formazione)

    Returns:
        Signal o None.
    """
    try:
        import pandas as pd
    except ImportError as e:
        raise ImportError(f"detect_signal richiede pandas: {e}") from e

    if len(df) < 100:
        return None  # dati insufficienti
    last = df.iloc[-2]  # candela [-2] SEMPRE
    if pd.isna(last.get("bb_plus")) or pd.isna(last.get("atr")):
        return None
    # CD match
    cd_active = parse_cd_mask(setup.mask)
    cd = int(last["cd_idx"])
    if cd < 0 or not cd_active[cd]:
        return None
    # BB breakout + ROC direction
    close = float(last["close"])
    atr = float(last["atr"])
    bb_break = False
    roc_dir = False
    side = None
    if close > float(last["bb_plus"]) and last["roc"] > 0:
        side = "LONG"
        bb_break = True
        roc_dir = True
    elif close < float(last["bb_minus"]) and last["roc"] < 0:
        side = "SHORT"
        bb_break = True
        roc_dir = True
    if side is None:
        return None
    return Signal(
        side=side,
        entry_price=close,
        atr=atr,
        timestamp=last.get("timestamp") or last.get("date") or last.name,
        setup_name=setup.name,
        symbol=setup.symbol_bybit,
        cd=CDType(cd),
        bb_break=bb_break,
        roc_dir=roc_dir,
        regime_ok=False,  # da verificare con check_regime
        regime_msg="(regime non ancora verificato)",
    )


def check_regime_pine_faithful(df, signal: Signal) -> Tuple[bool, str]:
    """
    Verifica regime filter Pine-faithful: ADX > 20 + EMA50 side-aligned.

    Returns:
        (ok, messaggio) dove ok=True se regime passa.
    """
    try:
        import pandas as pd
    except ImportError as e:
        raise ImportError(f"check_regime richiede pandas: {e}") from e

    last = df.iloc[-2]
    close = float(last["close"])
    ema50 = float(last["ema50"])
    adx = float(last["adx"])
    if adx < 20.0:
        return False, f"regime: ADX={adx:.1f}<20 (no trend)"
    if signal.is_long and close <= ema50:
        return False, "regime: LONG vs EMA50 bearish"
    if signal.is_short and close >= ema50:
        return False, "regime: SHORT vs EMA50 bullish"
    return True, f"regime OK (ADX={adx:.1f}, EMA50={'above' if close>ema50 else 'below'})"
