"""Paper trading adapter - local order simulation and state sync.

Implements a fully local paper trading engine that simulates order
execution without requiring a real broker API. Tracks positions,
account balance, fills, and order events in-memory with PostgreSQL
persistence for state sync and audit trail.

Requirements: 8.1, 8.3, 8.5, 2.4
Design: Section 4.9 - Broker Adapter (paper mode)
"""
from __future__ import annotations

import json
import logging
import uuid
from datetime import datetime, timezone
from typing import Any

import asyncpg

from services.adapters.base import AdapterResult
from services.adapters.broker_adapter import (
    AccountInfo,
    BrokerDataAdapter,
    OrderEventType,
    OrderRequest,
    OrderResponse,
    OrderSide,
    OrderStatus,
    OrderType,
    PositionInfo,
    TradingMode,
)

logger = logging.getLogger("paper_trading")


# ---------------------------------------------------------------------------
# In-memory paper trading state
# ---------------------------------------------------------------------------


class PaperPosition:
    """Tracks a single paper position."""

    def __init__(
        self,
        ticker: str,
        quantity: float = 0.0,
        avg_entry_price: float = 0.0,
        realized_pnl: float = 0.0,
    ) -> None:
        self.ticker = ticker
        self.quantity = quantity
        self.avg_entry_price = avg_entry_price
        self.realized_pnl = realized_pnl

    def apply_fill(self, side: OrderSide, fill_qty: float, fill_price: float) -> float:
        """Apply a fill to this position. Returns realized PnL from the fill."""
        realized = 0.0

        if side == OrderSide.BUY:
            # Buying: average up the entry price
            total_cost = self.avg_entry_price * self.quantity + fill_price * fill_qty
            self.quantity += fill_qty
            if self.quantity > 0:
                self.avg_entry_price = total_cost / self.quantity
        else:
            # Selling: realize PnL on the sold shares
            if self.quantity > 0:
                sell_qty = min(fill_qty, self.quantity)
                realized = sell_qty * (fill_price - self.avg_entry_price)
                self.quantity -= sell_qty
                self.realized_pnl += realized
            if self.quantity <= 0:
                self.quantity = 0.0
                self.avg_entry_price = 0.0

        return realized

    @property
    def is_open(self) -> bool:
        return self.quantity > 0

    def to_position_info(self, current_price: float | None = None) -> PositionInfo:
        """Convert to a PositionInfo for the broker interface."""
        price = current_price if current_price is not None else self.avg_entry_price
        unrealized = (price - self.avg_entry_price) * self.quantity if self.quantity > 0 else 0.0
        market_value = price * self.quantity
        return PositionInfo(
            ticker=self.ticker,
            quantity=self.quantity,
            avg_entry_price=self.avg_entry_price,
            current_price=price,
            unrealized_pnl=round(unrealized, 4),
            market_value=round(market_value, 4),
            side="long" if self.quantity > 0 else "flat",
        )


class PaperAccount:
    """In-memory paper trading account state."""

    def __init__(
        self,
        account_id: str = "paper-default",
        initial_cash: float = 100_000.0,
    ) -> None:
        self.account_id = account_id
        self.initial_cash = initial_cash
        self.cash = initial_cash
        self.positions: dict[str, PaperPosition] = {}
        self.orders: dict[str, OrderResponse] = {}
        self.order_events: list[dict[str, Any]] = []
        self._seen_idempotency_keys: dict[str, str] = {}  # key -> order_id

    @property
    def portfolio_value(self) -> float:
        position_value = sum(
            p.quantity * p.avg_entry_price for p in self.positions.values() if p.is_open
        )
        return self.cash + position_value

    @property
    def buying_power(self) -> float:
        return self.cash

    def get_position(self, ticker: str) -> PaperPosition:
        if ticker not in self.positions:
            self.positions[ticker] = PaperPosition(ticker=ticker)
        return self.positions[ticker]

    def to_account_info(self) -> AccountInfo:
        return AccountInfo(
            account_id=self.account_id,
            buying_power=round(self.buying_power, 2),
            cash=round(self.cash, 2),
            portfolio_value=round(self.portfolio_value, 2),
            currency="USD",
            mode=TradingMode.PAPER,
        )


