"""MockBroker: simulates Alpaca Paper Trading API using Oracle historical bar data.

Used by simulate_2week.py and the `fithia2 paper backtest` command.
"""
from __future__ import annotations

import asyncio
import datetime as dt
from typing import Any

from apps.paper_trader.alpaca_broker import AccountInfo, Bar, Order, Position


class MockBroker:
    """Simulates Alpaca Paper Trading API using Oracle historical bar data.

    Must call set_sim_context(date, phase) before each phase ('open' or 'close').
    """

    def __init__(self, initial_equity: float, bars_cache: dict[str, dict[dt.date, dict]]) -> None:
        self._initial_equity = initial_equity
        self._cash = float(initial_equity)
        self._positions: dict[str, dict] = {}  # symbol -> {qty, avg_price, current_price}
        self._bars = bars_cache  # pre-fetched: symbol -> {date -> {open,high,low,close,volume}}
        self._sim_date: dt.date | None = None
        self._sim_phase: str = "close"  # 'open' or 'close'
        self._order_counter = 0
        self._trade_log: list[dict] = []

    # ── Context ────────────────────────────────────────────────────────────────

    def set_sim_context(self, date: dt.date, phase: str) -> None:
        """Set current simulation date + phase before each engine call."""
        self._sim_date = date
        self._sim_phase = phase
        # Refresh position current prices using the appropriate price field for the phase
        price_field = "open" if phase == "open" else "close"
        for sym, pos in self._positions.items():
            bar = self._get_bar_for_date(sym, date)
            if bar:
                pos["current_price"] = bar.get(price_field) or bar["close"]

    # ── Account ────────────────────────────────────────────────────────────────

    def get_account(self) -> AccountInfo:
        market_value = sum(p["qty"] * p["current_price"] for p in self._positions.values())
        equity = self._cash + market_value
        unrealized_pl = sum(
            (p["current_price"] - p["avg_price"]) * p["qty"]
            for p in self._positions.values()
        )
        return AccountInfo(
            equity=equity,
            cash=self._cash,
            buying_power=self._cash,
            long_market_value=market_value,
            unrealized_pl=unrealized_pl,
            portfolio_value=equity,
        )

    # ── Orders ─────────────────────────────────────────────────────────────────

    def submit_market_buy(self, symbol: str, qty: int) -> Order:
        """Fill at today's open price."""
        price = self._fill_price(symbol, "open")
        if price is None or price <= 0:
            price = self._fill_price(symbol, "close") or 0.0
        return self._fill_buy(symbol, qty, price, "market")

    def submit_moc_buy(self, symbol: str, qty: int) -> Order:
        """Fill at today's close price."""
        price = self._fill_price(symbol, "close")
        if price is None or price <= 0:
            price = self._fill_price(symbol, "open") or 0.0
        return self._fill_buy(symbol, qty, price, "moc")

    def submit_market_sell(self, symbol: str, qty: int) -> Order:
        return self.close_position(symbol, qty)

    def get_order(self, order_id: str) -> Order:
        for t in self._trade_log:
            if t["order_id"] == order_id:
                return Order(
                    id=order_id, symbol=t["symbol"], qty=t["qty"],
                    side=t["side"], status="filled",
                    filled_avg_price=t["price"], filled_qty=t["qty"],
                )
        raise ValueError(f"Order {order_id} not found")

    def list_orders(self, status: str = "open") -> list[Order]:
        return []

    def cancel_order(self, order_id: str) -> None:
        pass

    # ── Positions ──────────────────────────────────────────────────────────────

    def list_positions(self) -> list[Position]:
        return [
            Position(
                symbol=sym,
                qty=p["qty"],
                avg_entry_price=p["avg_price"],
                current_price=p["current_price"],
                unrealized_pl=(p["current_price"] - p["avg_price"]) * p["qty"],
                market_value=p["qty"] * p["current_price"],
            )
            for sym, p in self._positions.items()
            if p["qty"] > 0
        ]

    def get_position(self, symbol: str) -> Position | None:
        p = self._positions.get(symbol)
        if p is None or p["qty"] <= 0:
            return None
        return Position(
            symbol=symbol,
            qty=p["qty"],
            avg_entry_price=p["avg_price"],
            current_price=p["current_price"],
            unrealized_pl=(p["current_price"] - p["avg_price"]) * p["qty"],
            market_value=p["qty"] * p["current_price"],
        )

    def close_position(self, symbol: str, qty: int | None = None, *, fill_price: float | None = None) -> Order:
        p = self._positions.get(symbol)
        if p is None or p["qty"] <= 0:
            raise ValueError(f"No open position in {symbol}")
        close_qty = qty if qty is not None else p["qty"]
        price = fill_price if fill_price is not None else (self._fill_price(symbol, self._sim_phase) or p["avg_price"])
        self._cash += price * close_qty
        if close_qty >= p["qty"]:
            del self._positions[symbol]
        else:
            p["qty"] -= close_qty
        self._order_counter += 1
        oid = f"mock_sell_{self._order_counter}"
        self._trade_log.append({
            "order_id": oid, "symbol": symbol, "qty": close_qty,
            "side": "sell", "price": price,
        })
        return Order(
            id=oid, symbol=symbol, qty=close_qty, side="sell",
            status="filled", filled_avg_price=price, filled_qty=close_qty,
        )

    def close_all_positions(self) -> list[Order]:
        orders = []
        for sym in list(self._positions.keys()):
            try:
                orders.append(self.close_position(sym))
            except Exception:
                pass
        return orders

    # ── Price data ─────────────────────────────────────────────────────────────

    def get_bars_as_dict(
        self,
        symbols: list[str],
        start: dt.date,
        end: dt.date,
    ) -> dict[str, dict[dt.date, dict[str, Any]]]:
        """Return bars from pre-fetched cache filtered to [start, end]."""
        result: dict[str, dict[dt.date, dict]] = {}
        for sym in symbols:
            sym_bars = self._bars.get(sym, {})
            result[sym] = {d: b for d, b in sym_bars.items() if start <= d <= end}
        return result

    def get_latest_bars(self, symbols: list[str]) -> dict[str, Bar]:
        result: dict[str, Bar] = {}
        for sym in symbols:
            sym_bars = self._bars.get(sym, {})
            if not sym_bars:
                continue
            latest = max(sym_bars.keys())
            b = sym_bars[latest]
            result[sym] = Bar(
                date=latest.isoformat(),
                open=b["open"], high=b["high"], low=b["low"],
                close=b["close"], volume=b.get("volume", 0),
            )
        return result

    # ── Helpers ────────────────────────────────────────────────────────────────

    def _fill_price(self, symbol: str, field: str) -> float | None:
        bar = self._get_bar_for_date(symbol, self._sim_date)
        if bar:
            return bar.get(field)
        sym_bars = self._bars.get(symbol, {})
        if sym_bars:
            available = [d for d in sym_bars if d <= (self._sim_date or dt.date.today())]
            if available:
                return sym_bars[max(available)].get(field)
        return None

    def _get_bar_for_date(self, symbol: str, date: dt.date | None) -> dict | None:
        if date is None:
            return None
        sym_bars = self._bars.get(symbol, {})
        if date in sym_bars:
            return sym_bars[date]
        available = [d for d in sym_bars if d <= date]
        if available:
            return sym_bars[max(available)]
        return None

    def _fill_buy(self, symbol: str, qty: int, price: float, order_type: str) -> Order:
        if price <= 0:
            raise ValueError(f"Cannot fill {symbol}: no price available for {self._sim_date}")
        cost = price * qty
        self._cash -= cost
        if symbol in self._positions:
            p = self._positions[symbol]
            total_qty = p["qty"] + qty
            p["avg_price"] = (p["avg_price"] * p["qty"] + price * qty) / total_qty
            p["qty"] = total_qty
            p["current_price"] = price
        else:
            self._positions[symbol] = {
                "qty": qty,
                "avg_price": price,
                "current_price": price,
            }
        self._order_counter += 1
        oid = f"mock_buy_{self._order_counter}"
        self._trade_log.append({
            "order_id": oid, "symbol": symbol, "qty": qty,
            "side": "buy", "price": price,
        })
        return Order(
            id=oid, symbol=symbol, qty=qty, side="buy",
            status="filled", filled_avg_price=price, filled_qty=qty,
        )


