"""
CV6 AI Trading OS
Order Manager
Uses the shared broker singleton from broker_api so execution
always routes through the authenticated broker connection.
"""

import time

from loguru import logger

from app.execution.execution_models import (
    ExecutionRequest,
    ExecutionResponse,
)
from app.execution import idempotency, fill_tracker, position_guard


class OrderManager:

    def __init__(self):
        # Use the same connected singleton, not a fresh unauthenticated instance
        from app.api.broker_api import manager as _shared_manager
        self.broker_manager = _shared_manager

    def place_order(
        self,
        request: ExecutionRequest,
    ) -> ExecutionResponse:

        # ── PHASE-2 (Goals 1 & 3): idempotency / duplicate-order guard ─────
        # Check an explicit caller-supplied key first, then fall back to an
        # implicit fingerprint of the order's identity within a short window,
        # so retries/double-clicks/re-fired signals can't place two real
        # orders for the same intent. PHASE-2.5: this is now DB-backed and
        # survives a server restart (see app/execution/idempotency.py).
        dedup_key = (
            idempotency.explicit_key(request.idempotency_key)
            if request.idempotency_key
            else idempotency.fingerprint(
                request.broker, request.symbol, request.exchange,
                request.side.value, request.order_type.value,
                request.product.value, request.quantity, request.price or 0,
            )
        )
        cached = idempotency.get_cached(dedup_key)
        if cached is not None:
            return ExecutionResponse(**cached)

        broker = self.broker_manager.get_broker(request.broker)

        if broker is None:
            response = ExecutionResponse(
                success=False,
                message="No broker connected. Connect a broker first.",
                broker=request.broker,
                order_id=None,
                status="FAILED",
                symbol=request.symbol,
                side=request.side.value,
                quantity=request.quantity,
                fill_status="REJECTED",
            )
            idempotency.store(dedup_key, response)
            return response

        # ── PHASE-2.5 (Goal 5): one-symbol-one-position guard ───────────────
        # This path (unlike the autonomous engine) keeps no local position
        # ledger, so the broker's own live position book is the only source
        # of truth available to check against.
        existing_qty = position_guard.get_net_position_qty(broker, request.symbol, request.exchange)
        if position_guard.blocks_new_entry(existing_qty, request.side.value):
            response = ExecutionResponse(
                success=False,
                message=(
                    f"{request.symbol} already has an open "
                    f"{position_guard.describe_existing(existing_qty)} position — "
                    f"one-symbol-one-position rule blocks another {request.side.value}. "
                    f"Place an opposite-side order to reduce/close it instead."
                ),
                broker=request.broker,
                order_id=None,
                status="BLOCKED",
                symbol=request.symbol,
                side=request.side.value,
                quantity=request.quantity,
                fill_status="REJECTED",
            )
            idempotency.store(dedup_key, response)
            return response

        # ── RISK ENFORCEMENT on the manual/execution path ───────────────────
        # Previously this path bypassed risk_guard entirely (it only gated the
        # autonomous engine), so a manual order could be placed after the kill
        # switch fired, outside market hours, or past the daily-loss limit.
        # Now every real order clears the same gate. Capital/deployed come from
        # the source of truth; symbol-specific inputs (change_pct/vix/news) are
        # neutral defaults so this enforces the account-level layers (kill
        # switch, loss limits, drawdown, market hours) without false blocks.
        try:
            from app.risk_guard.risk_guard import risk_guard
            from app.state.trading_state_manager import trading_state_manager
            cap = trading_state_manager.snapshot()["capital"]
            verdict = risk_guard.check_entry(
                symbol=request.symbol, sector="Other", ltp=request.price or 0.0,
                change_pct=0.0, vix=0.0, news_sentiment="NEUTRAL", open_positions=[],
                capital=cap["total"], deployed=cap["deployed"], mode="REAL",
                exchange=request.exchange,
            )
            if not verdict.allowed:
                response = ExecutionResponse(
                    success=False,
                    message=f"Risk blocked [{verdict.layer}]: {verdict.reason}",
                    broker=request.broker, order_id=None, status="BLOCKED",
                    symbol=request.symbol, side=request.side.value,
                    quantity=request.quantity, fill_status="REJECTED",
                )
                idempotency.store(dedup_key, response)
                return response
        except Exception as _rg_err:
            logger.warning(f"[OrderManager] risk_guard check skipped: {_rg_err}")

        # ── Build normalised order dict — broker's place_order() handles
        #    symbol-token lookup and field mapping internally. ─────────────
        order_dict = {
            "variety":         "NORMAL",
            "tradingsymbol":   request.symbol,
            "transactiontype": request.side.value,
            "exchange":        request.exchange,
            "ordertype":       request.order_type.value,
            "producttype":     request.product.value,
            "duration":        "DAY",
            "price":           request.price or 0,
            "quantity":        request.quantity,
            "triggerprice":    request.trigger_price or 0,
        }

        attempt_ts = time.time()
        result = broker.place_order(order_dict)
        order_id = str(result.get("order_id") or "")
        accepted = bool(result.get("success", False))

        # ── PHASE-2.5 (Goal 2): "accepted" != "filled" ──────────────────────
        # Never trust the immediate response as a fill confirmation — poll
        # the broker's own order book until a final state is reached (or a
        # bounded timeout), and report the ACTUAL fill status/quantity.
        fill = None
        if accepted and order_id:
            fill = fill_tracker.poll_until_final(
                broker, order_id=order_id, symbol=request.symbol,
                side=request.side.value, quantity=request.quantity, since_ts=attempt_ts,
            )
        elif not accepted and fill_tracker.looks_ambiguous(result.get("message", "")):
            # Broker call failed with a network/timeout-shaped error — the
            # exchange may have actually received the order. Reconcile
            # against the order book by symbol+side+qty+recency before
            # concluding it was really rejected.
            logger.warning(
                f"[OrderManager] Ambiguous failure for {request.symbol} "
                f"({result.get('message')}) — reconciling against order book"
            )
            fill = fill_tracker.poll_until_final(
                broker, order_id="", symbol=request.symbol,
                side=request.side.value, quantity=request.quantity, since_ts=attempt_ts,
                max_wait_secs=8.0,
            )
            if fill.status != "UNKNOWN":
                order_id = fill.order_id or order_id
                accepted = fill.status in ("FILLED", "PARTIALLY_FILLED", "PENDING")

        if fill is None:
            fill_status = "REJECTED" if not accepted else "UNKNOWN"
            filled_qty, avg_price = 0, 0.0
        else:
            fill_status, filled_qty, avg_price = fill.status, fill.filled_qty, fill.avg_price

        # ── TIER-1: place a broker-native STOPLOSS_MARKET order after the
        # entry fills, so the position has server-side downside protection
        # even if this process crashes or the price feed stalls. Target stays
        # software-managed. On any non-SL close the caller cancels sl_order_id.
        sl_order_id, sl_protected = "", False
        if fill_status in ("FILLED", "PARTIALLY_FILLED") and request.stop_loss and request.stop_loss > 0:
            sl_qty = filled_qty if filled_qty > 0 else request.quantity
            sl_order_id = self._place_stop_loss(broker, request, sl_qty)
            sl_protected = bool(sl_order_id)
            if not sl_protected:
                logger.critical(
                    f"[OrderManager] ⚠ ENTRY FILLED BUT STOP-LOSS FAILED for "
                    f"{request.symbol} qty={sl_qty} @SL {request.stop_loss} — "
                    f"position is UNPROTECTED at the broker; monitor loop must retry."
                )

        response = ExecutionResponse(
            success=accepted,
            message=result.get("message", "Order placed" if accepted else "Order failed"),
            broker=request.broker,
            order_id=order_id,
            status="PLACED" if accepted else "REJECTED",
            symbol=request.symbol,
            side=request.side.value,
            quantity=request.quantity,
            fill_status=fill_status,
            filled_quantity=filled_qty,
            avg_fill_price=avg_price,
            sl_order_id=sl_order_id,
            sl_protected=sl_protected,
        )
        idempotency.store(dedup_key, response)

        # ── SOURCE OF TRUTH: a confirmed real fill on the manual/execution
        # path emits ONE event to the TradingStateManager, so manual orders
        # update the same authoritative positions/capital/PnL/journal as
        # autonomous ones (this path previously kept no position ledger at all).
        if fill_status in ("FILLED", "PARTIALLY_FILLED"):
            try:
                from app.state.trading_state_manager import trading_state_manager
                from app.state.trading_events import TradingEvent, EventType
                trading_state_manager.apply_event(TradingEvent(
                    event_type=EventType.ORDER_FILLED, symbol=request.symbol,
                    broker=request.broker, exchange=request.exchange,
                    side=request.side.value, order_id=order_id,
                    quantity=request.quantity, filled_quantity=filled_qty,
                    avg_price=avg_price, fill_status=fill_status,
                    style=request.product.value, stop_loss=request.stop_loss or 0.0,
                    target_price=request.target_price or 0.0, sl_order_id=sl_order_id,
                    mode="REAL", source="execution_api",
                ))
            except Exception as _e:
                logger.error(f"[OrderManager] TSM emit failed: {_e}")
        return response

    # ── TIER-1: broker-native stop-loss helpers ─────────────────────────────
    def _place_stop_loss(self, broker, request: ExecutionRequest, qty: int) -> str:
        """
        Place a broker-native STOPLOSS_MARKET order protecting a just-filled
        position. Opposite side, trigger at request.stop_loss. Returns the
        broker SL order id on success, "" on failure.
        """
        exit_side = "SELL" if request.side.value == "BUY" else "BUY"
        sl_order = {
            "variety":         "STOPLOSS",          # Angel One requires STOPLOSS variety for SL orders
            "tradingsymbol":   request.symbol,
            "transactiontype": exit_side,
            "exchange":        request.exchange,
            "ordertype":       "SL-M",              # → STOPLOSS_MARKET in the broker layer
            "producttype":     request.product.value,
            "duration":        "DAY",
            "price":           0,
            "quantity":        qty,
            "triggerprice":    request.stop_loss,
        }
        try:
            res = broker.place_order(sl_order)
            if res.get("success"):
                sl_id = str(res.get("order_id") or "")
                logger.info(
                    f"[OrderManager] SL-M placed for {request.symbol}: id={sl_id} "
                    f"{exit_side} {qty} trigger={request.stop_loss}"
                )
                return sl_id
            logger.error(f"[OrderManager] SL-M rejected for {request.symbol}: {res.get('message')}")
        except Exception as e:
            logger.error(f"[OrderManager] SL-M exception for {request.symbol}: {e}")
        return ""

    def cancel_stop_loss(self, broker_name: str, sl_order_id: str) -> dict:
        """Cancel an outstanding broker-side SL order (called when a position
        closes for a non-SL reason: target hit, EOD, or manual exit)."""
        if not sl_order_id:
            return {"success": True, "message": "no SL order to cancel"}
        broker = self.broker_manager.get_broker(broker_name)
        if broker is None:
            return {"success": False, "message": "broker not connected"}
        try:
            return broker.cancel_order(sl_order_id)
        except Exception as e:
            logger.warning(f"[OrderManager] cancel SL {sl_order_id} failed: {e}")
            return {"success": False, "message": str(e)}

    def modify_order(
        self,
        broker_name: str,
        order_id: str,
        **kwargs,
    ):

        broker = self.broker_manager.get_broker(
            broker_name
        )

        return broker.modify_order(
            order_id=order_id,
            **kwargs,
        )

    def cancel_order(
        self,
        broker_name: str,
        order_id: str,
    ):

        broker = self.broker_manager.get_broker(
            broker_name
        )

        return broker.cancel_order(
            order_id=order_id,
        )

    def order_book(
        self,
        broker_name: str,
    ):

        broker = self.broker_manager.get_broker(
            broker_name
        )

        return broker.order_book()