"""Alpaca Paper Trading API wrapper.

Switch from paper to live by setting paper=False (or ALPACA_PAPER=false).
Requires: pip install alpaca-py
"""
from __future__ import annotations

import datetime as dt
import os
from dataclasses import dataclass
from typing import Any


@dataclass
class AccountInfo:
    equity: float
    cash: float
    buying_power: float
    long_market_value: float
    unrealized_pl: float
    portfolio_value: float


@dataclass
class Order:
    id: str
    symbol: str
    qty: int
    side: str  # "buy" or "sell"
    status: str
    filled_avg_price: float | None
    filled_qty: int
    client_order_id: str | None = None


@dataclass
class Position:
    symbol: str
    qty: int
    avg_entry_price: float
    current_price: float
    unrealized_pl: float
    market_value: float


@dataclass
class Bar:
    date: str
    open: float
    high: float
    low: float
    close: float
    volume: float


@dataclass
class PortfolioHistory:
    timestamps: list[int]
    equity: list[float]
    profit_loss: list[float]
    profit_loss_pct: list[float]


@dataclass
class MarketClock:
    is_open: bool
    next_open: dt.datetime | None
    next_close: dt.datetime | None


class AlpacaBroker:
    """Thin wrapper around alpaca-py TradingClient for paper trading."""

    def __init__(
        self,
        api_key: str | None = None,
        secret_key: str | None = None,
        paper: bool = True,
    ) -> None:
        self._api_key = api_key or os.environ.get("ALPACA_API_KEY", "")
        self._secret_key = secret_key or os.environ.get("ALPACA_SECRET_KEY", "")
        self._paper = paper

        if not self._api_key or not self._secret_key:
            raise ValueError(
                "Alpaca credentials missing. Set ALPACA_API_KEY and ALPACA_SECRET_KEY env vars."
            )

        try:
            from alpaca.trading.client import TradingClient
            from alpaca.data.historical import StockHistoricalDataClient
        except ImportError as exc:
            raise ImportError("alpaca-py not installed. Run: pip install alpaca-py") from exc

        self._trading = TradingClient(
            api_key=self._api_key,
            secret_key=self._secret_key,
            paper=self._paper,
        )
        self._data = StockHistoricalDataClient(
            api_key=self._api_key,
            secret_key=self._secret_key,
        )

    # ------------------------------------------------------------------ #
    # Account
    # ------------------------------------------------------------------ #

    def get_clock(self) -> MarketClock:
        """Return Alpaca's market clock (respects holidays, early closes, halts)."""
        clk = self._trading.get_clock()
        return MarketClock(
            is_open=bool(clk.is_open),
            next_open=clk.next_open,
            next_close=clk.next_close,
        )

    def get_account(self) -> AccountInfo:
        acct = self._trading.get_account()
        equity = float(acct.equity or 0)
        last_equity = float(acct.last_equity or equity)
        return AccountInfo(
            equity=equity,
            cash=float(acct.cash or 0),
            buying_power=float(acct.buying_power or 0),
            long_market_value=float(acct.long_market_value or 0),
            unrealized_pl=equity - last_equity,
            portfolio_value=float(acct.portfolio_value or equity),
        )

    # ------------------------------------------------------------------ #
    # Orders
    # ------------------------------------------------------------------ #

    def submit_market_buy(self, symbol: str, qty: int, client_order_id: str | None = None) -> Order:
        from alpaca.trading.requests import MarketOrderRequest
        from alpaca.trading.enums import OrderSide, TimeInForce

        req = MarketOrderRequest(
            symbol=symbol,
            qty=qty,
            side=OrderSide.BUY,
            time_in_force=TimeInForce.DAY,
            **({"client_order_id": client_order_id} if client_order_id else {}),
        )
        order = self._trading.submit_order(req)
        return self._to_order(order)

    def submit_moc_buy(self, symbol: str, qty: int, client_order_id: str | None = None) -> Order:
        """Submit a Market-on-Close buy order (fills at today's closing price)."""
        from alpaca.trading.requests import MarketOrderRequest
        from alpaca.trading.enums import OrderSide, TimeInForce

        req = MarketOrderRequest(
            symbol=symbol,
            qty=qty,
            side=OrderSide.BUY,
            time_in_force=TimeInForce.CLS,
            **({"client_order_id": client_order_id} if client_order_id else {}),
        )
        order = self._trading.submit_order(req)
        return self._to_order(order)

    def submit_market_sell(self, symbol: str, qty: int) -> Order:
        from alpaca.trading.requests import MarketOrderRequest
        from alpaca.trading.enums import OrderSide, TimeInForce

        req = MarketOrderRequest(
            symbol=symbol,
            qty=qty,
            side=OrderSide.SELL,
            time_in_force=TimeInForce.DAY,
        )
        order = self._trading.submit_order(req)
        return self._to_order(order)

    def get_order(self, order_id: str) -> Order:
        order = self._trading.get_order_by_id(order_id)
        return self._to_order(order)

    def list_orders(self, status: str = "open") -> list[Order]:
        from alpaca.trading.requests import GetOrdersRequest
        from alpaca.trading.enums import QueryOrderStatus

        status_map = {
            "open": QueryOrderStatus.OPEN,
            "closed": QueryOrderStatus.CLOSED,
            "all": QueryOrderStatus.ALL,
        }
        req = GetOrdersRequest(status=status_map.get(status, QueryOrderStatus.OPEN))
        orders = self._trading.get_orders(req)
        return [self._to_order(o) for o in orders]

    def cancel_order(self, order_id: str) -> None:
        self._trading.cancel_order_by_id(order_id)

    # ------------------------------------------------------------------ #
    # Positions
    # ------------------------------------------------------------------ #

    def list_positions(self) -> list[Position]:
        positions = self._trading.get_all_positions()
        return [self._to_position(p) for p in positions]

    def get_position(self, symbol: str) -> Position | None:
        try:
            pos = self._trading.get_open_position(symbol)
            return self._to_position(pos)
        except Exception:
            return None

    def close_position(self, symbol: str, qty: int | None = None) -> Order:
        """Close a position. Pass qty for partial close.

        Returns the submitted close Order; caller must poll `get_order(order.id)`
        for the actual `filled_avg_price` — the submit response does NOT include
        fill data.
        """
        from alpaca.trading.requests import ClosePositionRequest

        if qty is not None:
            req = ClosePositionRequest(qty=str(qty))
            order = self._trading.close_position(symbol, close_options=req)
        else:
            order = self._trading.close_position(symbol)
        return self._to_order(order)

    def close_all_positions(self) -> list[Order]:
        responses = self._trading.close_all_positions(cancel_orders=True)
        if not responses:
            return []
        result = []
        for resp in responses:
            try:
                # close_all_positions returns ClosePositionResponse; body is the actual Order
                order_obj = getattr(resp, "body", resp)
                result.append(self._to_order(order_obj))
            except Exception:
                pass
        return result

    # ------------------------------------------------------------------ #
    # Price data
    # ------------------------------------------------------------------ #

    def get_bars(
        self,
        symbols: list[str],
        start: dt.date,
        end: dt.date,
    ) -> dict[str, list[Bar]]:
        """Fetch daily OHLCV bars for a list of symbols via Oracle API.

        Oracle normalises problematic symbols (e.g. BF-B → BF.B) and maps
        responses back to the original symbol names.
        """
        if not symbols:
            return {}

        from libs.oracle_client.alpaca import get_multi_daily_bars

        raw = get_multi_daily_bars(
            tickers=symbols,
            start_date=start.isoformat(),
            end_date=end.isoformat(),
        )

        result: dict[str, list[Bar]] = {}
        for sym in symbols:
            bars_data = raw.get(sym, [])
            result[sym] = [
                Bar(
                    date=b["date"],
                    open=float(b["open"]),
                    high=float(b["high"]),
                    low=float(b["low"]),
                    close=float(b["close"]),
                    volume=float(b["volume"]),
                )
                for b in bars_data
            ]
        return result

    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 indexed by symbol → date → OHLCV dict (matches backtest format)."""
        raw = self.get_bars(symbols, start, end)
        result: dict[str, dict[dt.date, dict[str, Any]]] = {}
        for sym, bars in raw.items():
            date_map: dict[dt.date, dict[str, Any]] = {}
            for bar in bars:
                d = dt.date.fromisoformat(bar.date)
                date_map[d] = {
                    "date": d,
                    "open": bar.open,
                    "high": bar.high,
                    "low": bar.low,
                    "close": bar.close,
                    "volume": bar.volume,
                }
            result[sym] = date_map
        return result

    def get_intraday_bars(
        self,
        symbols: list[str],
        start: dt.datetime,
        end: dt.datetime,
        timeframe_minutes: int = 5,
    ) -> dict[str, list[dict]]:
        """Fetch intraday OHLCV bars for a list of symbols via Oracle API.

        - Today's date → /alpaca/intraday/today (IEX real-time, force_refresh)
        - Historical dates → /alpaca/intraday (SIP, DB-cached)

        Returns {symbol: [{timestamp: ISO8601, open, high, low, close, volume}, ...]}.
        """
        if not symbols:
            return {}

        interval = f"{timeframe_minutes}min"
        today = dt.date.today()

        if start.date() >= today:
            from libs.oracle_client.alpaca import get_multi_intraday_bars_today
            raw = get_multi_intraday_bars_today(tickers=symbols, interval=interval)
        else:
            from libs.oracle_client.alpaca import get_multi_intraday_bars
            raw = get_multi_intraday_bars(
                tickers=symbols,
                start_date=start.date().isoformat(),
                end_date=end.date().isoformat(),
                interval=interval,
            )

        result: dict[str, list[dict]] = {sym: [] for sym in symbols}
        for sym in symbols:
            result[sym] = raw.get(sym, [])
        return result

    def get_latest_bars(self, symbols: list[str]) -> dict[str, Bar]:
        """Fetch the latest bar for each symbol via Oracle snapshot API.

        Routes through Oracle so that problematic symbols (e.g. BF-B → BF.B)
        are normalised server-side before hitting Alpaca.
        """
        if not symbols:
            return {}

        from libs.oracle_client.alpaca import get_snapshots

        snaps = get_snapshots(symbols)
        result: dict[str, Bar] = {}
        import datetime as _dt
        today = _dt.date.today().isoformat()
        for sym, snap in snaps.items():
            price = snap.price or snap.mid
            if price is not None:
                result[sym] = Bar(
                    date=today,
                    open=price,
                    high=price,
                    low=price,
                    close=price,
                    volume=snap.volume or 0,
                )
        return result

    # ------------------------------------------------------------------ #
    # Portfolio history
    # ------------------------------------------------------------------ #

    def get_portfolio_history(self, period: str = "1M") -> PortfolioHistory:
        from alpaca.trading.requests import GetPortfolioHistoryRequest

        req = GetPortfolioHistoryRequest(period=period, timeframe="1D")
        hist = self._trading.get_portfolio_history(req)
        return PortfolioHistory(
            timestamps=list(hist.timestamp or []),
            equity=[float(v) for v in (hist.equity or [])],
            profit_loss=[float(v) for v in (hist.profit_loss or [])],
            profit_loss_pct=[float(v) for v in (hist.profit_loss_pct or [])],
        )

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

    @staticmethod
    def _to_order(order: Any) -> Order:
        return Order(
            id=str(order.id),
            symbol=str(order.symbol),
            qty=int(float(order.qty or 0)),
            side=str(order.side.value if hasattr(order.side, "value") else order.side),
            status=str(order.status.value if hasattr(order.status, "value") else order.status),
            filled_avg_price=float(order.filled_avg_price) if order.filled_avg_price else None,
            filled_qty=int(float(order.filled_qty or 0)),
            client_order_id=str(order.client_order_id) if getattr(order, "client_order_id", None) else None,
        )

    @staticmethod
    def _to_position(pos: Any) -> Position:
        return Position(
            symbol=str(pos.symbol),
            qty=int(float(pos.qty or 0)),
            avg_entry_price=float(pos.avg_entry_price or 0),
            current_price=float(pos.current_price or 0),
            unrealized_pl=float(pos.unrealized_pl or 0),
            market_value=float(pos.market_value or 0),
        )

    @classmethod
    def from_env(cls) -> "AlpacaBroker":
        """Create from environment variables."""
        paper = os.environ.get("ALPACA_PAPER", "true").lower() != "false"
        return cls(
            api_key=os.environ.get("ALPACA_API_KEY"),
            secret_key=os.environ.get("ALPACA_SECRET_KEY"),
            paper=paper,
        )