# ---------------------------------------------------------------------------
# Paper trading adapter
# ---------------------------------------------------------------------------


class PaperTradingAdapter(BrokerDataAdapter):
    """Local paper trading adapter that simulates order execution.

    All orders are filled immediately at the estimated price (market orders)
    or at the limit/stop price when applicable. No real broker API is called.

    Features:
    - Idempotent order submission via idempotency_key (Req 8.5)
    - Full order event trail for audit (Req 8.3)
    - Position tracking with average entry price
    - Cash balance management
    - State sync to/from PostgreSQL

    The adapter operates in PAPER mode only and rejects any attempt
    to switch to LIVE mode.
    """

    def __init__(
        self,
        account_id: str = "paper-default",
        initial_cash: float = 100_000.0,
        simulated_slippage_pct: float = 0.001,
    ) -> None:
        super().__init__(mode=TradingMode.PAPER)
        self.account = PaperAccount(account_id=account_id, initial_cash=initial_cash)
        self.slippage_pct = simulated_slippage_pct

    def source_type(self) -> str:
        return "broker"

    async def fetch(self, ticker: str, config: dict[str, Any]) -> AdapterResult:
        """Fetch paper positions/account as a raw artifact snapshot."""
        endpoint = config.get("endpoint", "positions")
        now = datetime.now(timezone.utc)

        if endpoint == "account":
            data = self.account.to_account_info().to_dict()
            items = [data]
        elif endpoint == "orders":
            items = [
                resp.to_dict()
                for resp in self.account.orders.values()
                if resp.ticker == ticker or ticker == "*"
            ]
        else:
            pos = self.account.get_position(ticker)
            data = pos.to_position_info().to_dict()
            items = [data] if pos.is_open else []

        raw = json.dumps(items).encode()
        return AdapterResult(
            source_type="broker",
            ticker=ticker,
            items=items,
            raw_payload=raw,
            content_hash="",
            fetched_at=now,
            metadata={"provider": "paper", "mode": "paper", "endpoint": endpoint},
        )

    async def submit_order(self, order: OrderRequest) -> OrderResponse:
        """Simulate order submission and immediate fill.

        Idempotency: if the same idempotency_key was already used,
        return the original response (Req 8.5).
        """
        # Idempotency check
        existing_id = self.account._seen_idempotency_keys.get(order.idempotency_key)
        if existing_id and existing_id in self.account.orders:
            logger.info("Duplicate order key %s — returning cached response", order.idempotency_key)
            return self.account.orders[existing_id]

        now = datetime.now(timezone.utc)
        order_id = str(uuid.uuid4())

        # Determine fill price based on order type
        fill_price = self._compute_fill_price(order)

        # Check if we have enough cash for buys
        if order.side == OrderSide.BUY:
            required_cash = fill_price * order.quantity
            if required_cash > self.account.cash:
                resp = OrderResponse(
                    broker_order_id=order_id,
                    status=OrderStatus.REJECTED,
                    ticker=order.ticker,
                    side=order.side,
                    quantity=order.quantity,
                    submitted_at=now,
                    error=f"Insufficient cash: need {required_cash:.2f}, have {self.account.cash:.2f}",
                )
                self._record_event(order_id, OrderEventType.REJECTED, resp.to_dict(), now)
                self.account.orders[order_id] = resp
                self.account._seen_idempotency_keys[order.idempotency_key] = order_id
                return resp

        # Check if we have enough shares for sells
        if order.side == OrderSide.SELL:
            pos = self.account.get_position(order.ticker)
            if pos.quantity < order.quantity:
                resp = OrderResponse(
                    broker_order_id=order_id,
                    status=OrderStatus.REJECTED,
                    ticker=order.ticker,
                    side=order.side,
                    quantity=order.quantity,
                    submitted_at=now,
                    error=f"Insufficient shares: need {order.quantity}, have {pos.quantity}",
                )
                self._record_event(order_id, OrderEventType.REJECTED, resp.to_dict(), now)
                self.account.orders[order_id] = resp
                self.account._seen_idempotency_keys[order.idempotency_key] = order_id
                return resp

        # Simulate immediate fill
        position = self.account.get_position(order.ticker)
        realized_pnl = position.apply_fill(order.side, order.quantity, fill_price)

        # Update cash
        if order.side == OrderSide.BUY:
            self.account.cash -= fill_price * order.quantity
        else:
            self.account.cash += fill_price * order.quantity

        resp = OrderResponse(
            broker_order_id=order_id,
            status=OrderStatus.FILLED,
            ticker=order.ticker,
            side=order.side,
            quantity=order.quantity,
            filled_quantity=order.quantity,
            filled_avg_price=fill_price,
            submitted_at=now,
            raw_response={
                "realized_pnl": round(realized_pnl, 4),
                "cash_after": round(self.account.cash, 2),
                "position_qty_after": position.quantity,
                "simulated": True,
            },
        )

        # Record events
        self._record_event(order_id, OrderEventType.SUBMITTED, {"ticker": order.ticker}, now)
        self._record_event(order_id, OrderEventType.ACCEPTED, {"ticker": order.ticker}, now)
        self._record_event(order_id, OrderEventType.FILL, {
            "fill_price": fill_price,
            "fill_qty": order.quantity,
            "realized_pnl": round(realized_pnl, 4),
        }, now)

        self.account.orders[order_id] = resp
        self.account._seen_idempotency_keys[order.idempotency_key] = order_id

        logger.info(
            "Paper fill: %s %s %.0f %s @ %.2f | cash=%.2f pnl=%.4f",
            order_id[:8], order.side.value, order.quantity,
            order.ticker, fill_price, self.account.cash, realized_pnl,
        )

        return resp

    async def cancel_order(self, broker_order_id: str) -> OrderResponse:
        """Cancel a paper order. Only pending orders can be cancelled."""
        existing = self.account.orders.get(broker_order_id)
        if existing is None:
            return OrderResponse(
                broker_order_id=broker_order_id,
                status=OrderStatus.REJECTED,
                ticker="",
                side=OrderSide.BUY,
                quantity=0,
                error=f"Order {broker_order_id} not found",
            )

        # Paper orders fill immediately, so they can't be cancelled
        if existing.status == OrderStatus.FILLED:
            return OrderResponse(
                broker_order_id=broker_order_id,
                status=OrderStatus.REJECTED,
                ticker=existing.ticker,
                side=existing.side,
                quantity=existing.quantity,
                error="Cannot cancel a filled order",
            )

        now = datetime.now(timezone.utc)
        cancelled = OrderResponse(
            broker_order_id=broker_order_id,
            status=OrderStatus.CANCELLED,
            ticker=existing.ticker,
            side=existing.side,
            quantity=existing.quantity,
            submitted_at=existing.submitted_at,
        )
        self.account.orders[broker_order_id] = cancelled
        self._record_event(broker_order_id, OrderEventType.CANCELLED, {}, now)
        return cancelled

    async def get_order_status(self, broker_order_id: str) -> OrderResponse:
        """Get the status of a paper order."""
        existing = self.account.orders.get(broker_order_id)
        if existing is None:
            return OrderResponse(
                broker_order_id=broker_order_id,
                status=OrderStatus.REJECTED,
                ticker="",
                side=OrderSide.BUY,
                quantity=0,
                error=f"Order {broker_order_id} not found",
            )
        return existing

    async def get_positions(self) -> list[PositionInfo]:
        """Get all open paper positions."""
        return [
            p.to_position_info()
            for p in self.account.positions.values()
            if p.is_open
        ]

    async def get_account(self) -> AccountInfo:
        """Get paper account summary."""
        return self.account.to_account_info()

    # -----------------------------------------------------------------------
    # Internal helpers
    # -----------------------------------------------------------------------

    def _compute_fill_price(self, order: OrderRequest) -> float:
        """Determine the simulated fill price for an order.

        Market orders use the limit_price as a proxy (or 0 if not set).
        Limit orders fill at the limit price.
        Stop orders fill at the stop price.
        A small slippage is applied to market orders.
        """
        if order.order_type == OrderType.LIMIT and order.limit_price is not None:
            return order.limit_price
        if order.order_type == OrderType.STOP and order.stop_price is not None:
            return order.stop_price
        if order.order_type == OrderType.STOP_LIMIT and order.limit_price is not None:
            return order.limit_price

        # Market order: use limit_price as estimate, or a default
        base_price = order.limit_price if order.limit_price is not None else 100.0
        if order.side == OrderSide.BUY:
            return round(base_price * (1 + self.slippage_pct), 4)
        return round(base_price * (1 - self.slippage_pct), 4)

    def _record_event(
        self,
        order_id: str,
        event_type: OrderEventType,
        data: dict[str, Any],
        timestamp: datetime,
    ) -> None:
        """Record an order event for audit trail."""
        self.account.order_events.append({
            "order_id": order_id,
            "event_type": event_type.value,
            "data": data,
            "timestamp": timestamp.isoformat(),
        })


