from __future__ import annotations

import logging
import os
from datetime import datetime
from types import SimpleNamespace
from typing import AsyncIterator, Callable, Iterable, Optional, TypeVar

from alpaca.trading.client import TradingClient
from alpaca.trading.requests import GetOrdersRequest, LimitOrderRequest, MarketOrderRequest, ReplaceOrderRequest

from ..enums import OrderSide, OrderType, TimeInForce
from ..interfaces import BrokerAdapter
from ..models import (
    OrderSubmitRequest,
    OrderReplaceRequest,
    Order,
    Account,
    Position,
    PortfolioHistory,
    Quote,
)
from .mapping import map_order_status
from ...common.cpz_ai import CPZAIClient

_logger = logging.getLogger(__name__)
_T = TypeVar("_T")


def _mk_side(value: str):
    try:
        from alpaca.trading.enums import OrderSide as AlpacaSide  # type: ignore

        return AlpacaSide(value)
    except Exception:
        return SimpleNamespace(value=value)


def _mk_tif(value: str):
    try:
        from alpaca.trading.enums import TimeInForce as AlpacaTIF  # type: ignore

        return AlpacaTIF(value)
    except Exception:
        return SimpleNamespace(value=value)


class AlpacaAdapter(BrokerAdapter):
    _create_kwargs: dict  # stored for 401 re-auth

    def __init__(
        self,
        api_key_id: str = "",
        api_secret_key: str = "",
        *,
        oauth_token: str = "",
        env: str = "paper",
    ) -> None:
        self._env = env
        self._is_oauth = bool(oauth_token)
        paper = env == "paper"
        if oauth_token:
            self._client = TradingClient(oauth_token=oauth_token, paper=paper)
        else:
            self._client = TradingClient(api_key_id, api_secret_key, paper=paper)

    def _with_retry(self, fn: Callable[[], _T]) -> _T:
        """Execute *fn*; on 401/403 from an expired OAuth token, re-fetch
        credentials from the CPZ platform, rebuild the TradingClient, and retry once."""
        try:
            return fn()
        except Exception as exc:
            status = getattr(exc, "status_code", None) or getattr(exc, "status", 0)
            if not self._is_oauth or status not in (401, 403):
                raise
            _logger.info("OAuth token expired (HTTP %s), refreshing credentials...", status)
            try:
                refreshed = self.create(**self._create_kwargs)
                self._client = refreshed._client
                self._is_oauth = refreshed._is_oauth
                return fn()
            except Exception as retry_exc:
                _logger.error("Credential refresh failed: %s", retry_exc)
                raise exc from retry_exc

    @staticmethod
    def create(**kwargs: object) -> "AlpacaAdapter":
        api_key_id = str(kwargs.get("api_key_id") or "")
        api_secret_key = str(kwargs.get("api_secret_key") or "")
        oauth_token = str(kwargs.get("oauth_token") or "")
        env = str(kwargs.get("env") or os.getenv("ALPACA_ENV", "paper"))
        account_id = str(kwargs.get("account_id") or "")

        # 1) Check explicit OAuth token from kwargs or env
        if not oauth_token:
            oauth_token = os.getenv("ALPACA_OAUTH_TOKEN", "")

        if oauth_token:
            adapter = AlpacaAdapter(oauth_token=oauth_token, env=env)
            adapter._create_kwargs = dict(kwargs)
            return adapter

        # 2) Check API key/secret from env vars
        if not api_key_id:
            api_key_id = os.getenv("ALPACA_API_KEY_ID", "") or os.getenv("APCA_API_KEY_ID", "")
        if not api_secret_key:
            api_secret_key = os.getenv("ALPACA_API_SECRET_KEY", "") or os.getenv("APCA_API_SECRET_KEY", "")

        # 3) If still missing, fetch from CPZAI platform (tries OAuth tokens first, then API keys)
        if not api_key_id or not api_secret_key:
            platform = CPZAIClient.from_env()
            lookup_env = None if account_id else (env or None)
            try:
                creds = platform.get_broker_credentials(
                    "alpaca", env=lookup_env, account_id=(account_id or None)
                )
                # Fallback: if exact account_id match failed, try without it
                # Handles OAuth users where the stored account_id (UUID) differs
                # from the account_number used in use_broker()
                if creds is None and account_id:
                    creds = platform.get_broker_credentials(
                        "alpaca", env=(env or None), account_id=None
                    )
                if creds and creds.get("oauth_token"):
                    _a = AlpacaAdapter(
                        oauth_token=creds["oauth_token"],
                        env=str(creds.get("env") or env),
                    )
                    _a._create_kwargs = dict(kwargs)
                    return _a
                if creds and creds.get("api_key_id") and creds.get("api_secret_key"):
                    api_key_id = creds.get("api_key_id", api_key_id)
                    api_secret_key = creds.get("api_secret_key", api_secret_key)
                    if creds.get("env"):
                        env = str(creds["env"])
                elif creds is None:
                    if account_id:
                        error_msg = (
                            f"Alpaca broker credentials not found in CPZAI platform for account ID: {account_id}. "
                            f"Please add your Alpaca trading credentials for this account to your CPZAI account at "
                            f"https://ai.cpz-lab.com/execution"
                        )
                    else:
                        error_msg = (
                            f"Alpaca broker credentials not found in CPZAI platform. "
                            f"Broker: alpaca, Environment: {env or 'any'}. "
                            f"Please add your Alpaca trading credentials to your CPZAI account at "
                            f"https://ai.cpz-lab.com/execution"
                        )
                    raise ValueError(error_msg)
            except ValueError:
                raise
            except Exception as exc:
                import logging
                logger = logging.getLogger(__name__)
                logger.warning(f"Failed to fetch broker credentials from CPZAI: {exc}")
                if not api_key_id or not api_secret_key:
                    raise ValueError(
                        f"Alpaca API credentials are required but could not be retrieved from CPZAI. "
                        f"Error: {exc}. "
                        f"Set ALPACA_API_KEY_ID and ALPACA_API_SECRET_KEY environment variables, "
                        f"or ensure broker credentials are configured in CPZAI platform."
                    ) from exc

        if not api_key_id or not api_secret_key:
            raise ValueError(
                "Alpaca API credentials are required. Set ALPACA_API_KEY_ID and "
                "ALPACA_API_SECRET_KEY environment variables, or ensure broker credentials "
                "are configured in CPZAI platform."
            )

        adapter = AlpacaAdapter(api_key_id=api_key_id, api_secret_key=api_secret_key, env=env)
        adapter._create_kwargs = dict(kwargs)
        return adapter

    def get_account(self) -> Account:
        acct = self._with_retry(lambda: self._client.get_account())
        # CRITICAL: Use account_number (e.g., "PA3FHUB575J3") not id (UUID)
        # account_number is the actual Alpaca account identifier
        acct_id = str(
            getattr(acct, "account_number", None) or 
            getattr(acct, "account_id", None) or 
            getattr(acct, "id", "")
        )
        buying_power = float(getattr(acct, "buying_power", 0) or 0)
        equity = float(getattr(acct, "equity", 0) or 0)
        cash = float(getattr(acct, "cash", 0) or 0)
        return Account(id=acct_id, buying_power=buying_power, equity=equity, cash=cash)

    def get_positions(self) -> list[Position]:
        raw = self._with_retry(lambda: self._client.get_all_positions())
        positions: list[Position] = []
        for p in raw:
            symbol = str(getattr(p, "symbol", ""))
            qty = float(getattr(p, "qty", 0) or 0)
            avg_entry_price = float(getattr(p, "avg_entry_price", 0) or 0)
            market_value = float(getattr(p, "market_value", 0) or 0)
            positions.append(
                Position(
                    symbol=symbol,
                    qty=qty,
                    avg_entry_price=avg_entry_price,
                    market_value=market_value,
                )
            )
        return positions

    def submit_order(self, req: OrderSubmitRequest) -> Order:
        side_val = req.side.value  # already lowercase
        tif_val = req.time_in_force.value.lower()
        # Crypto symbols (contain "/" like ETH/USD) require GTC, not DAY
        if "/" in req.symbol and tif_val == "day":
            tif_val = "gtc"
        if req.type == OrderType.MARKET:
            kwargs: dict = {
                "symbol": req.symbol,
                "side": _mk_side(side_val),
                "time_in_force": _mk_tif(tif_val),
            }
            if req.notional is not None:
                kwargs["notional"] = req.notional
            else:
                kwargs["qty"] = req.qty
            order = self._with_retry(lambda: self._client.submit_order(
                order_data=MarketOrderRequest(**kwargs)
            ))
        else:
            _limit_req = LimitOrderRequest(
                symbol=req.symbol,
                qty=req.qty,
                side=_mk_side(side_val),
                time_in_force=_mk_tif(tif_val),
                limit_price=req.limit_price,
            )
            order = self._with_retry(lambda: self._client.submit_order(order_data=_limit_req))
        return self._map_order(order)

    def get_order(self, order_id: str) -> Order:
        order = self._with_retry(lambda: self._client.get_order_by_id(order_id))
        return self._map_order(order)

    def cancel_order(self, order_id: str) -> Order:
        self._with_retry(lambda: self._client.cancel_order_by_id(order_id))
        return self.get_order(order_id)

    def replace_order(self, order_id: str, req: OrderReplaceRequest) -> Order:
        a_req = ReplaceOrderRequest(qty=req.qty, limit_price=req.limit_price)
        order = self._with_retry(lambda: self._client.replace_order_by_id(order_id=order_id, order_data=a_req))
        return self._map_order(order)

    def list_orders(
        self,
        status: Optional[str] = None,
        limit: int = 50,
        after: Optional[datetime] = None,
        until: Optional[datetime] = None,
        symbols: Optional[list[str]] = None,
    ) -> list[Order]:
        req_kwargs: dict = {"limit": limit}
        if status:
            from alpaca.trading.enums import QueryOrderStatus
            status_map = {
                "open": QueryOrderStatus.OPEN,
                "closed": QueryOrderStatus.CLOSED,
                "all": QueryOrderStatus.ALL,
            }
            req_kwargs["status"] = status_map.get(status.lower(), QueryOrderStatus.ALL)
        if after:
            req_kwargs["after"] = after
        if until:
            req_kwargs["until"] = until
        if symbols:
            req_kwargs["symbols"] = symbols
        request = GetOrdersRequest(**req_kwargs)
        raw = self._with_retry(lambda: self._client.get_orders(filter=request))
        return [self._map_order(o) for o in raw]

    def close_position(self, symbol: str) -> Order:
        raw = self._with_retry(lambda: self._client.close_position(symbol))
        return self._map_order(raw)

    def close_all_positions(self) -> list[Order]:
        raw = self._with_retry(lambda: self._client.close_all_positions(cancel_orders=True))
        results: list[Order] = []
        for item in raw:
            order_obj = getattr(item, "body", item)
            try:
                results.append(self._map_order(order_obj))
            except Exception:
                pass
        return results

    def get_portfolio_history(
        self,
        period: str = "1D",
        timeframe: str = "1H",
    ) -> PortfolioHistory:
        import requests as _requests

        base = "https://api.alpaca.markets" if self._env != "paper" else "https://paper-api.alpaca.markets"
        url = f"{base}/v2/account/portfolio/history"
        params = {"period": period, "timeframe": timeframe, "intraday_reporting": "market_hours"}

        headers: dict = {}
        if self._is_oauth:
            token = getattr(self._client, "_oauth_token", None) or ""
            headers["Authorization"] = f"Bearer {token}"
        else:
            key_id = getattr(self._client, "_api_key", None) or ""
            secret = getattr(self._client, "_secret_key", None) or ""
            headers["APCA-API-KEY-ID"] = str(key_id)
            headers["APCA-API-SECRET-KEY"] = str(secret)

        resp = _requests.get(url, params=params, headers=headers, timeout=10)
        resp.raise_for_status()
        data = resp.json()

        timestamps = data.get("timestamp", [])
        equity_vals = [float(v) for v in data.get("equity", [])]
        pl_vals = [float(v) for v in data.get("profit_loss", [])]
        pl_pct_vals = [float(v) for v in data.get("profit_loss_pct", [])]
        base_value = float(data.get("base_value", 0))

        return PortfolioHistory(
            timestamps=timestamps,
            equity=equity_vals,
            profit_loss=pl_vals,
            profit_loss_pct=pl_pct_vals,
            base_value=base_value,
            timeframe=timeframe,
        )

    def get_historical_data(self, symbol: str, timeframe: str = "1Day", limit: int = 100,
                            start=None, end=None) -> list:
        """Fetch bars from Alpaca Data API. Works for stocks, ETFs, and crypto."""
        import requests as _requests
        from ..models import Bar
        from datetime import datetime as _dt, timedelta, timezone as _tz

        is_crypto = "/" in symbol
        if is_crypto:
            base_url = "https://data.alpaca.markets/v1beta3/crypto/us/bars"
        else:
            base_url = "https://data.alpaca.markets/v2/stocks/bars"

        tf_map = {"15Min": "15Min", "1H": "1Hour", "1Day": "1Day", "1D": "1Day",
                   "5Min": "5Min", "1Min": "1Min"}
        alpaca_tf = tf_map.get(timeframe, timeframe)

        if not start:
            days_back = max(limit * 2, 30) if "Min" in alpaca_tf else max(limit * 2, 200)
            start = (_dt.now(_tz.utc) - timedelta(days=days_back)).strftime("%Y-%m-%dT00:00:00Z")
        if not end:
            end = _dt.now(_tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

        headers: dict = {}
        if self._is_oauth:
            token = getattr(self._client, "_oauth_token", None) or ""
            headers["Authorization"] = f"Bearer {token}"
        else:
            key_id = getattr(self._client, "_api_key", None) or ""
            secret = getattr(self._client, "_secret_key", None) or ""
            headers["APCA-API-KEY-ID"] = str(key_id)
            headers["APCA-API-SECRET-KEY"] = str(secret)

        params = {"timeframe": alpaca_tf, "start": str(start), "end": str(end),
                  "limit": min(limit, 10000), "sort": "asc"}
        if is_crypto:
            params["symbols"] = symbol

        resp = _requests.get(f"{base_url}" if is_crypto else f"{base_url}/{symbol}",
                             params=params, headers=headers, timeout=15)
        resp.raise_for_status()
        data = resp.json()

        raw_bars = data.get("bars", [])
        if is_crypto and isinstance(raw_bars, dict):
            raw_bars = raw_bars.get(symbol, [])

        bars = []
        for b in raw_bars[-limit:]:
            bars.append(Bar(
                symbol=symbol,
                open=float(b.get("o", 0)),
                high=float(b.get("h", 0)),
                low=float(b.get("l", 0)),
                close=float(b.get("c", 0)),
                volume=float(b.get("v", 0)),
            ))
        return bars

    def get_quotes(self, symbols: list[str]) -> list[Quote]:
        return [Quote(symbol=s, bid=0.0, ask=0.0) for s in symbols]

    def stream_quotes(self, symbols: Iterable[str]) -> AsyncIterator[Quote]:
        async def _gen() -> AsyncIterator[Quote]:
            for sym in symbols:
                yield Quote(symbol=sym, bid=0.0, ask=0.0)
                break

        return _gen()

    @staticmethod
    def _map_order(order_obj: object) -> Order:
        oid = str(getattr(order_obj, "id", ""))
        symbol = str(getattr(order_obj, "symbol", ""))
        side_val = getattr(
            getattr(order_obj, "side", None), "value", getattr(order_obj, "side", "buy")
        )
        type_val = getattr(
            getattr(order_obj, "type", None), "value", getattr(order_obj, "type", "market")
        )
        tif_val = getattr(
            getattr(order_obj, "time_in_force", None),
            "value",
            getattr(order_obj, "time_in_force", "DAY"),
        )
        status_val = getattr(
            getattr(order_obj, "status", None), "value", getattr(order_obj, "status", "")
        )
        qty_val = getattr(order_obj, "qty", 0)

        # Optional execution fields, if present
        filled_qty = getattr(order_obj, "filled_qty", None)
        avg_fill_price = getattr(order_obj, "average_fill_price", None)
        submitted_at = getattr(order_obj, "submitted_at", None)
        filled_at = getattr(order_obj, "filled_at", None)

        return Order(
            id=oid,
            symbol=symbol,
            side=OrderSide(str(side_val).lower()),
            qty=float(qty_val or 0),
            type=OrderType(str(type_val).lower()),
            time_in_force=TimeInForce(str(tif_val).upper()),
            status=map_order_status(str(status_val)),
            filled_qty=float(filled_qty or 0) if filled_qty is not None else None,
            average_fill_price=float(avg_fill_price or 0) if avg_fill_price is not None else None,
            submitted_at=submitted_at,
            filled_at=filled_at,
        )