# ─────────────────────────────────────────────────────────────────────────────
# Bar pre-fetcher
# ─────────────────────────────────────────────────────────────────────────────

async def prefetch_bars(
    symbols: list[str],
    start: dt.date,
    end: dt.date,
    oracle_url: str,
    console=None,
) -> dict[str, dict[dt.date, dict]]:
    """Fetch OHLCV bars from Oracle for given symbols and date range."""
    from libs.oracle_client import OracleClient, PriceService

    cache: dict[str, dict[dt.date, dict]] = {}
    sem = asyncio.Semaphore(10)

    async def _fetch_one(sym: str) -> None:
        async with sem:
            try:
                async with OracleClient(base_url=oracle_url) as client:
                    svc = PriceService(client)
                    resp = await svc.get_daily_bars(
                        sym,
                        start=start.isoformat(),
                        end=end.isoformat(),
                    )
                date_map: dict[dt.date, dict] = {}
                for b in resp.bars:
                    d = dt.date.fromisoformat(b.date[:10])
                    date_map[d] = {
                        "date": d,
                        "open": float(b.open),
                        "high": float(b.high),
                        "low": float(b.low),
                        "close": float(b.close),
                        "volume": float(b.volume),
                    }
                cache[sym] = date_map
            except Exception as exc:
                msg = f"  Bar fetch failed for {sym}: {exc}"
                if console is not None:
                    console.print(f"  [yellow]{msg}[/]")
                cache[sym] = {}

    await asyncio.gather(*(_fetch_one(sym) for sym in symbols))
    return cache


async def get_event_symbols_from_db(
    db_dsn: str,
    start_date: dt.date,
    end_date: dt.date,
    console=None,
) -> list[str]:
    """Query pipeline DB for symbols appearing in events during the date range."""
    try:
        from sqlalchemy import select
        from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
        from libs.db.models import Event, EventLabel, SymbolMaster

        engine = create_async_engine(db_dsn, echo=False, connect_args={"timeout": 5})
        async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

        async with async_session() as session:
            stmt = (
                select(SymbolMaster.ticker)
                .join(Event, Event.symbol_id == SymbolMaster.symbol_id)
                .join(EventLabel, Event.event_id == EventLabel.event_id)
                .where(EventLabel.entry_date >= start_date)
                .where(EventLabel.entry_date <= end_date)
                .where(EventLabel.label_status.in_(["ok", "truncated", "pending"]))
                .distinct()
            )
            rows = (await session.execute(stmt)).all()

        await engine.dispose()
        return [r[0] for r in rows if r[0]]

    except Exception as exc:
        msg = f"DB symbol fetch failed: {type(exc).__name__}: {exc}" if str(exc) else f"DB symbol fetch failed: {type(exc).__name__}"
        if console is not None:
            console.print(f"  [red]{msg}[/]")
        return []
