"""
trader.py - Ejecucion de ordenes y gestion de posiciones con OCO
=================================================================
Flujo:
1. Market Buy - compra inmediata al mejor precio disponible
2. Coloca OCO (TP + SL) directamente en el libro de ordenes de Binance
3. Binance gestiona la ejecucion - el bot NO necesita estar corriendo

Ventajas de OCO vs monitoreo por bot:
- Si el bot se cae, Binance SIGUE protegiendo tu posicion
- TP y SL se ejecutan al instante (milisegundos), no cada minuto del cron
- No dependes de la conectividad del hosting
"""

from utils import setup_logger, retry_api, log_trade_to_csv
import time

logger = setup_logger()


class Trader:
    """
    Ejecuta compras de mercado y coloca ordenes OCO (One Cancels the Other)
    directamente en el libro de ordenes de Binance para gestionar TP y SL.
    """

    def __init__(self, exchange, state_manager, notifier, strategy,
                 default_tp_pct=0.02, default_sl_pct=0.01):
        self.exchange = exchange
        self.state = state_manager
        self.notifier = notifier
        self.strategy = strategy
        self.default_tp_pct = default_tp_pct
        self.default_sl_pct = default_sl_pct
        self.trading_fee = 0.001  # 0.1% Binance taker fee

    # ------------------------------------------------------------------ #
    # Utilidades                                                          #
    # ------------------------------------------------------------------ #

    @retry_api(max_retries=2)
    def get_available_balance(self, currency="USDT"):
        """Obtiene el balance libre disponible para operar."""
        balance = self.exchange.fetch_balance()
        return float(balance[currency]["free"])

    def _round_price(self, symbol, price):
        """Redondea precio segun la precision del par en Binance."""
        return float(self.exchange.price_to_precision(symbol, price))

    def _round_amount(self, symbol, amount):
        """Redondea cantidad segun la precision del par en Binance."""
        return float(self.exchange.amount_to_precision(symbol, amount))

    # ------------------------------------------------------------------ #
    # Compra + OCO                                                        #
    # ------------------------------------------------------------------ #

    def execute_buy(self, symbol):
        """
        Flujo completo de entrada:
        1. Compra a mercado (market order)
        2. Calcula TP/SL dinamicos con ATR
        3. Coloca OCO (Take Profit + Stop Loss) en Binance
        4. Si OCO falla - venta de emergencia (no dejar posicion sin proteccion)
        """
        try:
            # Verificar que no haya posicion abierta
            if self.state.get_position():
                logger.warning("Ya existe una posicion abierta. Saltando compra.")
                return False

            # Obtener balance y calcular monto
            balance = self.get_available_balance("USDT")
            trade_amount = balance * 1  # 100% del balance (margen para fees)

            if trade_amount < 10:  # Minimo ~$10 USDT
                logger.warning(f"Balance insuficiente: ${balance:.2f} USDT")
                return False

            # -- 1. MARKET BUY --
            logger.info(
                f"Ejecutando COMPRA de {symbol} por ~${trade_amount:.2f} USDT..."
            )
            order = self.exchange.create_market_buy_order(
                symbol, None, {"quoteOrderQty": trade_amount}
            )

            # Extraer datos reales de la orden ejecutada
            buy_price = float(order.get("average", order.get("price", 0)))
            filled_amount = float(order.get("filled", 0))
            cost = float(order.get("cost", buy_price * filled_amount))

            if buy_price <= 0 or filled_amount <= 0:
                logger.error(
                    f"Orden de compra anomala: price={buy_price}, "
                    f"filled={filled_amount}"
                )
                return False

            logger.info(
                f"COMPRA EJECUTADA: {symbol} | "
                f"Precio: {buy_price:.8f} | Cant: {filled_amount:.8f} | "
                f"Costo: ${cost:.2f}"
            )

            # --- AJUSTE CLAVE ---
            # Si pagas fees con la moneda comprada, NO puedes asumir que
            # filled_amount es 100% vendible. Usa el balance 'free' real del activo base.
            market = self.exchange.market(symbol)
            base = market["base"]

            bal = self.exchange.fetch_balance()
            free_base = float(bal.get(base, {}).get("free", 0) or 0)

            if free_base <= 0:
                logger.warning(
                    f"[{symbol}] Balance free de {base} es 0 justo despues de comprar. "
                    "Esperando 2s y reintentando..."
                )
                time.sleep(2)
                bal = self.exchange.fetch_balance()
                free_base = float(bal.get(base, {}).get("free", 0) or 0)

            if free_base <= 0:
                logger.error(
                    f"[{symbol}] No se pudo obtener balance vendible de {base} tras la compra. "
                    "No se colocara OCO; intentando venta de emergencia."
                )
                self._emergency_sell(symbol, filled_amount, buy_price)
                return False

            # Margen anti 'insufficient balance' por fees/redondeo
            sell_amount = free_base * 0.995
            sell_amount = float(self.exchange.amount_to_precision(symbol, sell_amount))

            if sell_amount <= 0:
                logger.error(
                    f"[{symbol}] sell_amount redondeado es 0. free_base={free_base} {base}. "
                    "Intentando venta de emergencia."
                )
                self._emergency_sell(symbol, filled_amount, buy_price)
                return False

            logger.info(
                f"[{symbol}] Cantidad vendible calculada para OCO: {sell_amount} {base} "
                f"(free={free_base})"
            )
            # --- FIN AJUSTE CLAVE ---

            # -- 2. CALCULAR TP/SL DINAMICOS --
            atr_result = self.strategy.calculate_dynamic_tp_sl(symbol)
            if atr_result:
                tp_pct, sl_pct = atr_result
                logger.info(
                    f"TP/SL dinamicos (ATR): "
                    f"TP={tp_pct * 100:.2f}%, SL={sl_pct * 100:.2f}%"
                )
            else:
                tp_pct = self.default_tp_pct
                sl_pct = self.default_sl_pct
                logger.warning(
                    f"ATR no disponible, usando defaults: "
                    f"TP={tp_pct * 100:.1f}%, SL={sl_pct * 100:.1f}%"
                )

            # Calcular precios de TP y SL
            tp_price = buy_price * (1 + tp_pct + self.trading_fee)
            sl_trigger = buy_price * (1 - sl_pct)
            # Precio limite del SL un poco peor que el trigger (garantizar ejecucion)
            sl_price = buy_price * (1 - sl_pct - self.trading_fee)

            tp_price = self._round_price(symbol, tp_price)
            sl_trigger = self._round_price(symbol, sl_trigger)
            sl_price = self._round_price(symbol, sl_price)

            logger.info(
                f"Niveles OCO: TP={tp_price:.8f} | "
                f"SL trigger={sl_trigger:.8f} | "
                f"SL limit={sl_price:.8f} | Cant={sell_amount}"
            )

            # -- 3. COLOCAR OCO --
            oco_success = self._place_oco_order(
                symbol=symbol,
                amount=sell_amount,
                tp_price=tp_price,
                sl_trigger=sl_trigger,
                sl_price=sl_price,
            )

            if oco_success:
                # Guardar posicion en estado local
                self.state.set_position({
                    "symbol": symbol,
                    "buy_price": buy_price,
                    "amount": sell_amount,
                    "cost": cost,
                    "tp_price": tp_price,
                    "sl_trigger": sl_trigger,
                    "sl_price": sl_price,
                    "tp_pct": tp_pct,
                    "sl_pct": sl_pct,
                    "oco_placed": True,
                    "order_id": order.get("id", "unknown"),
                })

                # Notificacion Telegram
                self.notifier.send_message(
                    f"<b>COMPRA EJECUTADA</b>\n"
                    f"Par: <code>{symbol}</code>\n"
                    f"Precio: <code>{buy_price:.8f}</code>\n"
                    f"Cantidad: <code>{sell_amount}</code>\n"
                    f"Costo: <code>${cost:.2f}</code>\n"
                    f"---\n"
                    f"TP: <code>{tp_price:.8f}</code> (+{tp_pct * 100:.2f}%)\n"
                    f"SL: <code>{sl_trigger:.8f}</code> (-{sl_pct * 100:.2f}%)\n"
                    f"OCO colocada en Binance"
                )
                
                # >>> ADD THIS BLOCK OVER HERE <<<
                log_trade_to_csv({
                    "symbol": symbol,
                    "side": "BUY",
                    "amount": filled_amount,
                    "price": buy_price,
                    "cost": cost,
                    "notes": f"OCO Set | TP: {tp_price}, SL: {sl_price}"
                })
                return True
            else:
                # -- OCO FALLO - VENTA DE EMERGENCIA --
                logger.error("OCO fallo. Ejecutando venta de emergencia...")
                self._emergency_sell(symbol, sell_amount, buy_price)
                return False

        except Exception as e:
            logger.error(f"Error critico en execute_buy({symbol}): {e}")
            self.notifier.send_message(
                f"<b>ERROR en compra</b>\n{symbol}: {e}"
            )
            return False

    # ------------------------------------------------------------------ #
    # Colocacion de OCO                                                   #
    # ------------------------------------------------------------------ #

    def _place_oco_order(self, symbol, amount, tp_price, sl_trigger, sl_price):
        """
        Coloca una orden OCO (One Cancels the Other) en Binance.

        La OCO consiste en DOS ordenes en el libro de Binance:
        - LIMIT_MAKER SELL al precio de TP (Take Profit) - above current price
        - STOP_LOSS_LIMIT SELL al precio de SL (Stop Loss) - below current price

        Cuando una se ejecuta, Binance cancela automaticamente la otra.

        Intenta primero el endpoint nuevo (POST /api/v3/orderList/oco)
        y hace fallback al viejo (POST /api/v3/order/oco) si falla.
        """
        market = self.exchange.market(symbol)
        error_new = None

        # -- Endpoint NUEVO: POST /api/v3/orderList/oco --
        new_params = {
            "symbol": market["id"],
            "side": "SELL",
            "quantity": str(amount),
            "aboveType": "LIMIT_MAKER",
            "abovePrice": str(tp_price),
            "belowType": "STOP_LOSS_LIMIT",
            "belowPrice": str(sl_price),
            "belowStopPrice": str(sl_trigger),
            "belowTimeInForce": "GTC",
        }

        try:
            logger.info(f"Colocando OCO (orderList/oco) para {symbol}...")
            response = self.exchange.private_post_orderlist_oco(new_params)
            oco_id = response.get("orderListId", "unknown")
            logger.info(f"OCO colocada exitosamente. ID: {oco_id}")
            logger.debug(f"Respuesta OCO: {response}")
            return True
        except Exception as e1:
            error_new = e1
            logger.warning(f"Endpoint nuevo fallo: {e1}")
            logger.info("Intentando endpoint legacy (order/oco)...")

        # -- Endpoint LEGACY: POST /api/v3/order/oco --
        legacy_params = {
            "symbol": market["id"],
            "side": "SELL",
            "quantity": str(amount),
            "price": str(tp_price),
            "stopPrice": str(sl_trigger),
            "stopLimitPrice": str(sl_price),
            "stopLimitTimeInForce": "GTC",
        }

        try:
            response = self.exchange.private_post_order_oco(legacy_params)
            oco_id = response.get("orderListId", "unknown")
            logger.info(f"OCO (legacy) colocada exitosamente. ID: {oco_id}")
            return True
        except Exception as e2:
            logger.error("Ambos endpoints OCO fallaron.")
            logger.error(f"  Nuevo (orderList/oco): {error_new}")
            logger.error(f"  Legacy (order/oco): {e2}")
            return False

    # ------------------------------------------------------------------ #
    # Venta de emergencia                                                 #
    # ------------------------------------------------------------------ #

    def _emergency_sell(self, symbol, amount, buy_price):
        """
        Venta de emergencia a mercado si la OCO no se pudo colocar.

        IMPORTANTE: si pagas fees con la moneda comprada, la cantidad `amount`
        puede ser MAYOR a lo realmente disponible. Por eso aquí vendemos el
        balance libre real (free) del activo base con un margen.
        """
        try:
            market = self.exchange.market(symbol)
            base = market["base"]

            logger.warning(f"VENTA DE EMERGENCIA: {symbol} (protegiendo posicion)")

            # Balance real disponible del activo base (free)
            balance = self.exchange.fetch_balance()
            free_base = float(balance.get(base, {}).get("free", 0) or 0)

            if free_base <= 0:
                raise Exception(f"Balance disponible de {base} es 0. No se puede vender.")

            # Margen para evitar 'insufficient balance' por fees/redondeo
            sell_amount = free_base * 0.995
            sell_amount = float(self.exchange.amount_to_precision(symbol, sell_amount))

            if sell_amount <= 0:
                raise Exception(f"Cantidad a vender redondeada es 0. free={free_base} {base}")

            logger.warning(f"VENTA DE EMERGENCIA: vendiendo {sell_amount} {base} (free={free_base})")
            sell_order = self.exchange.create_market_sell_order(symbol, sell_amount)

            sell_price = float(sell_order.get("average", sell_order.get("price", 0)) or 0)
            pnl = ((sell_price - buy_price) / buy_price) * 100 if sell_price else 0.0

            self.state.clear_position()

            self.notifier.send_message(
                f"<b>VENTA DE EMERGENCIA</b>\n"
                f"Par: <code>{symbol}</code>\n"
                f"Razon: OCO no se pudo colocar\n"
                f"Cantidad vendida: <code>{sell_amount}</code> {base}\n"
                f"Precio venta: <code>{sell_price:.8f}</code>\n"
                f"P&L: <code>{pnl:+.2f}%</code>"
            )
            log_trade_to_csv({
                    "symbol": symbol,
                    "side": "SELL",
                    "amount": amount,
                    "price": sell_price,
                    "cost": amount * sell_price,
                    "notes": "Emergency Sell"
                })
            logger.info(f"Venta de emergencia ejecutada. P&L: {pnl:+.2f}%")

        except Exception as e:
            logger.error(f"ERROR CRITICO: Venta de emergencia fallo: {e}")
            self.notifier.send_message(
                f"<b>ALERTA CRITICA</b>\n"
                f"Venta de emergencia de {symbol} FALLO.\n"
                f"Posicion abierta SIN proteccion.\n"
                f"Cantidad solicitada (orig): {amount}\n"
                f"Error: {e}\n"
                f"INTERVENCION MANUAL REQUERIDA"
            )

    # ------------------------------------------------------------------ #
    # Monitoreo de posicion abierta                                       #
    # ------------------------------------------------------------------ #

    def check_position(self):
        """
        Verifica si la OCO ya se ejecuto (TP o SL).

        Como la OCO vive en Binance, solo necesitamos verificar:
        1. Hay ordenes abiertas del par? -> OCO sigue activa, no hacer nada
        2. No hay ordenes abiertas? -> OCO se ejecuto, limpiar estado

        Este metodo se llama cada minuto desde el cron.
        """
        position = self.state.get_position()
        if not position:
            return

        symbol = position["symbol"]

        try:
            # Verificar si hay ordenes abiertas para este par
            open_orders = self.exchange.fetch_open_orders(symbol)

            if len(open_orders) > 0:
                # OCO sigue activa - todo normal
                #logger.info(
                #   f"[{symbol}] OCO activa ({len(open_orders)} ordenes). "
                #   f"TP={position['tp_price']:.8f} | "
                #   f"SL={position['sl_trigger']:.8f}"
                #)
                return

            # No hay ordenes abiertas - la OCO se ejecuto
            logger.info(
                f"[{symbol}] No hay ordenes abiertas. Verificando resultado..."
            )

            # Determinar si fue TP o SL
            result_msg = self._determine_exit_result(symbol, position)

            # Limpiar estado local
            self.state.clear_position()

            # Notificar resultado
            if result_msg:
                self.notifier.send_message(result_msg)

        except Exception as e:
            logger.error(f"Error verificando posicion de {symbol}: {e}")

    def _determine_exit_result(self, symbol, position):
        """
        Determina si la OCO se ejecuto por TP o SL analizando trades recientes.
        """
        buy_price = position["buy_price"]
        tp_price = position["tp_price"]
        sl_trigger = position["sl_trigger"]

        try:
            # Consultar ultimos trades del par
            trades = self.exchange.fetch_my_trades(symbol, limit=5)

            if not trades:
                logger.warning(
                    f"[{symbol}] No se encontraron trades recientes."
                )
                return (
                    f"<b>POSICION CERRADA</b>\n"
                    f"Par: <code>{symbol}</code>\n"
                    f"No se pudieron determinar los detalles."
                )

            # Filtrar trades de venta
            sell_trades = [t for t in trades if t["side"] == "sell"]

            if not sell_trades:
                logger.warning(
                    f"[{symbol}] No hay trades de venta recientes."
                )
                return (
                    f"<b>POSICION CERRADA</b>\n"
                    f"Par: <code>{symbol}</code>\n"
                    f"Sin trades de venta detectados. Verificar manualmente."
                )

            last_sell = sell_trades[-1]
            sell_price = float(last_sell["price"])
            pnl_pct = ((sell_price - buy_price) / buy_price) * 100
            pnl_usd = (sell_price - buy_price) * position.get("amount", 0)

            # Determinar si fue TP o SL por cercania al precio objetivo
            dist_to_tp = abs(sell_price - tp_price)
            dist_to_sl = abs(sell_price - sl_trigger)

            if dist_to_tp < dist_to_sl:
                exit_type = "TAKE PROFIT"
            else:
                exit_type = "STOP LOSS"

            # Si fue SL, activar cooldown para no re-entrar inmediatamente
            if exit_type == "STOP LOSS":
                self.state.set_cooldown(symbol, minutes=90)

            msg = (
                f"<b>{exit_type} EJECUTADO</b>\n"
                f"Par: <code>{symbol}</code>\n"
                f"Compra: <code>{buy_price:.8f}</code>\n"
                f"Venta: <code>{sell_price:.8f}</code>\n"
                f"P&L: <code>{pnl_pct:+.2f}%</code> (${pnl_usd:+.2f})"
            )

            logger.info(f"[{symbol}] {exit_type}: P&L = {pnl_pct:+.2f}%")
            
            # >>> AÑADE ESTO, AQUÍ ES DONDE SE REGISTRAN LAS VENTAS <<<
            log_trade_to_csv({
                "symbol": symbol,
                "side": "SELL",
                "amount": float(last_sell.get("amount", position.get("amount", 0))),
                "price": sell_price,
                "cost": sell_price * float(last_sell.get("amount", position.get("amount", 0))),
                "notes": exit_type,
                "pnl": f"{pnl_usd:.2f}",
                "pnl_pct": f"{pnl_pct:.2f}"
            })
            
            return msg

        except Exception as e:
            logger.error(
                f"Error determinando resultado de {symbol}: {e}"
            )
            return (
                f"<b>POSICION CERRADA</b>\n"
                f"Par: <code>{symbol}</code>\n"
                f"Error al consultar resultado: {e}"
            )

    # ------------------------------------------------------------------ #
    # Utilidades de emergencia                                            #
    # ------------------------------------------------------------------ #

    def cancel_oco_if_exists(self, symbol):
        """
        Cancela todas las ordenes abiertas del par (incluyendo OCO).
        Usar solo en emergencias o para resetear el estado.
        """
        try:
            open_orders = self.exchange.fetch_open_orders(symbol)
            if not open_orders:
                logger.info(
                    f"[{symbol}] No hay ordenes abiertas para cancelar."
                )
                return True

            count = len(open_orders)
            logger.warning(
                f"[{symbol}] Cancelando {count} ordenes abiertas..."
            )

            # cancel_all_orders cancela TODO incluyendo OCOs
            self.exchange.cancel_all_orders(symbol)
            logger.info(f"[{symbol}] Todas las ordenes canceladas.")

            self.notifier.send_message(
                f"<b>ORDENES CANCELADAS</b>\n"
                f"Par: <code>{symbol}</code>\n"
                f"Se cancelaron {count} ordenes."
            )
            return True

        except Exception as e:
            logger.error(f"Error cancelando ordenes de {symbol}: {e}")
            return False

    def verify_position_integrity(self):
        """
        Verificacion de integridad: compara estado local vs Binance real.
        Si el estado dice que hay posicion pero no hay balance, limpia el estado.
        Util si el bot se reinicio y perdio sincronizacion.
        """
        position = self.state.get_position()
        if not position:
            return

        symbol = position["symbol"]
        base_currency = symbol.split("/")[0]  # Ej: 'BTC' de 'BTC/USDT'

        try:
            balance = self.exchange.fetch_balance()
            base_total = float(
                balance.get(base_currency, {}).get("total", 0)
            )

            # Si no hay balance significativo, la posicion ya se cerro
            min_value = position.get("amount", 0) * 0.1  # 10% del original
            if base_total < min_value:
                logger.warning(
                    f"[{symbol}] Estado inconsistente: posicion en state.json "
                    f"pero balance de {base_currency} es {base_total:.8f}. "
                    f"Limpiando."
                )
                self.state.clear_position()
                self.notifier.send_message(
                    f"<b>ESTADO CORREGIDO</b>\n"
                    f"Par: <code>{symbol}</code>\n"
                    f"Posicion cerrada pero estado no se actualizo.\n"
                    f"Balance {base_currency}: {base_total:.8f}"
                )
            else:
                logger.info(
                    f"[{symbol}] Integridad OK: "
                    f"balance {base_currency} = {base_total:.8f}"
                )

        except Exception as e:
            logger.error(
                f"Error verificando integridad de {symbol}: {e}"
            )
