"""
Live Trading Engine - Real testnet order execution with same V2 strategy
Wraps strategy logic, replaces simulation with real Binance testnet orders
"""
import os
import sys
import time
import json
import logging
import threading
from datetime import datetime
from collections import deque
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict
from dotenv import load_dotenv

load_dotenv()

sys.path.insert(0, os.path.dirname(__file__))
from config.settings_v2 import V2Params
from strategy.detector_v2 import detect_v2
from strategy.simulator_v2 import simulate_v2
from binance_client import BinanceClient, load_client_from_env

logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
logger = logging.getLogger(__name__)


@dataclass
class Position:
    symbol: str
    side: str  # "SHORT"
    entry_price: float
    quantity: float
    leverage: float
    entry_time: int
    sl_price: float
    trailing_sl: float
    highest_price: float  # track low for short
    binance_order_id: int = 0
    binance_entry_price: float = 0.0
    unrealized_pnl: float = 0.0
    realized_pnl: float = 0.0


@dataclass
class TradeRecord:
    symbol: str
    entry_time: int
    exit_time: int
    entry_price: float
    exit_price: float
    quantity: float
    leverage: float
    pnl_usd: float
    pnl_pct: float
    exit_reason: str
    score: int


class LiveEngine:
    def __init__(self, client: BinanceClient, params: V2Params = None):
        self.client = client
        self.params = params or V2Params()
        self.equity = 100.0  # Paper equity for risk calc (testnet has 100k USDT)
        self.initial_equity = 100.0
        
        # State
        self.positions: Dict[str, Position] = {}  # symbol -> Position
        self.trade_history: List[TradeRecord] = []
        self.equity_curve: List[float] = [self.equity]
        
        # Data buffers (symbol -> deque of klines)
        self.klines_15m: Dict[str, deque] = {}
        self.klines_4h: Dict[str, deque] = {}
        self.max_bars_15m = 2000  # ~17 days at 96 bars/day
        self.max_bars_4h = 200    # ~33 days at 6 bars/day
        
        # Risk limits
        self.max_daily_loss_pct = float(os.getenv("MAX_DAILY_LOSS_PCT", "5.0"))
        self.max_total_loss_pct = float(os.getenv("MAX_TOTAL_LOSS_PCT", "15.0"))
        self.max_position_usd = float(os.getenv("MAX_POSITION_USD", "200"))
        self.max_concurrent = int(os.getenv("MAX_CONCURRENT_POSITIONS", "3"))
        self.min_notional = float(os.getenv("MIN_24H_NOTIONAL_USD", "5000000"))
        
        # Tracking
        self.daily_start_equity = self.equity
        self.last_daily_reset = datetime.now().date()
        self.running = False
        self.symbols: List[str] = []
        
        # Sync
        self.last_sync = 0
        self.sync_interval = 30  # seconds
        
        # State file
        self.state_file = "paper_state.json"
        self.load_state()
        
        logger.info(f"LiveEngine initialized | Testnet: {client.base_url}")
        logger.info(f"Risk: daily={self.max_daily_loss_pct}% total={self.max_total_loss_pct}% max_pos=${self.max_position_usd} concurrent={self.max_concurrent}")

    def load_initial_data(self):
        """Load historical klines for all symbols"""
        logger.info(f"Loading initial data for {len(self.symbols)} symbols...")
        for sym in self.symbols:
            try:
                k15 = self.client.get_klines(sym, "15m", limit=min(self.max_bars_15m, 1500))
                k4 = self.client.get_klines(sym, "4h", limit=self.max_bars_4h)
                self.klines_15m[sym] = deque(k15, maxlen=self.max_bars_15m)
                self.klines_4h[sym] = deque(k4, maxlen=self.max_bars_4h)
                logger.info(f"  {sym}: {len(k15)} 15m bars, {len(k4)} 4h bars")
            except Exception as e:
                logger.error(f"  {sym}: Failed - {e}")
        logger.info("Initial data loaded")
        
        # Sync existing positions from Binance
        self.sync_positions_from_binance()

    def update_symbols(self):
        """Refresh tradable universe daily - use top gainers for pump detection"""
        try:
            self.symbols = self.client.get_top_gainers(
                min_notional=self.min_notional, 
                limit=100
            )
            logger.info(f"Universe updated (top gainers): {len(self.symbols)} symbols")
            if self.symbols:
                logger.info(f"Top 5: {self.symbols[:5]}")
        except Exception as e:
            logger.error(f"Failed to update symbols: {e}")

    def _check_risk_limits(self) -> bool:
        """Check if we can open new positions"""
        # Daily loss limit
        daily_loss_pct = (self.daily_start_equity - self.equity) / self.daily_start_equity * 100
        if daily_loss_pct >= self.max_daily_loss_pct:
            logger.warning(f"Daily loss limit hit: {daily_loss_pct:.1f}% >= {self.max_daily_loss_pct}%")
            return False
        
        # Total loss limit
        total_loss_pct = (self.initial_equity - self.equity) / self.initial_equity * 100
        if total_loss_pct >= self.max_total_loss_pct:
            logger.warning(f"Total loss limit hit: {total_loss_pct:.1f}% >= {self.max_total_loss_pct}%")
            return False
        
        # Max concurrent positions
        if len(self.positions) >= self.max_concurrent:
            return False
            
        return True

    def _reset_daily(self):
        """Reset daily tracking at midnight"""
        today = datetime.now().date()
        if today > self.last_daily_reset:
            self.daily_start_equity = self.equity
            self.last_daily_reset = today
            logger.info(f"Daily reset: equity = ${self.equity:.2f}")

    def sync_positions_from_binance(self):
        """Sync positions from Binance testnet"""
        try:
            for sym in list(self.positions.keys()) + self.symbols:
                pos_data = self.client.get_position(sym)
                amt = pos_data.get("amt", 0.0)
                if amt < 0:  # SHORT position
                    if sym not in self.positions:
                        # New position from Binance - reconstruct
                        entry = pos_data.get("entry", 0.0)
                        logger.info(f"SYNC: Found existing SHORT {sym} amt={amt} entry={entry}")
                        self.positions[sym] = Position(
                            symbol=sym,
                            side="SHORT",
                            entry_price=entry,
                            quantity=abs(amt),
                            leverage=5.0,  # Will be updated from order
                            entry_time=int(time.time() * 1000),
                            sl_price=entry * 1.02,  # Approx
                            trailing_sl=entry * 1.02,
                            highest_price=entry,
                        )
                elif sym in self.positions and amt >= 0:
                    # Position closed on Binance but not locally
                    logger.info(f"SYNC: Position {sym} closed on Binance")
                    self.close_position(sym, self.positions[sym].entry_price * 1.01, "SYNC_CLOSED")
        except Exception as e:
            logger.error(f"Position sync error: {e}")

    def place_short_order(self, symbol: str, sim, kline: dict) -> bool:
        """Place real SHORT order on Binance testnet"""
        try:
            # Calculate quantity
            risk_pct = self.params.risk_score4 if sim.get("squeeze_score", 0) >= 4 else self.params.risk_score3
            risk_usd = self.equity * risk_pct
            sl_dist = sim.sl - sim.entry_price  # positive for short
            if sl_dist <= 0:
                return False
            
            notional = (risk_usd * sim.leverage) / (sl_dist / sim.entry_price)
            notional = min(notional, self.max_position_usd)
            qty = notional / sim.entry_price
            
            if qty <= 0:
                return False
            
            # Format quantity for Binance precision
            qty_str = self.client._format_qty(symbol, qty)
            qty_formatted = float(qty_str)
            
            # Set leverage first
            lev = int(sim.leverage)
            self.client.set_leverage(symbol, lev)
            self.client.set_margin_type(symbol, "ISOLATED")
            
            # Place MARKET SELL order (SHORT)
            order = self.client.place_order(
                symbol=symbol,
                side="SELL",
                quantity=qty_formatted,
                order_type="MARKET",
                reduce_only=False
            )
            
            if "orderId" not in order:
                logger.error(f"Order failed: {order}")
                return False
            
            order_id = order["orderId"]
            logger.info(f"ORDER PLACED: {symbol} SELL {qty_formatted} @ MARKET | ID={order_id}")
            
            # Get fill price (for MARKET order, should be filled)
            # In testnet, MARKET orders fill instantly
            fill_price = sim.entry_price  # Approximate
            
            # Create local position
            pos = Position(
                symbol=symbol,
                side="SHORT",
                entry_price=fill_price,
                quantity=qty_formatted,
                leverage=sim.leverage,
                entry_time=kline["time"],
                sl_price=sim.sl,
                trailing_sl=sim.sl,
                highest_price=fill_price,
                binance_order_id=order_id,
                binance_entry_price=fill_price,
            )
            self.positions[symbol] = pos
            
            logger.info(f"OPEN SHORT {symbol} @ {fill_price:.6f} qty={qty_formatted} lev={sim.leverage:.2f}x SL={sim.sl:.6f} | OrderID={order_id}")
            
            # Save state
            self.save_state()
            return True
            
        except Exception as e:
            logger.error(f"Place order error {symbol}: {e}")
            return False

    def close_position(self, symbol: str, exit_price: float, reason: str):
        """Close position on Binance and locally"""
        if symbol not in self.positions:
            return
        
        pos = self.positions.pop(symbol)
        
        try:
            # Place BUY order to close SHORT (reduceOnly)
            close_order = self.client.place_order(
                symbol=symbol,
                side="BUY",
                quantity=pos.quantity,
                order_type="MARKET",
                reduce_only=True
            )
            logger.info(f"CLOSE ORDER: {symbol} BUY {pos.quantity} @ MARKET reduceOnly | ID={close_order.get('orderId', 'N/A')}")
        except Exception as e:
            logger.error(f"Close order error {symbol}: {e}")
        
        # Calculate PnL
        pnl_pct = (pos.entry_price - exit_price) / pos.entry_price  # short
        pnl_usd = pnl_pct * pos.quantity * pos.entry_price * pos.leverage
        
        # Fees
        fee = pos.quantity * exit_price * pos.leverage * self.params.fee_pct * 2
        slippage = pos.quantity * exit_price * pos.leverage * self.params.slippage_pct * 2
        net_pnl = pnl_usd - fee - slippage
        
        # Update equity
        self.equity += net_pnl
        self.equity_curve.append(self.equity)
        
        # Record
        trade = TradeRecord(
            symbol=symbol,
            entry_time=pos.entry_time,
            exit_time=int(time.time() * 1000),
            entry_price=pos.entry_price,
            exit_price=exit_price,
            quantity=pos.quantity,
            leverage=pos.leverage,
            pnl_usd=net_pnl,
            pnl_pct=net_pnl / self.equity * 100,
            exit_reason=reason,
            score=3,
        )
        self.trade_history.append(trade)
        
        logger.info(f"CLOSE {symbol} @ {exit_price:.6f} | {reason} | PnL: ${net_pnl:.2f} ({net_pnl/self.equity*100:+.2f}%) | Equity: ${self.equity:.2f}")
        
        # Save state
        self.save_state()

    def update_positions(self, symbol: str, current_price: float):
        """Update trailing SL and check exits for open position"""
        if symbol not in self.positions:
            return
        
        pos = self.positions[symbol]
        
        # Update highest (lowest for short)
        if current_price < pos.highest_price:
            pos.highest_price = current_price
            # Update trailing SL
            coef = self.params.trailing_k * pos.highest_price * 0.001  # approx ATR
            new_sl = min(pos.trailing_sl, pos.highest_price + coef)
            new_sl = min(new_sl, pos.entry_price - coef)  # floor below entry
            if new_sl < pos.trailing_sl:
                pos.trailing_sl = new_sl
                logger.debug(f"Trailing SL update {symbol}: {pos.trailing_sl:.6f}")
        
        # Check exits
        exit_reason = None
        exit_price = None
        
        # Liquidation check
        liq_price = pos.entry_price * (1 + (1 - self.params.maintenance_margin) / pos.leverage)
        if current_price >= liq_price:
            exit_reason = "LIQUIDATION"
            exit_price = liq_price
        # Hard SL hit
        elif current_price >= pos.sl_price:
            exit_reason = "LOSS_HARD_STOP"
            exit_price = pos.sl_price
        # Trailing SL hit
        elif current_price >= pos.trailing_sl:
            exit_reason = "WINNER_TRAIL"
            exit_price = pos.trailing_sl
        
        if exit_reason:
            self.close_position(symbol, exit_price, exit_reason)

    def on_kline_update(self, kline: dict):
        """Callback for WebSocket kline updates"""
        symbol = kline["symbol"]
        if not kline.get("x"):  # Not closed yet
            return
        
        self._reset_daily()
        
        # Update 15m buffer
        self.process_closed_kline(symbol, kline)
        
        # Check position exits
        if symbol in self.positions:
            self.update_positions(symbol, kline["close"])
        
        # Periodic position sync with Binance
        if time.time() - self.last_sync > self.sync_interval:
            self.sync_positions_from_binance()
            self.last_sync = time.time()

    def process_closed_kline(self, symbol: str, kline: dict):
        """Process a closed 15m kline - main signal generation"""
        # Update buffers
        if symbol not in self.klines_15m:
            self.klines_15m[symbol] = deque(maxlen=self.max_bars_15m)
        self.klines_15m[symbol].append(kline)
        
        # Need 4h data too
        if symbol not in self.klines_4h or len(self.klines_4h[symbol]) < 50:
            return
        
        # Convert to list format for detector
        k15_list = list(self.klines_15m[symbol])
        k4_list = list(self.klines_4h[symbol])
        
        # Run detector
        signals = detect_v2(k4_list, k15_list, self.params)
        
        for sig in signals:
            # Check if we already have position
            if symbol in self.positions:
                continue
            
            # Risk check
            if not self._check_risk_limits():
                continue
            
            # Simulate trade to get SL, leverage, etc.
            sim = simulate_v2(symbol, k15_list, sig, self.params, equity=self.equity)
            if not sim or sim.r_multiple is None:
                continue
            
            # Place real order
            self.place_short_order(symbol, sim, kline)

    def save_state(self, path: str = None):
        """Save state to disk (for dashboard)"""
        path = path or self.state_file
        state = {
            "equity": self.equity,
            "equity_curve": self.equity_curve,
            "trade_history": [asdict(t) for t in self.trade_history],
            "positions": {s: asdict(p) for s, p in self.positions.items()},
            "timestamp": int(time.time() * 1000),
        }
        with open(path, "w") as f:
            json.dump(state, f, indent=2)

    def load_state(self, path: str = None):
        """Load state from disk"""
        path = path or self.state_file
        if not os.path.exists(path):
            return
        try:
            with open(path, "r") as f:
                state = json.load(f)
            self.equity = state["equity"]
            self.equity_curve = state["equity_curve"]
            self.trade_history = [TradeRecord(**t) for t in state["trade_history"]]
            self.positions = {s: Position(**p) for s, p in state["positions"].items()}
            logger.info(f"Loaded state: equity=${self.equity:.2f}, {len(self.trade_history)} trades, {len(self.positions)} open")
        except Exception as e:
            logger.warning(f"Load state failed: {e}")

    def emergency_close_all(self):
        """Kill switch - close all positions"""
        logger.warning("EMERGENCY CLOSE ALL TRIGGERED")
        for sym in list(self.positions.keys()):
            pos = self.positions[sym]
            try:
                self.client.place_order(
                    symbol=sym,
                    side="BUY",
                    quantity=pos.quantity,
                    order_type="MARKET",
                    reduce_only=True
                )
            except Exception as e:
                logger.error(f"Emergency close failed {sym}: {e}")
            self.close_position(sym, pos.entry_price * 1.01, "EMERGENCY")


