"""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


@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]


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_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) -> 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,
        )
        order = self._trading.submit_order(req)
        return self._to_order(order)

    def submit_moc_buy(self, symbol: str, qty: int) -> 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,
        )
        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, **kwargs) -> Order:
        """Close a position. Pass qty for partial close."""
        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 in [start, end]."""
        if not symbols:
            return {}

        from alpaca.data.requests import StockBarsRequest
        from alpaca.data.timeframe import TimeFrame

        req = StockBarsRequest(
            symbol_or_symbols=symbols,
            timeframe=TimeFrame.Day,
            start=dt.datetime.combine(start, dt.time.min),
            end=dt.datetime.combine(end, dt.time.max),
            feed="iex",
        )
        response = self._data.get_stock_bars(req)
        result: dict[str, list[Bar]] = {}
        for sym in symbols:
            try:
                bars_data = response[sym]
            except (KeyError, TypeError):
                bars_data = []
            result[sym] = [
                Bar(
                    date=b.timestamp.date().isoformat() if hasattr(b.timestamp, "date") else str(b.timestamp)[:10],
                    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_latest_bars(self, symbols: list[str]) -> dict[str, Bar]:
        """Fetch the latest bar for each symbol."""
        if not symbols:
            return {}

        from alpaca.data.requests import StockLatestBarRequest

        req = StockLatestBarRequest(symbol_or_symbols=symbols, feed="iex")
        response = self._data.get_stock_latest_bar(req)
        result: dict[str, Bar] = {}
        for sym in symbols:
            b = response.get(sym)
            if b is not None:
                result[sym] = Bar(
                    date=b.timestamp.date().isoformat() if hasattr(b.timestamp, "date") else str(b.timestamp)[:10],
                    open=float(b.open),
                    high=float(b.high),
                    low=float(b.low),
                    close=float(b.close),
                    volume=float(b.volume),
                )
        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)),
        )

    @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,
        )
