"""
SnapTrade Adapter
=================
BrokerAdapter implementation for brokers connected via SnapTrade
(Fidelity, Schwab, Robinhood, Questrade, Wealthsimple, etc.).

All operations are proxied through the snaptrade-direct Supabase Edge Function
which handles SnapTrade API authentication (HMAC-SHA256 signing) server-side.
"""

from __future__ import annotations

import os
import logging
from datetime import datetime
from typing import AsyncIterator, Dict, Any, Iterable, List, Optional

import requests

from ..enums import OrderSide, OrderType, TimeInForce
from ..interfaces import BrokerAdapter
from ..models import (
    OrderSubmitRequest,
    OrderReplaceRequest,
    Order,
    Account,
    Position,
    Quote,
    Bar,
)
from .mapping import map_order_status, map_order_side, map_order_type, map_time_in_force

logger = logging.getLogger(__name__)

SUPABASE_URL = "https://brkcjojfmlygsujiqglv.supabase.co"
SUPABASE_ANON_KEY = (
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
    "eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJya2Nqb2pmbWx5Z3N1amlxZ2x2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDc5MDg3NTksImV4cCI6MjA2MzQ4NDc1OX0."
    "1mM0avWJHVH-7_MKIW7_gvh-WwMj97qTVOe0pbk0S-E"
)