# ---------------------------------------------------------------------------
# State sync: persist and restore paper trading state to/from PostgreSQL
# ---------------------------------------------------------------------------

# SQL for persisting paper orders to the orders table
_INSERT_PAPER_ORDER = """
INSERT INTO orders (
    id, recommendation_id, broker_account_id, ticker, side, order_type,
    quantity, limit_price, stop_price, status, idempotency_key,
    broker_order_id, decision_trace, submitted_at, filled_at,
    fill_price, fill_quantity
) VALUES (
    $1::uuid, $2, $3, $4, $5, $6,
    $7, $8, $9, $10, $11,
    $12, $13::jsonb, $14, $15,
    $16, $17
)
ON CONFLICT (idempotency_key) DO NOTHING
"""

_INSERT_PAPER_ORDER_EVENT = """
INSERT INTO order_events (order_id, event_type, data, broker_timestamp)
VALUES ($1::uuid, $2, $3::jsonb, $4)
"""

_UPSERT_PAPER_POSITION = """
INSERT INTO positions (broker_account_id, ticker, quantity, avg_entry_price, realized_pnl, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (broker_account_id, ticker)
    DO UPDATE SET
        quantity = EXCLUDED.quantity,
        avg_entry_price = EXCLUDED.avg_entry_price,
        realized_pnl = EXCLUDED.realized_pnl,
        updated_at = EXCLUDED.updated_at
"""

