"""Market Motion API client."""

from __future__ import annotations

import os
from typing import Any, Optional
from urllib.parse import urlencode

import requests


class ApiError(Exception):
    """Raised when the API returns a non-2xx response."""

    def __init__(self, status: int, message: str, body: Any = None):
        super().__init__(message)
        self.status = status
        self.body = body


class MotionClient:
    """Client for the Market Motion intelligence API.

    Usage::

        from marketmotion import MotionClient

        client = MotionClient(api_key="mk_...")
        entities = client.search_entities("bitcoin")
    """

    DEFAULT_BASE_URL = "https://api.marketmotion.xyz"

    def __init__(
        self,
        api_key: Optional[str] = None,
        base_url: Optional[str] = None,
        timeout: float = 30.0,
    ):
        self.api_key = api_key or os.environ.get("MOTION_API_KEY", "")
        self.base_url = (
            base_url or os.environ.get("MOTION_API_URL", self.DEFAULT_BASE_URL)
        ).rstrip("/")
        self.timeout = timeout
        self._session = requests.Session()
        if self.api_key:
            self._session.headers["X-API-Key"] = self.api_key
        self._session.headers["Content-Type"] = "application/json"

    # ── request helper ───────────────────────────────────────────────

    def _request(
        self,
        method: str,
        path: str,
        params: Optional[dict] = None,
        json: Optional[dict] = None,
    ) -> Any:
        url = f"{self.base_url}{path}"
        resp = self._session.request(
            method, url, params=params, json=json, timeout=self.timeout
        )
        if not resp.ok:
            try:
                body = resp.json()
            except Exception:
                body = resp.text
            if resp.status_code == 401:
                raise ApiError(401, "Authentication required.")
            if resp.status_code == 429:
                raise ApiError(429, "Rate limit exceeded.")
            raise ApiError(resp.status_code, f"API error: {resp.status_code}", body)
        body = resp.json()
        # Unwrap { success, data: {...} } envelope so callers get the payload directly
        if isinstance(body, dict) and "data" in body:
            return body["data"]
        return body

    def _get(self, path: str, **params: Any) -> Any:
        clean = {k: v for k, v in params.items() if v is not None}
        return self._request("GET", path, params=clean)

    def _post(self, path: str, **json_body: Any) -> Any:
        clean = {k: v for k, v in json_body.items() if v is not None}
        return self._request("POST", path, json=clean)

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

    # ── auth ─────────────────────────────────────────────────────────

    def validate_key(self) -> dict:
        return self._get("/api/developer/validate")

    # ── entities ─────────────────────────────────────────────────────

    def search_entities(
        self,
        q: str,
        *,
        category: Optional[str] = None,
        type: Optional[str] = None,
        limit: Optional[int] = None,
        cursor: Optional[str] = None,
    ) -> dict:
        return self._get(
            "/api/entities", q=q, category=category, type=type, limit=limit, cursor=cursor
        )

    def get_entity(self, slug: str) -> dict:
        return self._get(f"/api/entities/{slug}/full", marketLimit=100)

    def get_entity_attributes(self, slug: str) -> dict:
        return self._get(f"/api/entities/by-slug/{slug}/attributes")

    def get_entity_graph(
        self,
        slug: str,
        *,
        depth: Optional[int] = None,
        include_markets: Optional[bool] = None,
        max_nodes: Optional[int] = None,
    ) -> dict:
        return self._get(
            f"/api/entities/{slug}/graph",
            depth=depth,
            includeMarkets=include_markets,
            maxNodes=max_nodes,
        )

    def get_entity_related(
        self, slug: str, *, depth: Optional[int] = None, types: Optional[str] = None
    ) -> dict:
        return self._get(f"/api/entities/{slug}/related", depth=depth, types=types)

    # ── markets ──────────────────────────────────────────────────────

    def get_curated_markets(self) -> dict:
        return self._get("/api/markets/curated")

    def search_markets(
        self,
        q: str,
        *,
        venue: Optional[str] = None,
        limit: Optional[int] = None,
    ) -> dict:
        return self._get("/api/markets/search", q=q, venue=venue, limit=limit)

    def filter_markets(
        self,
        *,
        category: Optional[str] = None,
        sort: Optional[str] = None,
        order: Optional[str] = None,
        search: Optional[str] = None,
        type: Optional[str] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> dict:
        return self._get(
            "/api/markets/filter",
            category=category, sort=sort, order=order,
            search=search, type=type, limit=limit, offset=offset,
        )

    def get_prediction_markets(
        self,
        *,
        venue: Optional[str] = None,
        category: Optional[str] = None,
        sort: Optional[str] = None,
        search: Optional[str] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> dict:
        return self._get(
            "/api/markets/predictions",
            venue=venue, category=category, sort=sort,
            search=search, limit=limit, offset=offset,
        )

    def get_hyperliquid_markets(
        self,
        *,
        product_type: Optional[str] = None,
        sort: Optional[str] = None,
        search: Optional[str] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> dict:
        return self._get(
            "/api/markets/hyperliquid",
            productType=product_type, sort=sort,
            search=search, limit=limit, offset=offset,
        )

    def get_resolved_markets(
        self, *, limit: Optional[int] = None, venue: Optional[str] = None
    ) -> dict:
        return self._get("/api/markets/resolved", limit=limit, venue=venue)

    def get_price_history(
        self,
        market_id: str,
        *,
        hours: Optional[int] = None,
        points: Optional[int] = None,
    ) -> dict:
        return self._get(f"/api/markets/{market_id}/history", hours=hours, points=points)

    def get_cross_venue_markets(
        self,
        *,
        min_spread: Optional[float] = None,
        category: Optional[str] = None,
        limit: Optional[int] = None,
    ) -> dict:
        return self._get(
            "/api/markets/cross-venue",
            minSpread=min_spread, category=category, limit=limit,
        )

    # ── alerts & signals ─────────────────────────────────────────────

    def get_alerts(
        self,
        *,
        type: Optional[str] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> dict:
        return self._get("/api/alerts/inbox", type=type, limit=limit, offset=offset)

    def get_signal_feed(
        self,
        *,
        type: Optional[str] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> dict:
        return self._get("/api/alerts/feed", type=type, limit=limit, offset=offset)

    # ── news ─────────────────────────────────────────────────────────

    def get_trending_news(self, *, limit: Optional[int] = None) -> dict:
        return self._get("/api/news/trending", limit=limit)

    def get_entity_news(
        self, slug: str, *, limit: Optional[int] = None
    ) -> dict:
        return self._get(f"/api/news/entity/{slug}", limit=limit)

    def get_market_news(
        self, market_id: str, *, limit: Optional[int] = None
    ) -> dict:
        return self._get(f"/api/news/market/{market_id}", limit=limit)

    def get_news_by_category(
        self, category: str, *, limit: Optional[int] = None
    ) -> dict:
        return self._get(f"/api/news/category/{category}", limit=limit)

    # ── intelligence ─────────────────────────────────────────────────

    def get_entity_rumors(
        self, slug: str, *, limit: Optional[int] = None
    ) -> dict:
        return self._get(f"/api/entities/{slug}/rumors", limit=limit)

    def get_entity_isq(
        self, slug: str, *, limit: Optional[int] = None, min_score: Optional[float] = None
    ) -> dict:
        return self._get(
            f"/api/intelligence/isq/{slug}", limit=limit, minScore=min_score
        )

    def get_entity_sentiment(
        self, slug: str, *, window: Optional[str] = None, limit: Optional[int] = None
    ) -> dict:
        return self._get(
            f"/api/intelligence/sentiment/{slug}", window=window, limit=limit
        )

    def get_intelligence_brief(
        self,
        *,
        query: Optional[str] = None,
        entity_slug: Optional[str] = None,
        market_id: Optional[str] = None,
    ) -> dict:
        return self._post(
            "/api/intelligence/research",
            query=query, entitySlug=entity_slug, marketId=market_id,
        )

    # ── graph / movers / mispricings ─────────────────────────────────

    def get_movers(
        self,
        *,
        scope: Optional[str] = None,
        window: Optional[int] = None,
        limit: Optional[int] = None,
    ) -> dict:
        return self._get("/api/graph/movers", scope=scope, window=window, limit=limit)

    def get_mispricings(
        self,
        *,
        min_spread: Optional[float] = None,
        category: Optional[str] = None,
        limit: Optional[int] = None,
        actionable_only: Optional[bool] = None,
    ) -> dict:
        return self._get(
            "/api/graph/mispricings",
            minSpread=min_spread, category=category,
            limit=limit, actionableOnly=actionable_only,
        )

    def get_entity_neighborhood(
        self,
        entity_id: str,
        *,
        depth: Optional[int] = None,
        limit: Optional[int] = None,
    ) -> dict:
        return self._get(
            f"/api/graph/entity/{entity_id}/neighborhood", depth=depth, limit=limit
        )

    # ── forecasts ────────────────────────────────────────────────────

    def get_forecast_history(
        self,
        venue: str,
        market_id: str,
        *,
        limit: Optional[int] = None,
    ) -> dict:
        return self._get(
            f"/api/intelligence/forecast/{venue}/{market_id}/history", limit=limit
        )

    def get_forecast_accuracy(self) -> dict:
        return self._get("/api/intelligence/forecast/accuracy")

    # ── webhooks ─────────────────────────────────────────────────────

    def list_webhooks(self) -> dict:
        return self._get("/api/b2b/webhooks")

    def create_webhook(
        self, url: str, events: list[str], description: Optional[str] = None
    ) -> dict:
        return self._post(
            "/api/b2b/webhooks", url=url, events=events, description=description
        )

    def delete_webhook(self, webhook_id: str) -> dict:
        return self._delete(f"/api/b2b/webhooks/{webhook_id}")

    def get_webhook_deliveries(
        self,
        webhook_id: str,
        *,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
    ) -> dict:
        return self._get(
            f"/api/b2b/webhooks/{webhook_id}/deliveries", limit=limit, offset=offset
        )

    # ── trading (polymarket) ─────────────────────────────────────────

    def get_polymarket_status(self) -> dict:
        return self._get("/api/polymarket/session/status")

    def get_polymarket_balance(self) -> dict:
        return self._get("/api/polymarket/balance")

    def get_polymarket_positions(self) -> dict:
        return self._get("/api/polymarket/positions")

    def get_polymarket_orders(self, *, market: Optional[str] = None) -> dict:
        return self._get("/api/polymarket/orders", market=market)

    def place_polymarket_order(
        self,
        token_id: str,
        side: str,
        size: float,
        price: float,
        *,
        type: Optional[str] = None,
        neg_risk: Optional[bool] = None,
    ) -> dict:
        return self._post(
            "/api/polymarket/order",
            tokenId=token_id, side=side, size=size, price=price,
            type=type, negRisk=neg_risk,
        )

    def cancel_polymarket_order(self, order_id: str) -> dict:
        return self._delete(f"/api/polymarket/order/{order_id}")

    def get_polymarket_orderbook(self, token_id: str) -> dict:
        return self._get(f"/api/polymarket/orderbook/{token_id}")

    # ── trading (hyperliquid) ────────────────────────────────────────

    def place_hyperliquid_order(
        self,
        symbol: str,
        side: str,
        size: float,
        *,
        leverage: Optional[int] = None,
        price: Optional[float] = None,
    ) -> dict:
        return self._post(
            "/api/orders",
            venue="hyperliquid",
            marketId=symbol.upper(),
            symbol=symbol.upper(),
            side=side,
            size=size,
            leverage=leverage or 1,
            price=price,
        )

    def close_hyperliquid_position(self, order_id: str) -> dict:
        return self._delete(f"/api/orders/{order_id}")

    def get_positions(self, *, venue: Optional[str] = None) -> dict:
        return self._get("/api/positions", venue=venue)

    # ── watchlist ─────────────────────────────────────────────────────

    def get_watchlist(self) -> dict:
        return self._get("/api/watchlist")

    def add_to_watchlist(
        self, type: str, id: str, *, venue: Optional[str] = None
    ) -> dict:
        return self._post("/api/watchlist", type=type, id=id, venue=venue)

    def remove_from_watchlist(self, type: str, id: str) -> dict:
        return self._delete(f"/api/watchlist/{type}/{id}")

    # ── user ─────────────────────────────────────────────────────────

    def get_user_profile(self) -> dict:
        return self._get("/api/user/me")