class SnapTradeAdapter(BrokerAdapter):
    """
    SnapTrade adapter for CPZ Python SDK.

    Connects to brokers via the SnapTrade aggregation layer through the
    ``snaptrade-direct`` Supabase Edge Function.  The edge function handles
    SnapTrade API signing and credential management server-side so no
    SnapTrade secrets are needed in the SDK environment.

    Prerequisites:
        1. Connect your broker account via the CPZ AI web UI
           (Settings → Connect Broker → Other Brokers).
        2. The connected account will appear in your trading credentials.

    Usage::

        from cpz import CPZClient

        client = CPZClient()

        # Use the actual broker name — no need to mention SnapTrade
        client.execution.use_broker("fidelity")
        client.execution.use_broker("schwab")
        client.execution.use_broker("robinhood")

        # Or specify an explicit account
        client.execution.use_broker("fidelity", account_id="<credential-uuid>")

        account = client.execution.get_account()
        positions = client.execution.get_positions()
    """

    def __init__(
        self,
        cpz_api_key: str,
        account_id: str,
        supabase_url: str = SUPABASE_URL,
        supabase_anon_key: str = SUPABASE_ANON_KEY,
        timeout: int = 30,
    ) -> None:
        self._cpz_api_key = cpz_api_key
        self._account_id = account_id
        self._supabase_url = supabase_url.rstrip("/")
        self._anon_key = supabase_anon_key
        self._timeout = timeout
        self._fn_url = f"{self._supabase_url}/functions/v1/snaptrade-direct"

    @staticmethod
    def create(**kwargs: object) -> "SnapTradeAdapter":
        from ...common.cpz_ai import CPZAIClient

        account_id = str(kwargs.get("account_id") or "")
        broker_name = str(kwargs.get("broker_name") or "snaptrade")
        env = str(kwargs.get("env") or os.getenv("SNAPTRADE_ENV", "live"))
        supabase_url = str(kwargs.get("supabase_url") or os.getenv("SUPABASE_URL", SUPABASE_URL))
        supabase_anon_key = str(
            kwargs.get("supabase_anon_key")
            or os.getenv("SUPABASE_ANON_KEY", SUPABASE_ANON_KEY)
        )

        platform = CPZAIClient.from_env()
        cpz_api_key = platform.api_key

        if not account_id:
            try:
                creds = platform.get_broker_credentials(
                    broker="snaptrade", env=env or None, account_id=None
                )
                if creds:
                    # If user selected a specific broker name (e.g. "fidelity"),
                    # try to match by broker_display_name
                    if broker_name != "snaptrade":
                        all_creds = platform.list_trading_credentials() or []
                        for c in all_creds:
                            if (c.get("broker") == "snaptrade" and
                                    broker_name.lower() in (c.get("broker_display_name") or "").lower()):
                                account_id = str(c.get("id") or "")
                                break
                    if not account_id:
                        account_id = str(creds.get("id") or creds.get("account_id") or "")
            except Exception as exc:
                logger.warning("Failed to get SnapTrade credentials from platform: %s", exc)

        display = broker_name.title() if broker_name != "snaptrade" else "SnapTrade"
        if not account_id:
            raise ValueError(
                f"{display} account not found.  Connect your broker via the CPZ AI "
                "web UI first (Settings → Connect Broker → Other Brokers)."
            )

        return SnapTradeAdapter(
            cpz_api_key=cpz_api_key,
            account_id=account_id,
            supabase_url=supabase_url,
            supabase_anon_key=supabase_anon_key,
        )

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _call(self, action: str, extra: Optional[Dict[str, Any]] = None) -> Any:
        """Call the snaptrade-direct edge function."""
        payload: Dict[str, Any] = {"action": action, "accountId": self._account_id}
        if extra:
            payload.update(extra)

        headers = {
            "apikey": self._anon_key,
            "X-CPZ-Key": self._cpz_api_key,
            "Content-Type": "application/json",
        }

        try:
            resp = requests.post(
                self._fn_url, json=payload, headers=headers, timeout=self._timeout
            )
        except requests.exceptions.Timeout:
            raise Exception(f"SnapTrade edge function timeout after {self._timeout}s")
        except requests.exceptions.ConnectionError:
            raise Exception(f"Failed to connect to SnapTrade edge function at {self._fn_url}")

        if resp.status_code >= 400:
            error_text = resp.text
            try:
                error_data = resp.json()
                error_text = error_data.get("message") or error_data.get("error") or error_text
            except Exception:
                pass
            raise Exception(f"SnapTrade error ({resp.status_code}): {error_text}")

        data = resp.json()
        if data.get("error"):
            raise Exception(data.get("message") or data["error"])
        return data.get("data", data)

    # ------------------------------------------------------------------
    # BrokerAdapter protocol implementation
    # ------------------------------------------------------------------

    def get_account(self) -> Account:
        data = self._call("account")
        return Account(
            id=str(data.get("account_id") or data.get("id") or self._account_id),
            buying_power=float(data.get("buying_power") or 0),
            equity=float(data.get("equity") or 0),
            cash=float(data.get("cash") or 0),
        )

    def get_positions(self) -> list[Position]:
        data = self._call("positions")
        if not isinstance(data, list):
            data = []
        return [
            Position(
                symbol=str(p.get("symbol", "")),
                qty=float(p.get("qty") or 0),
                avg_entry_price=float(p.get("avg_entry_price") or 0),
                market_value=float(p.get("market_value") or 0),
            )
            for p in data
        ]

    def submit_order(self, req: OrderSubmitRequest) -> Order:
        raise NotImplementedError(
            "Order placement through SnapTrade requires a custom plan.  "
            "Use the CPZ AI web UI for trade execution, or connect directly "
            "via Alpaca / IBKR for SDK-based trading."
        )

    def get_order(self, order_id: str) -> Order:
        data = self._call("orders")
        if not isinstance(data, list):
            data = []
        for o in data:
            oid = str(o.get("id") or o.get("order_id") or "")
            if oid == order_id:
                return self._map_order(o)
        raise Exception(f"Order {order_id} not found")

    def cancel_order(self, order_id: str) -> Order:
        raise NotImplementedError(
            "Order cancellation through SnapTrade is not yet supported in the SDK."
        )

    def replace_order(self, order_id: str, req: OrderReplaceRequest) -> Order:
        raise NotImplementedError(
            "Order replacement through SnapTrade is not supported. "
            "Cancel the order and submit a new one instead."
        )

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

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

    def get_historical_data(
        self,
        symbol: str,
        timeframe: str,
        limit: int = 100,
        start: Optional[datetime] = None,
        end: Optional[datetime] = None,
    ) -> list[Bar]:
        return []

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    def _map_order(self, data: Dict[str, Any]) -> Order:
        return Order(
            id=str(data.get("id") or data.get("order_id") or ""),
            symbol=str(data.get("symbol", "")),
            side=map_order_side(str(data.get("side", "buy"))),
            qty=float(data.get("qty") or 0),
            type=map_order_type(str(data.get("type", "market"))),
            time_in_force=map_time_in_force(str(data.get("time_in_force", "DAY"))),
            status=map_order_status(str(data.get("status", "pending"))),
            filled_qty=float(data["filled_qty"]) if data.get("filled_qty") is not None else None,
            average_fill_price=(
                float(data["filled_avg_price"]) if data.get("filled_avg_price") is not None else None
            ),
            filled_at=(
                datetime.fromisoformat(data["filled_at"].replace("Z", "+00:00"))
                if data.get("filled_at") else None
            ),
        )