_UPSERT_PAPER_ACCOUNT = """
INSERT INTO broker_accounts (id, provider, account_id, mode, config, active)
VALUES ($1::uuid, 'paper', $2, 'paper', $3::jsonb, TRUE)
ON CONFLICT (id) DO UPDATE SET
    config = EXCLUDED.config,
    active = TRUE
"""

_LOAD_PAPER_POSITIONS = """
SELECT ticker, quantity, avg_entry_price, COALESCE(realized_pnl, 0) AS realized_pnl
FROM positions
WHERE broker_account_id = $1 AND quantity > 0
"""

_LOAD_PAPER_ACCOUNT_CONFIG = """
SELECT config FROM broker_accounts
WHERE account_id = $1 AND mode = 'paper' AND active = TRUE
LIMIT 1
"""

_LOAD_PAPER_ORDERS = """
SELECT
    id, ticker, side, order_type, quantity, status,
    idempotency_key, broker_order_id, fill_price, fill_quantity,
    submitted_at
FROM orders
WHERE broker_account_id = (
    SELECT id FROM broker_accounts WHERE account_id = $1 AND mode = 'paper' LIMIT 1
)
ORDER BY submitted_at DESC
LIMIT 500
"""


async def sync_state_to_db(
    adapter: PaperTradingAdapter,
    pool: asyncpg.Pool,
    broker_account_uuid: str | None = None,
) -> None:
    """Persist the current paper trading state to PostgreSQL.

    Writes:
    - broker_accounts row for the paper account
    - positions rows for all open positions
    - orders rows for all orders (idempotent via ON CONFLICT)
    - order_events for audit trail

    This enables state recovery after restarts and provides the
    full execution audit trail (Requirement 8.3).
    """
    acct = adapter.account
    now = datetime.now(timezone.utc)
    acct_uuid = broker_account_uuid or str(uuid.uuid5(uuid.NAMESPACE_DNS, acct.account_id))

    async with pool.acquire() as conn:
        async with conn.transaction():
            # 1. Upsert broker account
            config_json = json.dumps({
                "initial_cash": acct.initial_cash,
                "current_cash": round(acct.cash, 2),
                "portfolio_value": round(acct.portfolio_value, 2),
                "slippage_pct": adapter.slippage_pct,
            })
            await conn.execute(_UPSERT_PAPER_ACCOUNT, acct_uuid, acct.account_id, config_json)

            # 2. Upsert positions
            for ticker, pos in acct.positions.items():
                await conn.execute(
                    _UPSERT_PAPER_POSITION,
                    acct_uuid, ticker,
                    pos.quantity, pos.avg_entry_price, pos.realized_pnl,
                    now,
                )

            # 3. Insert orders (idempotent)
            for order_id, resp in acct.orders.items():
                filled_at = now if resp.status == OrderStatus.FILLED else None
                await conn.execute(
                    _INSERT_PAPER_ORDER,
                    order_id,
                    None,  # recommendation_id
                    acct_uuid,
                    resp.ticker,
                    resp.side.value,
                    "market",  # paper orders are always market-simulated
                    resp.quantity,
                    resp.filled_avg_price,  # limit_price
                    None,  # stop_price
                    resp.status.value,
                    order_id,  # use order_id as idempotency_key fallback
                    order_id,
                    json.dumps(resp.raw_response),
                    resp.submitted_at,
                    filled_at,
                    resp.filled_avg_price,
                    resp.filled_quantity,
                )

            # 4. Insert order events
            for event in acct.order_events:
                await conn.execute(
                    _INSERT_PAPER_ORDER_EVENT,
                    event["order_id"],
                    event["event_type"],
                    json.dumps(event["data"]),
                    datetime.fromisoformat(event["timestamp"]),
                )

    logger.info(
        "Synced paper state to DB: account=%s positions=%d orders=%d events=%d",
        acct.account_id, len(acct.positions), len(acct.orders), len(acct.order_events),
    )

    # Clear events after sync to avoid re-inserting
    acct.order_events.clear()


