from __future__ import annotations

import os
from types import SimpleNamespace
from typing import AsyncIterator, Iterable

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

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


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):
    def __init__(self, api_key_id: str, api_secret_key: str, env: str = "paper") -> None:
        paper = env == "paper"
        self._client = TradingClient(api_key_id, api_secret_key, paper=paper)

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

        # If not provided, try environment vars
        if not api_key_id:
            api_key_id = os.getenv("ALPACA_API_KEY_ID", "")
        if not api_secret_key:
            api_secret_key = os.getenv("ALPACA_API_SECRET_KEY", "")

        # If still missing, fetch from CPZAI platform using service key
        if not api_key_id or not api_secret_key:
            platform = CPZAIClient.from_env()
            # If account_id is provided, only match by account_id (ignore env)
            # If only env is provided, match by env
            # This ensures account_id takes priority over environment
            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)
                )
                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)
                    # Use env from credentials if not already set
                    if not env and creds.get("env"):
                        env = str(creds["env"])
                elif creds is None:
                    # Credentials not found - raise clear error immediately
                    if account_id:
                        error_msg = (
                            f"Alpaca broker credentials not found in CPZ AI platform for account ID: {account_id}. "
                            f"Please add your Alpaca trading credentials for this account to your CPZ AI account at "
                            f"https://ai.cpz-lab.com/execution"
                        )
                    else:
                        error_msg = (
                            f"Alpaca broker credentials not found in CPZ AI platform. "
                            f"Broker: alpaca, Environment: {env or 'any'}. "
                            f"Please add your Alpaca trading credentials to your CPZ AI account at "
                            f"https://ai.cpz-lab.com/execution"
                        )
                    raise ValueError(error_msg)
            except ValueError:
                # Re-raise ValueError as-is (it has a clear message)
                raise
            except Exception as exc:
                # Log but don't fail - might have direct Alpaca creds in env
                import logging
                logger = logging.getLogger(__name__)
                logger.warning(f"Failed to fetch broker credentials from CPZAI: {exc}")
                # If we still don't have creds after the exception, raise error
                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

        # Final check - must have credentials to proceed
        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."
            )

        return AlpacaAdapter(api_key_id=api_key_id, api_secret_key=api_secret_key, env=env)

    def get_account(self) -> Account:
        acct = 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._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._client.submit_order(
                order_data=MarketOrderRequest(**kwargs)
            )
        else:
            order = self._client.submit_order(
                order_data=LimitOrderRequest(
                    symbol=req.symbol,
                    qty=req.qty,
                    side=_mk_side(side_val),
                    time_in_force=_mk_tif(tif_val),
                    limit_price=req.limit_price,
                )
            )
        return self._map_order(order)

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

    def cancel_order(self, order_id: str) -> Order:
        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._client.replace_order_by_id(order_id=order_id, order_data=a_req)
        return self._map_order(order)

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