def run_live_trading():
    """Main entry point for live testnet trading"""
    client = load_client_from_env()
    
    # Load optimized params
    params = V2Params()
    params.range_pctile_thr = float(os.getenv("RANGE_PCTILE_THR", "24.0"))
    params.atr_ratio_thr = float(os.getenv("ATR_RATIO_THR", "0.78"))
    params.volume_ratio_thr = float(os.getenv("VOLUME_RATIO_THR", "0.88"))
    params.va_pctile_thr = float(os.getenv("VA_PCTILE_THR", "28.0"))
    params.trailing_k = float(os.getenv("TRAILING_K", "2.9"))
    
    engine = LiveEngine(client, params)
    
    # Initial setup
    engine.update_symbols()
    engine.load_initial_data()
    
    # Start WebSocket for 15m klines
    ws = client.start_kline_stream(engine.symbols, "15m", engine.on_kline_update)
    
    # Also fetch 4h klines periodically
    def refresh_4h():
        while engine.running:
            try:
                for sym in engine.symbols:
                    k4 = client.get_klines(sym, "4h", limit=engine.max_bars_4h)
                    engine.klines_4h[sym] = deque(k4, maxlen=engine.max_bars_4h)
            except Exception as e:
                logger.error(f"4h refresh error: {e}")
            time.sleep(3600)  # every hour
    
    threading.Thread(target=refresh_4h, daemon=True).start()
    
    logger.info(f"LIVE TRADING STARTED for {len(engine.symbols)} symbols")
    logger.info(f"Params: range<{params.range_pctile_thr} atr<{params.atr_ratio_thr} vol<{params.volume_ratio_thr} va<{params.va_pctile_thr} trail={params.trailing_k}")
    logger.info("Dashboard: paper_state.json updates in real-time")
    
    engine.running = True
    try:
        while engine.running:
            time.sleep(1)
    except KeyboardInterrupt:
        logger.info("Shutting down...")
    finally:
        engine.running = False
        engine.save_state()
        logger.info(f"Final equity: ${engine.equity:.2f} ({(engine.equity/engine.initial_equity-1)*100:+.1f}%)")
        logger.info(f"Total trades: {len(engine.trade_history)}")


if __name__ == "__main__":
    run_live_trading()