"""
Main trading loop: orchestrates analysis → signal → entry → monitoring → exit.
"""
import logging
import time
import uuid
from datetime import datetime, timezone
from typing import List, Optional

from tabdeal_bot.config.settings import BotConfig
from tabdeal_bot.exchange.client import TabdealClient
from tabdeal_bot.exchange.models import Candle, Direction, Signal, Trade, TradeResult
from tabdeal_bot.strategy.signal_engine import SignalEngine
from tabdeal_bot.risk.position_sizer import PositionSizer
from tabdeal_bot.risk.stop_manager import TrailingStopManager
from tabdeal_bot.bot.state import StateManager
from tabdeal_bot.dashboard.reporter import Reporter

logger = logging.getLogger(__name__)


class Trader:
    def __init__(self, cfg: BotConfig):
        self.cfg = cfg
        self.client = TabdealClient(cfg.exchange)
        self.signal_engine = SignalEngine(cfg.strategy, cfg.trading)
        self.sizer = PositionSizer(cfg.trading)
        self.trailing = TrailingStopManager(cfg.trading)
        self.state_mgr = StateManager(cfg.state_file)
        self.reporter = Reporter(cfg.trades_file)
        self.state = self.state_mgr.state
        self._current_trade: Optional[Trade] = None

    # ------------------------------------------------------------------ #
    #  Startup                                                             #
    # ------------------------------------------------------------------ #

    def start(self):
        logger.info("=== Tabdeal Bot starting ===")
        logger.info("Demo mode: %s", self.cfg.exchange.demo_mode)
        logger.info("Symbol: %s | TF: %s | Leverage: %dx",
                    self.cfg.trading.symbol, self.cfg.trading.timeframe, self.cfg.trading.leverage)

        if not self.client.ping():
            logger.error("Cannot reach Tabdeal API — aborting")
            return

        if not self.cfg.exchange.demo_mode:
            self._setup_leverage()
            self._recover_open_position()

        logger.info("Initial analysis phase: %d minutes", self.cfg.trading.initial_analysis_minutes)
        time.sleep(self.cfg.trading.initial_analysis_minutes * 60)

        self._run_loop()

    # ------------------------------------------------------------------ #
    #  Main loop                                                           #
    # ------------------------------------------------------------------ #

    def _run_loop(self):
        while self.state.is_running:
            try:
                self._reset_daily_if_needed()

                if self.state.daily_loss_halt or self.state.drawdown_halt:
                    logger.warning("Bot halted: daily_loss=%s drawdown=%s",
                                   self.state.daily_loss_halt, self.state.drawdown_halt)
                    time.sleep(300)
                    continue

                if self._current_trade is not None:
                    self._monitor_open_trade()
                else:
                    self._seek_entry()

                time.sleep(self.cfg.poll_interval_seconds)

            except KeyboardInterrupt:
                logger.info("Interrupted — shutting down")
                break
            except Exception as e:
                logger.exception("Unexpected error in main loop: %s", e)
                time.sleep(30)

    # ------------------------------------------------------------------ #
    #  Entry                                                               #
    # ------------------------------------------------------------------ #

    def _seek_entry(self):
        candles = self._fetch_candles()
        if not candles:
            return

        signal = self.signal_engine.analyze(candles)
        if signal is None:
            logger.info("No valid signal — waiting")
            return

        # Apply adaptive direction filter
        effective_direction = self._apply_adaptive_filter(signal)
        if effective_direction == Direction.NONE:
            logger.info("Adaptive filter suppressed signal")
            return

        # Override signal direction if adaptive logic flipped it
        if effective_direction != signal.direction:
            logger.info("Adaptive logic: flipping signal from %s to %s",
                        signal.direction.value, effective_direction.value)
            signal = self._recalculate_signal_for_direction(signal, effective_direction, candles)
            if signal is None:
                return

        balance = self._get_balance()
        self._check_drawdown(balance)
        if self.state.drawdown_halt:
            return

        qty = self.sizer.calculate(balance, signal.entry_price, signal.sl_price)
        if qty <= 0:
            logger.info("Position size is zero — skipping")
            return

        logger.info(
            "ENTERING %s | entry=%.4f SL=%.4f TP=%.4f qty=%.4f R:R=%.2f conf=%.0f%%",
            signal.direction.value, signal.entry_price, signal.sl_price,
            signal.tp_price, qty, signal.risk_reward, signal.confidence * 100,
        )

        trade = self._open_trade(signal, qty)
        if trade:
            self._current_trade = trade
            self.state.open_trade_id = trade.id
            self.state.current_direction = trade.direction
            self.state_mgr.save()

    def _apply_adaptive_filter(self, signal: Signal) -> Direction:
        tc = self.cfg.trading
        last = self.state.last_trade_result

        if last is None:
            return signal.direction

        if last == TradeResult.WIN and tc.continue_on_win:
            # Prefer same direction; allow if signal matches
            if signal.direction == self.state.current_direction:
                return signal.direction
            else:
                # Signal contradicts winning direction — stay cautious, use signal anyway
                return signal.direction

        if last == TradeResult.LOSS and tc.reverse_on_loss:
            # Reverse the previous direction
            preferred = (Direction.SHORT if self.state.current_direction == Direction.LONG
                         else Direction.LONG)
            # Only reverse if market signal supports the preferred direction
            if signal.direction == preferred:
                return preferred
            else:
                # Market doesn't confirm — don't trade
                logger.info("Reverse-on-loss: preferred %s but signal is %s — skipping",
                            preferred.value, signal.direction.value)
                return Direction.NONE

        return signal.direction

    def _recalculate_signal_for_direction(
        self, original: Signal, direction: Direction, candles: List[Candle]
    ) -> Optional[Signal]:
        """Recompute SL/TP for the new direction at the same ATR level."""
        price = original.entry_price
        atr = original.atr
        sl_dist = atr * self.cfg.trading.sl_atr_multiplier
        tp_dist = atr * self.cfg.trading.tp_atr_multiplier

        if direction == Direction.LONG:
            sl = price - sl_dist
            tp = price + tp_dist
        else:
            sl = price + sl_dist
            tp = price - tp_dist

        risk = abs(price - sl)
        reward = abs(tp - price)
        rr = reward / risk if risk > 0 else 0

        if rr < self.cfg.trading.min_risk_reward:
            return None

        return Signal(
            direction=direction,
            confidence=original.confidence * 0.8,  # slightly lower confidence
            atr=atr,
            entry_price=price,
            sl_price=round(sl, 2),
            tp_price=round(tp, 2),
            risk_reward=rr,
            adx=original.adx,
            rsi=original.rsi,
            reason=original.reason + " [adaptive reversed]",
        )

    # ------------------------------------------------------------------ #
    #  Open / close                                                        #
    # ------------------------------------------------------------------ #

    def _open_trade(self, signal: Signal, qty: float) -> Optional[Trade]:
        trade_id = str(uuid.uuid4())[:8]

        if not self.cfg.exchange.demo_mode:
            side = "BUY" if signal.direction == Direction.LONG else "SELL"
            try:
                order = self.client.place_order(
                    symbol=self.cfg.trading.symbol,
                    side=side,
                    order_type="MARKET",
                    quantity=qty,
                    client_order_id=trade_id,
                )
                position_id = str(order.get("orderId", trade_id))
            except Exception as e:
                logger.error("Failed to place order: %s", e)
                return None

            # Set SL/TP on the exchange
            try:
                self.client.set_sl_tp(
                    position_id=position_id,
                    symbol=self.cfg.trading.symbol,
                    sl_price=signal.sl_price,
                    tp_price=signal.tp_price,
                )
            except Exception as e:
                logger.warning("Failed to set SL/TP on exchange: %s — managing locally", e)
        else:
            position_id = f"DEMO_{trade_id}"

        trade = Trade(
            id=trade_id,
            symbol=self.cfg.trading.symbol,
            direction=signal.direction,
            entry_price=signal.entry_price,
            sl_price=signal.sl_price,
            tp_price=signal.tp_price,
            quantity=qty,
            leverage=self.cfg.trading.leverage,
            open_time=int(time.time() * 1000),
            position_id=position_id,
            notes=signal.reason,
        )

        self.trailing.reset(signal.entry_price, signal.sl_price, signal.direction)
        self.reporter.record_open(trade)
        return trade

    def _close_trade(self, trade: Trade, exit_price: float, reason: str):
        logger.info("CLOSING trade %s at %.4f (%s)", trade.id, exit_price, reason)

        if not self.cfg.exchange.demo_mode:
            try:
                self.client.close_position(self.cfg.trading.symbol)
            except Exception as e:
                logger.error("Failed to close position: %s", e)

        multiplier = 1.0 if trade.direction == Direction.LONG else -1.0
        pnl = (exit_price - trade.entry_price) * multiplier * trade.quantity * trade.leverage
        fees = exit_price * trade.quantity * (self.cfg.trading.taker_fee + self.cfg.trading.slippage) * 2

        trade.close_time = int(time.time() * 1000)
        trade.exit_price = exit_price
        trade.pnl_usdt = pnl
        trade.fees_usdt = fees

        net = pnl - fees
        if net > 0:
            trade.result = TradeResult.WIN
            self.state.consecutive_wins += 1
            self.state.consecutive_losses = 0
        elif net < 0:
            trade.result = TradeResult.LOSS
            self.state.consecutive_losses += 1
            self.state.consecutive_wins = 0
        else:
            trade.result = TradeResult.BREAKEVEN

        self.state.last_trade_result = trade.result
        self.state.daily_pnl += net
        self.state.total_pnl += net
        self.state.trades_today += 1

        self.reporter.record_close(trade)
        logger.info(
            "Trade %s closed: pnl=%.4f fees=%.4f net=%.4f result=%s",
            trade.id, pnl, fees, net, trade.result.value,
        )

        self._current_trade = None
        self.state.open_trade_id = None
        self.state_mgr.save()

        # Daily loss check
        balance = self._get_balance()
        daily_loss_limit = balance * self.cfg.trading.max_daily_loss_pct
        if self.state.daily_pnl < -daily_loss_limit:
            logger.warning("Daily loss limit reached (%.2f USDT) — halting for today", self.state.daily_pnl)
            self.state.daily_loss_halt = True
            self.state_mgr.save()

    # ------------------------------------------------------------------ #
    #  Monitoring                                                          #
    # ------------------------------------------------------------------ #

    def _monitor_open_trade(self):
        trade = self._current_trade
        if trade is None:
            return

        # In demo mode, check price against SL/TP locally
        candles = self._fetch_candles(limit=5)
        if not candles:
            return

        price = candles[-1].close
        atr = self.signal_engine.analyze(candles[-self.cfg.trading.lookback_candles:])
        current_atr = candles[-1].high - candles[-1].low  # rough proxy if no full signal

        # Update trailing stop
        new_sl = self.trailing.update(price, current_atr)
        if new_sl != trade.sl_price and not self.cfg.exchange.demo_mode and trade.position_id:
            try:
                self.client.set_sl_tp(
                    position_id=trade.position_id,
                    symbol=trade.symbol,
                    sl_price=new_sl,
                )
                trade.sl_price = new_sl
            except Exception as e:
                logger.warning("Could not update trailing SL on exchange: %s", e)
        elif new_sl != trade.sl_price:
            trade.sl_price = new_sl

        # Check hit in demo mode (live mode: exchange handles it)
        if self.cfg.exchange.demo_mode:
            if trade.direction == Direction.LONG:
                if price <= trade.sl_price:
                    self._close_trade(trade, trade.sl_price, "SL hit")
                elif price >= trade.tp_price:
                    self._close_trade(trade, trade.tp_price, "TP hit")
            else:
                if price >= trade.sl_price:
                    self._close_trade(trade, trade.sl_price, "SL hit")
                elif price <= trade.tp_price:
                    self._close_trade(trade, trade.tp_price, "TP hit")
        else:
            # In live mode, check if position is still open
            pos = self.client.get_position(trade.symbol)
            if pos is None:
                # Position closed externally (SL/TP hit)
                self._close_trade(trade, price, "closed by exchange")

    # ------------------------------------------------------------------ #
    #  Helpers                                                             #
    # ------------------------------------------------------------------ #

    def _fetch_candles(self, limit: Optional[int] = None) -> List[Candle]:
        n = limit or self.cfg.trading.lookback_candles
        try:
            raw = self.client.get_klines(
                self.cfg.trading.symbol, self.cfg.trading.timeframe, limit=n
            )
            return [Candle.from_binance_list(r) for r in raw]
        except Exception as e:
            logger.error("Failed to fetch candles: %s", e)
            return []

    def _get_balance(self) -> float:
        if self.cfg.exchange.demo_mode:
            return 1000.0  # paper balance
        try:
            return self.client.get_usdt_balance()
        except Exception as e:
            logger.error("Could not fetch balance: %s", e)
            return 0.0

    def _setup_leverage(self):
        try:
            self.client.set_leverage(self.cfg.trading.symbol, self.cfg.trading.leverage)
            logger.info("Leverage set to %dx", self.cfg.trading.leverage)
        except Exception as e:
            logger.warning("Could not set leverage: %s", e)

    def _recover_open_position(self):
        """On restart, check if there is already an open position."""
        try:
            pos = self.client.get_position(self.cfg.trading.symbol)
            if pos:
                logger.warning(
                    "Found existing position on startup: %s — will monitor it",
                    pos
                )
                # Reconstruct a minimal trade record
                amt = float(pos.get("positionAmt", 0))
                direction = Direction.LONG if amt > 0 else Direction.SHORT
                self._current_trade = Trade(
                    id="RECOVERED",
                    symbol=self.cfg.trading.symbol,
                    direction=direction,
                    entry_price=float(pos.get("entryPrice", 0)),
                    sl_price=0.0,
                    tp_price=0.0,
                    quantity=abs(amt),
                    leverage=self.cfg.trading.leverage,
                    open_time=int(time.time() * 1000),
                    notes="recovered on restart",
                )
        except Exception as e:
            logger.warning("Could not recover position: %s", e)

    def _check_drawdown(self, balance: float):
        if balance > self.state.peak_balance:
            self.state.peak_balance = balance
        if self.state.peak_balance > 0:
            dd = (self.state.peak_balance - balance) / self.state.peak_balance
            if dd >= self.cfg.trading.max_drawdown_pct:
                logger.error(
                    "Max drawdown %.1f%% breached (peak=%.2f current=%.2f) — halting",
                    dd * 100, self.state.peak_balance, balance,
                )
                self.state.drawdown_halt = True
                self.state_mgr.save()

    def _reset_daily_if_needed(self):
        # Simple: reset at UTC midnight
        now = datetime.now(timezone.utc)
        if now.hour == 0 and now.minute < 1:
            if self.state.daily_pnl != 0 or self.state.trades_today != 0:
                logger.info("New UTC day — resetting daily stats")
                self.state_mgr.reset_daily()