async def load_state_from_db(
    adapter: PaperTradingAdapter,
    pool: asyncpg.Pool,
) -> bool:
    """Restore paper trading state from PostgreSQL.

    Loads positions and account config from the DB so the adapter
    can resume after a restart. Returns True if state was found.
    """
    acct = adapter.account

    async with pool.acquire() as conn:
        # Load account config
        row = await conn.fetchrow(_LOAD_PAPER_ACCOUNT_CONFIG, acct.account_id)
        if row is None:
            logger.info("No saved paper account state for %s", acct.account_id)
            return False

        config = json.loads(row["config"]) if isinstance(row["config"], str) else row["config"]
        acct.cash = float(config.get("current_cash", acct.initial_cash))

        # Load positions
        pos_rows = await conn.fetch(_LOAD_PAPER_POSITIONS, acct.account_id)
        for pr in pos_rows:
            ticker = pr["ticker"]
            acct.positions[ticker] = PaperPosition(
                ticker=ticker,
                quantity=float(pr["quantity"]),
                avg_entry_price=float(pr["avg_entry_price"] or 0),
                realized_pnl=float(pr["realized_pnl"]),
            )

    logger.info(
        "Loaded paper state from DB: account=%s cash=%.2f positions=%d",
        acct.account_id, acct.cash, len(acct.positions),
    )
    return True
