"""
AgentBroker Python SDK — Main Client

Usage:
    from agentbroker import AgentBroker

    # Register (no key needed)
    agent = AgentBroker.register(
        name="my-bot",
        email="me@example.com",
        preferred_pairs=["BTC-USDC", "ETH-USDC"],
    )
    print(agent.api_key)

    # Authenticated client
    client = AgentBroker(api_key=agent.api_key)
    client.deposit(1000)
    client.select_strategy("momentum")
    order = client.place_order("BTC-USDC", "buy", "market", quantity=0.001)
"""

from __future__ import annotations

import json
from typing import Any, Dict, List, Optional, Union

try:
    import requests
    from requests import Response
except ImportError:
    raise ImportError("requests is required. Install it with: pip install requests")

from .exceptions import (
    AgentBrokerError,
    AuthenticationError,
    InsufficientBalanceError,
    NotFoundError,
    RateLimitError,
    ServerError,
    ValidationError,
)
from .models import (
    Account,
    Agent,
    BalanceHistory,
    Deposit,
    Order,
    OrderBook,
    Price,
    Strategy,
    Trade,
    Withdrawal,
)
from .websocket import AgentBrokerWebSocket

DEFAULT_BASE_URL = "https://agentbroker.polsia.app"


class AgentBroker:
    """
    AgentBroker API client.

    Parameters
    ----------
    api_key : str, optional
        Your AgentBroker API key. Not required for public endpoints
        (``get_prices``, ``get_orderbook``, ``get_strategies``, ``get_pairs``).
    base_url : str, optional
        Override the default base URL. Defaults to ``https://agentbroker.polsia.app``.
    timeout : int, optional
        HTTP request timeout in seconds. Defaults to 30.

    Examples
    --------
    >>> client = AgentBroker(api_key="ab_live_...")
    >>> prices = client.get_prices()
    >>> order = client.place_order("BTC-USDC", "buy", "market", quantity=0.001)
    """

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: str = DEFAULT_BASE_URL,
        timeout: int = 30,
    ):
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self._session = requests.Session()
        self._session.headers.update({"Content-Type": "application/json"})
        if api_key:
            self._session.headers.update({"X-API-Key": api_key})

    # ── Internal HTTP helpers ───────────────────────────────────────────

    def _request(
        self,
        method: str,
        path: str,
        *,
        params: Optional[Dict] = None,
        json_body: Optional[Dict] = None,
        authenticated: bool = True,
    ) -> Any:
        if authenticated and not self.api_key:
            raise AuthenticationError(
                "This endpoint requires an API key. Pass api_key= to AgentBroker().",
                code="AUTH_REQUIRED",
            )

        url = f"{self.base_url}{path}"
        try:
            resp = self._session.request(
                method,
                url,
                params=params,
                json=json_body,
                timeout=self.timeout,
            )
        except requests.exceptions.ConnectionError as e:
            from .exceptions import ConnectionError as ABConnectionError
            raise ABConnectionError(f"Connection error: {e}", code="CONNECTION_ERROR")
        except requests.exceptions.Timeout:
            from .exceptions import ConnectionError as ABConnectionError
            raise ABConnectionError(f"Request timed out after {self.timeout}s", code="TIMEOUT")

        return self._handle_response(resp)

    def _handle_response(self, resp: Response) -> Any:
        try:
            data = resp.json()
        except ValueError:
            data = {"message": resp.text}

        if resp.status_code == 401:
            raise AuthenticationError(
                data.get("message", "Invalid or missing API key"),
                code=data.get("code", "UNAUTHORIZED"),
                status_code=401,
            )
        if resp.status_code == 400:
            code = data.get("code", "VALIDATION_ERROR")
            if code == "INSUFFICIENT_BALANCE":
                details = data.get("details", {})
                raise InsufficientBalanceError(
                    data.get("message", "Insufficient balance"),
                    required=details.get("required"),
                    available=details.get("available"),
                    status_code=400,
                )
            raise ValidationError(
                data.get("message", "Validation error"),
                code=code,
                status_code=400,
                details=data.get("details", {}),
            )
        if resp.status_code == 404:
            raise NotFoundError(
                data.get("message", "Resource not found"),
                code=data.get("code", "NOT_FOUND"),
                status_code=404,
            )
        if resp.status_code == 429:
            raise RateLimitError(
                data.get("message", "Rate limit exceeded"),
                code="RATE_LIMIT",
                status_code=429,
            )
        if resp.status_code >= 500:
            raise ServerError(
                data.get("message", "Server error"),
                code=data.get("code", "SERVER_ERROR"),
                status_code=resp.status_code,
            )
        if not resp.ok:
            raise AgentBrokerError(
                data.get("message", "Unexpected error"),
                code=data.get("code"),
                status_code=resp.status_code,
            )

        return data

    def _get(self, path: str, params: Optional[Dict] = None, authenticated: bool = True) -> Any:
        return self._request("GET", path, params=params, authenticated=authenticated)

    def _post(self, path: str, body: Optional[Dict] = None, authenticated: bool = True) -> Any:
        return self._request("POST", path, json_body=body or {}, authenticated=authenticated)

    def _put(self, path: str, body: Optional[Dict] = None, authenticated: bool = True) -> Any:
        return self._request("PUT", path, json_body=body or {}, authenticated=authenticated)

    def _delete(self, path: str, authenticated: bool = True) -> Any:
        return self._request("DELETE", path, authenticated=authenticated)

    # ── Registration (class method, no auth needed) ─────────────────────

    @classmethod
    def register(
        cls,
        name: str,
        email: Optional[str] = None,
        callback_url: Optional[str] = None,
        preferred_pairs: Optional[List[str]] = None,
        mode: str = "live",
        base_url: str = DEFAULT_BASE_URL,
    ) -> Agent:
        """
        Register a new agent. Returns an :class:`~agentbroker.models.Agent`
        with ``api_key`` set — **save it**, it won't be shown again.

        Parameters
        ----------
        name : str
            Human-readable name for your agent (e.g. ``"my-momentum-bot"``).
        email : str, optional
            Owner email. Used for notifications.
        callback_url : str, optional
            Webhook URL where trade events are POSTed.
        preferred_pairs : list of str, optional
            Trading pairs the agent is interested in.
            E.g. ``["BTC-USDC", "ETH-USDC"]``.
        mode : str, optional
            ``"sandbox"`` for 10,000 test USDC (no real funds, withdrawals disabled).
            ``"live"`` for production trading. Defaults to ``"live"``.
        base_url : str, optional
            Override the API base URL.

        Returns
        -------
        Agent
            Registered agent with ``api_key`` populated.

        Example
        -------
        >>> # Sandbox: free 10K test USDC, no real funds
        >>> agent = AgentBroker.register(name="my-bot", mode="sandbox")
        >>> print(agent.api_key)   # save this!

        >>> # Live trading
        >>> agent = AgentBroker.register(name="my-bot", email="me@example.com")
        >>> print(agent.api_key)   # save this!
        """
        temp = cls(api_key=None, base_url=base_url)
        body: Dict[str, Any] = {"name": name}
        if email:
            body["email"] = email
        if callback_url:
            body["callback_url"] = callback_url
        if preferred_pairs:
            body["preferred_pairs"] = preferred_pairs
        if mode and mode != "live":
            body["mode"] = mode

        data = temp._post("/v1/agents/register", body, authenticated=False)
        return Agent.from_dict(data)

    # ── Agent / Account ─────────────────────────────────────────────────

    def get_profile(self) -> Agent:
        """
        Fetch the full agent profile including balance, strategy, and trade count.

        Returns
        -------
        Agent
        """
        data = self._get("/api/v1/agents/me")
        return Agent.from_dict(data)

    def update_profile(
        self,
        callback_url: Optional[str] = None,
        preferred_pairs: Optional[List[str]] = None,
    ) -> Agent:
        """
        Update agent callback URL and/or preferred trading pairs.

        Parameters
        ----------
        callback_url : str, optional
            New webhook URL.
        preferred_pairs : list of str, optional
            New list of preferred trading pairs.

        Returns
        -------
        Agent
        """
        body: Dict[str, Any] = {}
        if callback_url is not None:
            body["callback_url"] = callback_url
        if preferred_pairs is not None:
            body["preferred_pairs"] = preferred_pairs
        if not body:
            raise ValidationError("Provide at least one of: callback_url, preferred_pairs", code="VALIDATION_ERROR")
        data = self._put("/api/v1/agents/me", body)
        return Agent.from_dict(data)

    def get_account(self) -> Account:
        """
        Fetch account summary: balance, strategy, deposit totals, trade count.

        Returns
        -------
        Account
        """
        data = self._get("/v1/account")
        return Account.from_dict(data)

    def get_balance(self) -> float:
        """
        Convenience method — returns your current USDC balance as a float.

        Returns
        -------
        float
            Balance in USDC.
        """
        account = self.get_account()
        return float(account.balance_usdc)

    # ── Deposits & Withdrawals ──────────────────────────────────────────

    def deposit(
        self,
        amount: Union[float, int, str],
        currency: str = "USDC",
        tx_hash: Optional[str] = None,
    ) -> Deposit:
        """
        Credit USDC to your agent balance.

        Parameters
        ----------
        amount : float or str
            Amount to deposit (in USDC).
        currency : str, optional
            Currency identifier — currently only ``"USDC"`` is supported.
        tx_hash : str, optional
            On-chain transaction hash for audit purposes.

        Returns
        -------
        Deposit
        """
        body: Dict[str, Any] = {"amount": float(amount)}
        if tx_hash:
            body["tx_hash"] = tx_hash
        data = self._post("/v1/deposit", body)
        return Deposit.from_dict(data)

    def get_deposits(self) -> List[Deposit]:
        """
        List all deposit records for this agent.

        Returns
        -------
        list of Deposit
        """
        data = self._get("/v1/deposits")
        return [Deposit.from_dict(d) for d in data.get("deposits", [])]

    def withdraw(
        self,
        amount: Union[float, int, str],
        address: str,
        currency: str = "USDC",
    ) -> Withdrawal:
        """
        Withdraw USDC from your agent balance.

        Parameters
        ----------
        amount : float or str
            Amount to withdraw.
        address : str
            Destination wallet address.
        currency : str, optional
            Currency identifier — currently only ``"USDC"`` is supported.

        Returns
        -------
        Withdrawal
        """
        body: Dict[str, Any] = {"amount": float(amount), "address": address}
        data = self._post("/v1/withdraw", body)
        return Withdrawal.from_dict(data)

    def get_withdrawals(self) -> List[Withdrawal]:
        """
        List all withdrawal records for this agent.

        Returns
        -------
        list of Withdrawal
        """
        data = self._get("/v1/withdrawals")
        return [Withdrawal.from_dict(w) for w in data.get("withdrawals", [])]

    def get_balance_history(self) -> List[BalanceHistory]:
        """
        Fetch balance change history (deposits, withdrawals, fills).

        Returns
        -------
        list of BalanceHistory
        """
        data = self._get("/v1/balance/history")
        return [BalanceHistory.from_dict(h) for h in data.get("history", [])]

    # ── Strategies ──────────────────────────────────────────────────────

    def get_strategies(self) -> List[Strategy]:
        """
        List all available trading strategies.

        Returns
        -------
        list of Strategy
        """
        data = self._get("/v1/strategies", authenticated=False)
        return [Strategy.from_dict(s) for s in data.get("strategies", [])]

    def select_strategy(self, strategy_name: str) -> Dict:
        """
        Set the active trading strategy for your agent.

        Parameters
        ----------
        strategy_name : str
            One of ``"momentum"``, ``"grid"``, ``"mean_reversion"``.

        Returns
        -------
        dict
            Confirmation including strategy name, display_name, and selected_at.

        Example
        -------
        >>> client.select_strategy("momentum")
        """
        data = self._post("/v1/strategy/select", {"strategy": strategy_name})
        return data

    # ── Orders ──────────────────────────────────────────────────────────

    def place_order(
        self,
        pair: str,
        side: str,
        type: str,
        quantity: Union[float, str],
        price: Optional[Union[float, str]] = None,
    ) -> Order:
        """
        Place a limit or market order.

        Parameters
        ----------
        pair : str
            Trading pair, e.g. ``"BTC-USDC"``, ``"ETH-USDC"``.
        side : str
            ``"buy"`` or ``"sell"``.
        type : str
            ``"limit"`` or ``"market"``.
        quantity : float or str
            Amount of base currency to buy/sell.
        price : float or str, optional
            Limit price in USDC. Required when ``type="limit"``.

        Returns
        -------
        Order

        Example
        -------
        >>> # Market buy 0.001 BTC
        >>> order = client.place_order("BTC-USDC", "buy", "market", quantity=0.001)

        >>> # Limit sell 0.5 ETH at $2,100
        >>> order = client.place_order("ETH-USDC", "sell", "limit", quantity=0.5, price=2100)
        """
        body: Dict[str, Any] = {
            "pair": pair,
            "side": side,
            "type": type,
            "quantity": float(quantity),
        }
        if price is not None:
            body["price"] = float(price)
        data = self._post("/v1/orders", body)
        return Order.from_dict(data)

    def get_orders(
        self,
        status: Optional[str] = None,
        pair: Optional[str] = None,
        limit: Optional[int] = None,
    ) -> List[Order]:
        """
        List orders for this agent.

        Parameters
        ----------
        status : str, optional
            Filter by status: ``"open"``, ``"filled"``, ``"cancelled"``,
            ``"partially_filled"``.
        pair : str, optional
            Filter by trading pair, e.g. ``"BTC-USDC"``.
        limit : int, optional
            Maximum number of orders to return.

        Returns
        -------
        list of Order
        """
        params: Dict[str, Any] = {}
        if status:
            params["status"] = status
        if pair:
            params["pair"] = pair
        if limit:
            params["limit"] = limit
        data = self._get("/v1/orders", params=params or None)
        return [Order.from_dict(o) for o in data.get("orders", [])]

    def cancel_order(self, order_id: Union[int, str]) -> Dict:
        """
        Cancel an open order.

        Parameters
        ----------
        order_id : int or str
            The order ID to cancel.

        Returns
        -------
        dict
            Cancellation confirmation.
        """
        data = self._delete(f"/v1/orders/{order_id}")
        return data

    # ── Market Data (public endpoints) ──────────────────────────────────

    def get_prices(self) -> Dict[str, Price]:
        """
        Get current prices for all trading pairs.

        Returns
        -------
        dict
            Mapping of pair → :class:`~agentbroker.models.Price`.

        Example
        -------
        >>> prices = client.get_prices()
        >>> print(prices["BTC-USDC"].price)
        """
        data = self._get("/v1/prices", authenticated=False)
        result: Dict[str, Price] = {}
        for pair, info in data.items():
            if isinstance(info, dict):
                result[pair] = Price.from_dict(pair, info)
            elif isinstance(info, (int, float)):
                result[pair] = Price(pair=pair, price=float(info))
        return result

    def get_price(self, pair: str) -> Price:
        """
        Get the current price for a specific trading pair.

        Parameters
        ----------
        pair : str
            E.g. ``"BTC-USDC"``

        Returns
        -------
        Price
        """
        data = self._get(f"/v1/prices/{pair}", authenticated=False)
        if isinstance(data, dict) and "price" in data:
            return Price.from_dict(pair, data)
        # Some endpoints return {pair: {...}}
        if isinstance(data, dict) and pair in data:
            inner = data[pair]
            return Price.from_dict(pair, inner if isinstance(inner, dict) else {"price": inner})
        # Flat number
        return Price(pair=pair, price=float(data) if isinstance(data, (int, float)) else 0.0)

    def get_orderbook(self, pair: str) -> OrderBook:
        """
        Get the current order book for a trading pair.

        Parameters
        ----------
        pair : str
            E.g. ``"BTC-USDC"``

        Returns
        -------
        OrderBook

        Example
        -------
        >>> ob = client.get_orderbook("BTC-USDC")
        >>> best_bid = ob.bids[0].price if ob.bids else None
        >>> best_ask = ob.asks[0].price if ob.asks else None
        """
        data = self._get(f"/v1/orderbook/{pair}", authenticated=False)
        return OrderBook.from_dict(data)

    def get_pairs(self) -> List[str]:
        """
        List all supported trading pairs.

        Returns
        -------
        list of str

        Example
        -------
        >>> pairs = client.get_pairs()
        >>> print(pairs)  # ["BTC-USDC", "ETH-USDC", ...]
        """
        data = self._get("/v1/pairs", authenticated=False)
        return data.get("pairs", data if isinstance(data, list) else [])

    # ── Trades ──────────────────────────────────────────────────────────

    def get_trades(
        self,
        pair: Optional[str] = None,
        limit: Optional[int] = None,
    ) -> List[Trade]:
        """
        List executed trades for this agent.

        Parameters
        ----------
        pair : str, optional
            Filter by trading pair.
        limit : int, optional
            Max number of trades to return.

        Returns
        -------
        list of Trade
        """
        params: Dict[str, Any] = {}
        if pair:
            params["pair"] = pair
        if limit:
            params["limit"] = limit
        data = self._get("/v1/trades", params=params or None)
        return [Trade.from_dict(t) for t in data.get("trades", [])]

    # ── WebSocket ────────────────────────────────────────────────────────

    def connect_ws(self) -> AgentBrokerWebSocket:
        """
        Create a WebSocket connection for real-time event streaming.

        The connection is established in the background. Use ``.run_forever()``
        to block, or ``.connect()`` + your own event loop.

        Requires ``websocket-client``: ``pip install agentbroker[ws]``

        Returns
        -------
        AgentBrokerWebSocket

        Example
        -------
        >>> ws = client.connect_ws()

        >>> @ws.on_trade
        ... def handle_trade(event):
        ...     print("Trade:", event)

        >>> ws.run_forever()
        """
        if not self.api_key:
            raise AuthenticationError(
                "WebSocket connections require an API key.",
                code="AUTH_REQUIRED",
            )
        return AgentBrokerWebSocket(api_key=self.api_key, base_url=self.base_url)

    # ── Convenience ──────────────────────────────────────────────────────

    def __repr__(self) -> str:
        key_preview = f"{self.api_key[:8]}..." if self.api_key else "unauthenticated"
        return f"AgentBroker(api_key={key_preview!r}, base_url={self.base_url!r})"
