"""Market discovery for Polymarket, Kalshi, and Limitless.

Searches exchange APIs for markets matching filters and returns
Market objects ready for use with hz.run().
"""

from __future__ import annotations

import json as _json
import logging
import time
from typing import Any

import requests

from horizon._horizon import Event, Market, Outcome
from horizon.constants import GAMMA_API_URL as _GAMMA_API_URL, KALSHI_API_URL as _KALSHI_API_URL

logger = logging.getLogger("horizon.discovery")


def discover_markets(
    exchange: str = "polymarket",
    query: str = "",
    active: bool = True,
    limit: int = 20,
    min_volume: float = 0.0,
    category: str = "",
    sort_by: str = "volume",
    gamma_url: str = _GAMMA_API_URL,
    kalshi_url: str = _KALSHI_API_URL,
) -> list[Market]:
    """Search for markets on the specified exchange.

    Args:
        exchange: "polymarket", "kalshi", or "limitless".
        query: Search text (market title/slug).
        active: Only return active (open) markets.
        limit: Max number of results.
        min_volume: Minimum 24h volume filter.
        category: Category filter (exchange-specific).
        sort_by: Sort order - "volume" (default), "newest", "liquidity", or "".
        gamma_url: Override Polymarket Gamma API URL.
        kalshi_url: Override Kalshi API URL.

    Returns:
        List of Market objects populated with metadata.
    """
    if exchange == "polymarket":
        return _discover_polymarket(
            query=query, active=active, limit=limit,
            min_volume=min_volume, category=category,
            sort_by=sort_by, gamma_url=gamma_url,
        )
    elif exchange == "kalshi":
        return _discover_kalshi(
            query=query, active=active, limit=limit,
            category=category, sort_by=sort_by, kalshi_url=kalshi_url,
        )
    elif exchange == "limitless":
        return _discover_limitless(
            query=query, active=active, limit=limit,
        )
    else:
        raise ValueError(f"Unknown exchange: {exchange}. Use 'polymarket', 'kalshi', or 'limitless'.")


def _discover_polymarket(
    query: str,
    active: bool,
    limit: int,
    min_volume: float,
    category: str,
    sort_by: str,
    gamma_url: str,
) -> list[Market]:
    """Search Polymarket markets via Gamma API.

    When ``category`` is provided, uses the ``/events`` endpoint with
    server-side ``tag`` filtering (e.g., ``tag=crypto``).  Otherwise uses
    the ``/markets`` endpoint with client-side text matching.
    """
    if category:
        return _discover_polymarket_by_tag(
            tag=category, query=query, active=active, limit=limit,
            min_volume=min_volume, sort_by=sort_by, gamma_url=gamma_url,
        )

    # The Gamma API does NOT support server-side text search -- all query
    # params like slug_contains, _q, title_contains are silently ignored.
    # We must paginate through results and filter client-side.
    params: dict[str, Any] = {"limit": 200}
    if active:
        params["closed"] = "false"
    if min_volume > 0:
        params["volume_num_min"] = min_volume
    sort_map = {
        "volume": "-volume",
        "newest": "-createdAt",
        "liquidity": "-liquidityNum",
    }
    if sort_by in sort_map:
        params["_sort"] = sort_map[sort_by]

    query_lower = query.lower() if query else ""
    query_terms = query_lower.split() if query_lower else []
    markets: list[Market] = []

    if query_lower:
        # Search via /events (paginated) for broader coverage.
        # Events group related markets so we find more matches.
        seen_slugs: set[str] = set()
        max_pages = 5  # Up to 1000 events

        for page in range(max_pages):
            page_params = dict(params)
            page_params["offset"] = page * 200
            if active:
                page_params["active"] = "true"

            try:
                resp = requests.get(
                    f"{gamma_url}/events", params=page_params, timeout=15,
                )
                resp.raise_for_status()
                data = resp.json()
            except requests.RequestException as e:
                logger.debug("Polymarket event page %d failed: %s", page, e)
                break

            if not isinstance(data, list) or not data:
                break

            for event in data:
                event_title = str(event.get("title", "")).lower()
                event_slug = str(event.get("slug", "")).lower()
                event_desc = str(event.get("description", "")).lower()
                event_searchable = f"{event_title} {event_slug} {event_desc}"

                # Check if any query term matches the event
                if not any(t in event_searchable for t in query_terms):
                    continue

                event_markets = event.get("markets", [])
                if not isinstance(event_markets, list):
                    continue

                for item in event_markets:
                    slug = item.get("slug", item.get("question_id", ""))
                    if slug in seen_slugs:
                        continue
                    if active and item.get("closed", False):
                        continue
                    vol = float(item.get("volume", 0) or 0)
                    if vol < min_volume:
                        continue
                    m = _market_from_gamma_item(item)
                    if m is not None:
                        markets.append(m)
                        seen_slugs.add(slug)

            # Stop early if we have enough results
            if len(markets) >= limit * 3:
                break

        # Also search /markets directly for single-market matches
        for page in range(max_pages):
            page_params = dict(params)
            page_params["offset"] = page * 200

            try:
                resp = requests.get(
                    f"{gamma_url}/markets", params=page_params, timeout=15,
                )
                resp.raise_for_status()
                data = resp.json()
            except requests.RequestException as e:
                logger.debug("Polymarket market page %d failed: %s", page, e)
                break

            if not isinstance(data, list) or not data:
                break

            for item in data:
                slug = item.get("slug", item.get("question_id", ""))
                if slug in seen_slugs:
                    continue
                name = item.get("question", slug)
                searchable = f"{name} {slug}".lower()
                if not any(t in searchable for t in query_terms):
                    continue
                vol = float(item.get("volume", 0) or 0)
                if vol < min_volume:
                    continue
                m = _market_from_gamma_item(item)
                if m is not None:
                    markets.append(m)
                    seen_slugs.add(slug)

            if len(markets) >= limit * 3:
                break
    else:
        # No query -- just fetch top markets by volume (single page)
        fetch_limit = max(limit * 3, 100)
        params["limit"] = fetch_limit
        try:
            resp = requests.get(f"{gamma_url}/markets", params=params, timeout=15)
            resp.raise_for_status()
            data = resp.json()
            if not isinstance(data, list):
                data = [data]
            for item in data:
                vol = float(item.get("volume", 0) or 0)
                if vol < min_volume:
                    continue
                m = _market_from_gamma_item(item)
                if m is not None:
                    markets.append(m)
        except requests.RequestException as e:
            logger.error("Polymarket discovery failed: %s", e)

    return markets[:limit]


def _discover_polymarket_by_tag(
    tag: str,
    query: str,
    active: bool,
    limit: int,
    min_volume: float,
    sort_by: str,
    gamma_url: str,
) -> list[Market]:
    """Discover Polymarket markets via the /events endpoint with tag filtering.

    The Gamma ``/events`` endpoint supports server-side ``tag=<slug>``
    filtering (e.g., ``tag=crypto``, ``tag=politics``).  Each event contains
    nested ``markets`` which we extract into individual Market objects.
    """
    # Fetch more events than requested since each event can have multiple markets
    fetch_limit = max(limit * 3, 50)
    params: dict[str, Any] = {"limit": fetch_limit, "tag": tag.lower()}
    if active:
        params["active"] = "true"
        params["closed"] = "false"
    sort_map = {
        "volume": "-volume",
        "newest": "-createdAt",
        "liquidity": "-liquidityNum",
    }
    if sort_by in sort_map:
        params["_sort"] = sort_map[sort_by]

    try:
        resp = requests.get(f"{gamma_url}/events", params=params, timeout=15)
        resp.raise_for_status()
    except requests.RequestException as e:
        logger.error("Polymarket category discovery failed: %s", e)
        return []

    try:
        data = resp.json()
    except ValueError as e:
        logger.error("Polymarket returned invalid JSON: %s", e)
        return []
    if not isinstance(data, list):
        data = [data]

    tag_lower = tag.lower()
    query_lower = query.lower() if query else ""

    markets: list[Market] = []
    for event in data:
        # Verify the event actually has the requested tag slug
        # (Gamma does fuzzy matching, so we filter by exact tag slug)
        event_tags = event.get("tags", [])
        if isinstance(event_tags, list):
            tag_slugs = [
                t.get("slug", "").lower()
                for t in event_tags
                if isinstance(t, dict)
            ]
            if tag_lower not in tag_slugs:
                continue

        event_markets = event.get("markets", [])
        if not isinstance(event_markets, list):
            continue

        for item in event_markets:
            slug = item.get("slug", item.get("question_id", ""))
            name = item.get("question", slug)

            # Text filter
            if query_lower:
                searchable = f"{name} {slug}".lower()
                if query_lower not in searchable:
                    continue

            # Volume filter
            vol = float(item.get("volume", 0) or 0)
            if vol < min_volume:
                continue

            # Active filter
            if active and item.get("closed", False):
                continue

            m = _market_from_gamma_item(item)
            if m is not None:
                markets.append(m)

    # Sort extracted markets by volume descending if requested
    if sort_by == "volume":
        pass  # Already sorted by event volume from API; stable enough

    return markets[:limit]


def _market_from_gamma_item(item: dict) -> Market | None:
    """Convert a Gamma API market dict into a Market object."""
    slug = item.get("slug", item.get("question_id", ""))
    name = item.get("question", slug)
    if not slug:
        return None

    # Extract token IDs
    yes_token = None
    no_token = None
    tokens = item.get("tokens", [])
    if isinstance(tokens, list):
        for token in tokens:
            outcome = token.get("outcome", "").lower()
            tid = token.get("token_id")
            if outcome == "yes" and tid:
                yes_token = str(tid)
            elif outcome == "no" and tid:
                no_token = str(tid)

    if not yes_token:
        yes_token = item.get("yes_token_id")
    if not no_token:
        no_token = item.get("no_token_id")

    # Fallback: clobTokenIds may be a JSON string or a list
    if not yes_token:
        clob_ids = item.get("clobTokenIds") or []
        if isinstance(clob_ids, str):
            try:
                clob_ids = _json.loads(clob_ids)
            except (ValueError, TypeError):
                clob_ids = []
        if isinstance(clob_ids, list):
            if len(clob_ids) > 0 and clob_ids[0]:
                yes_token = str(clob_ids[0])
            if len(clob_ids) > 1 and clob_ids[1]:
                no_token = str(clob_ids[1])

    condition_id = item.get("condition_id") or item.get("conditionId")
    neg_risk = bool(item.get("neg_risk") or item.get("negRisk", False))
    expiry = item.get("end_date_iso") or item.get("endDate")
    is_active = bool(item.get("active", True))

    return Market(
        id=slug,
        name=name,
        slug=slug,
        exchange="polymarket",
        expiry=expiry,
        active=is_active,
        yes_token_id=yes_token,
        no_token_id=no_token,
        condition_id=condition_id,
        neg_risk=neg_risk,
    )


def _discover_limitless(
    query: str,
    active: bool,
    limit: int,
) -> list[Market]:
    """Search Limitless markets via public API.

    Fetches active markets from the Limitless API and filters client-side.
    Limitless is a prediction market on Base (chain ID 8453).
    """
    api_url = "https://api.limitless.exchange"
    try:
        resp = requests.get(f"{api_url}/markets/active", timeout=15)
        resp.raise_for_status()
    except requests.RequestException as e:
        logger.error("Limitless discovery failed: %s", e)
        return []
    try:
        raw = resp.json()
    except ValueError as e:
        logger.error("Limitless returned invalid JSON: %s", e)
        return []

    # API returns {"data": [...], "totalMarketsCount": N}
    if isinstance(raw, dict):
        data = raw.get("data", [])
    elif isinstance(raw, list):
        data = raw
    else:
        return []

    query_lower = query.lower() if query else ""
    markets: list[Market] = []

    for item in data:
        title = item.get("title", "")
        slug = item.get("slug", "")
        if not slug:
            continue

        status = item.get("status", "")
        is_active = status in ("FUNDED", "LOCKED") and not item.get("expired", False)
        if active and not is_active:
            continue

        if item.get("hidden", False):
            continue

        if query_lower:
            cats = " ".join(item.get("categories", []))
            searchable = f"{title} {slug} {cats}".lower()
            if query_lower not in searchable:
                continue

        expiry = item.get("expirationDate")
        condition_id = item.get("conditionId")

        tokens = item.get("tokens", {})
        yes_token = tokens.get("yes") if isinstance(tokens, dict) else None
        no_token = tokens.get("no") if isinstance(tokens, dict) else None

        m = Market(
            id=str(slug),
            name=title or str(slug),
            slug=str(slug),
            exchange="limitless",
            expiry=expiry,
            active=is_active,
            yes_token_id=str(yes_token) if yes_token else None,
            no_token_id=str(no_token) if no_token else None,
            condition_id=str(condition_id) if condition_id else None,
        )
        markets.append(m)

    return markets[:limit]


def _discover_kalshi(
    query: str,
    active: bool,
    limit: int,
    category: str,
    sort_by: str,
    kalshi_url: str,
) -> list[Market]:
    """Search Kalshi markets via public API.

    Uses server-side filtering where possible:
    - ``category`` maps to ``series_ticker`` (exact series filter).
    - ``query`` that looks like a ticker prefix is sent as ``series_ticker``
      for server-side matching; otherwise we paginate and filter client-side.
    """
    params: dict[str, Any] = {}
    if active:
        params["status"] = "open"

    # Determine server-side filtering strategy
    server_filtered = False
    if category:
        params["series_ticker"] = category
        server_filtered = True
    elif query and query == query.upper() and query.isalnum():
        # Looks like a ticker prefix (e.g., "KXBTC") -- use server-side filter
        params["series_ticker"] = query
        server_filtered = True

    # Fetch strategy: if server-filtered, a single page is usually enough.
    # For client-side text search, paginate to find enough matches.
    if server_filtered:
        params["limit"] = max(limit * 3, 100)
        all_items = _kalshi_fetch(kalshi_url, params)
    else:
        # Paginate up to 3 pages (600 markets) for client-side text search
        params["limit"] = 200
        all_items = _kalshi_fetch_paginated(kalshi_url, params, max_pages=3)

    query_lower = query.lower() if query else ""

    markets = []
    for item in all_items:
        ticker = item.get("ticker", "")
        title = item.get("title", ticker)
        subtitle = item.get("subtitle", "")
        event_ticker = item.get("event_ticker", "")

        # Client-side text filter against all relevant fields
        if query_lower and not server_filtered:
            searchable = f"{title} {subtitle} {ticker} {event_ticker}".lower()
            if query_lower not in searchable:
                continue

        expiry = item.get("expiration_time") or item.get("close_time")
        status = item.get("status", "")
        is_active = status in ("open", "active")

        m = Market(
            id=ticker.lower(),
            name=title,
            slug=ticker.lower(),
            exchange="kalshi",
            expiry=expiry,
            active=is_active,
            ticker=ticker,
        )
        markets.append(m)

    return markets[:limit]


def _kalshi_fetch(kalshi_url: str, params: dict[str, Any]) -> list[dict]:
    """Single-page Kalshi API fetch."""
    try:
        resp = requests.get(f"{kalshi_url}/markets", params=params, timeout=15)
        resp.raise_for_status()
    except requests.RequestException as e:
        logger.error("Kalshi discovery failed: %s", e)
        return []
    try:
        data = resp.json()
    except ValueError as e:
        logger.error("Kalshi returned invalid JSON: %s", e)
        return []
    items = data.get("markets", data) if isinstance(data, dict) else data
    return items if isinstance(items, list) else [items]


def _kalshi_fetch_paginated(
    kalshi_url: str, params: dict[str, Any], max_pages: int = 3
) -> list[dict]:
    """Paginate Kalshi API using cursor for broader text search."""
    all_items: list[dict] = []
    cursor = ""
    for _ in range(max_pages):
        page_params = dict(params)
        if cursor:
            page_params["cursor"] = cursor
        try:
            resp = requests.get(
                f"{kalshi_url}/markets", params=page_params, timeout=15
            )
            resp.raise_for_status()
        except requests.RequestException as e:
            logger.error("Kalshi discovery failed: %s", e)
            break
        try:
            data = resp.json()
        except ValueError:
            break
        items = data.get("markets", []) if isinstance(data, dict) else data
        if not isinstance(items, list) or not items:
            break
        all_items.extend(items)
        cursor = data.get("cursor", "") if isinstance(data, dict) else ""
        if not cursor:
            break
    return all_items


# ---------------------------------------------------------------------------
# Multi-outcome event discovery
# ---------------------------------------------------------------------------


def _extract_outcome_name(question: str, event_title: str) -> str:
    """Derive a short outcome name from a market question and event title.

    Strips the event title prefix and common suffixes like "wins?" or "?"
    to produce a concise name like "Trump" from "Will Trump win the election?".
    """
    name = question
    if event_title and question.startswith(event_title):
        name = question[len(event_title):].strip(" -:")

    for suffix in ("?", " wins", " win", " to win"):
        if name.endswith(suffix):
            name = name[: -len(suffix)].strip()

    for prefix in ("Will ", "Does ", "Is ", "Can "):
        if name.startswith(prefix):
            name = name[len(prefix):]

    return name.strip() or question


def discover_events(
    exchange: str = "polymarket",
    query: str = "",
    active: bool = True,
    limit: int = 10,
    min_volume: float = 0.0,
    gamma_url: str = _GAMMA_API_URL,
) -> list[Event]:
    """Discover multi-outcome events from Polymarket.

    Hits the Gamma API /events endpoint and returns Event objects
    with all outcomes populated, including YES/NO token IDs and prices.

    Args:
        exchange: Currently only "polymarket" is supported.
        query: Search text for event title/slug.
        active: Only return active events.
        limit: Max number of events to return.
        min_volume: Minimum volume filter.
        gamma_url: Override Gamma API URL.

    Returns:
        List of Event objects with Outcome children.
    """
    if exchange != "polymarket":
        logger.warning("discover_events only supports polymarket, got %s", exchange)
        return []

    # The Gamma API does NOT support server-side text search.
    # We paginate and filter client-side.
    query_lower = query.lower() if query else ""
    query_terms = query_lower.split() if query_lower else []

    # Collect raw event dicts across pages
    all_event_data: list[dict] = []
    if query_lower:
        # Paginate to find matches across all events
        max_pages = 5
        for page in range(max_pages):
            params: dict[str, Any] = {"limit": 200, "offset": page * 200}
            if active:
                params["closed"] = "false"
                params["active"] = "true"
            try:
                resp = requests.get(f"{gamma_url}/events", params=params, timeout=15)
                resp.raise_for_status()
                data = resp.json()
            except (requests.RequestException, ValueError) as e:
                logger.debug("Polymarket event page %d failed: %s", page, e)
                break
            if not isinstance(data, list) or not data:
                break
            all_event_data.extend(data)
            # Stop early if we've found enough
            matched = sum(
                1 for d in all_event_data
                if any(t in f"{d.get('title','')} {d.get('slug','')} {d.get('description','')}".lower() for t in query_terms)
            )
            if matched >= limit * 3:
                break
    else:
        params_no_q: dict[str, Any] = {"limit": max(limit * 3, 50)}
        if active:
            params_no_q["closed"] = "false"
        try:
            resp = requests.get(f"{gamma_url}/events", params=params_no_q, timeout=15)
            resp.raise_for_status()
            data = resp.json()
            if not isinstance(data, list):
                data = [data]
            all_event_data = data
        except (requests.RequestException, ValueError) as e:
            logger.error("Polymarket event discovery failed: %s", e)
            return []

    events = []
    for item in all_event_data:
        event_id = str(item.get("slug", item.get("id", "")))
        event_title = str(item.get("title", event_id))

        # Client-side text filter
        if query_terms:
            event_desc = str(item.get("description", "")).lower()
            searchable = f"{event_title} {event_id} {event_desc}".lower()
            if not any(t in searchable for t in query_terms):
                continue
        condition_id = item.get("condition_id") or item.get("conditionId")
        neg_risk = bool(item.get("neg_risk") or item.get("negRisk", False))
        expiry = item.get("end_date_iso") or item.get("endDate")

        event_markets = item.get("markets", [])
        if not isinstance(event_markets, list) or len(event_markets) < 2:
            continue

        total_vol = sum(float(m.get("volume", 0) or 0) for m in event_markets)
        if total_vol < min_volume:
            continue

        outcomes = []
        for mkt in event_markets:
            mkt_slug = str(mkt.get("slug", mkt.get("question_id", "")))
            question = str(mkt.get("question", mkt_slug))
            outcome_name = _extract_outcome_name(question, event_title)

            yes_token = None
            no_token = None
            tokens = mkt.get("tokens", [])
            if isinstance(tokens, list):
                for token in tokens:
                    outcome_label = str(token.get("outcome", "")).lower()
                    tid = token.get("token_id")
                    if outcome_label == "yes" and tid:
                        yes_token = str(tid)
                    elif outcome_label == "no" and tid:
                        no_token = str(tid)

            if not yes_token:
                yes_token = mkt.get("yes_token_id")
            if not no_token:
                no_token = mkt.get("no_token_id")

            # Fallback: clobTokenIds may be a JSON string or a list
            if not yes_token:
                clob_ids = mkt.get("clobTokenIds") or []
                if isinstance(clob_ids, str):
                    try:
                        import json as _json_evt
                        clob_ids = _json_evt.loads(clob_ids)
                    except (ValueError, TypeError):
                        clob_ids = []
                if isinstance(clob_ids, list):
                    if len(clob_ids) > 0 and clob_ids[0]:
                        yes_token = str(clob_ids[0])
                    if len(clob_ids) > 1 and clob_ids[1]:
                        no_token = str(clob_ids[1])

            # outcomePrices may be a JSON array string like '["0.55", "0.45"]'
            # or a comma-separated string like "0.55,0.45"
            outcome_prices_raw = mkt.get("outcomePrices", "")
            yes_price = 0.0
            if outcome_prices_raw:
                try:
                    import json as _json
                    parsed_prices = _json.loads(outcome_prices_raw) if isinstance(outcome_prices_raw, str) else outcome_prices_raw
                    if isinstance(parsed_prices, list) and len(parsed_prices) > 0:
                        yes_price = float(parsed_prices[0] or 0)
                except (ValueError, TypeError):
                    # Fallback: try comma-separated
                    try:
                        yes_price = float(str(outcome_prices_raw).split(",")[0] or 0)
                    except (ValueError, TypeError):
                        yes_price = 0.0
            if yes_price == 0:
                yes_price = float(mkt.get("bestAsk", 0) or 0)

            outcomes.append(Outcome(
                name=outcome_name,
                market_id=mkt_slug,
                yes_token_id=yes_token,
                no_token_id=no_token,
                yes_price=yes_price,
            ))

        event = Event(
            id=event_id,
            name=event_title,
            outcomes=outcomes,
            neg_risk=neg_risk,
            exchange="polymarket",
            condition_id=str(condition_id) if condition_id else None,
            expiry=expiry,
        )
        events.append(event)

    return events[:limit]


# ---------------------------------------------------------------------------
# Convenience helpers
# ---------------------------------------------------------------------------


def top_markets(
    exchange: str = "polymarket",
    limit: int = 10,
    category: str = "",
    gamma_url: str = _GAMMA_API_URL,
    kalshi_url: str = _KALSHI_API_URL,
) -> list[Market]:
    """Get the top markets by volume.

    Convenience wrapper around discover_markets with sensible defaults.

    Args:
        exchange: "polymarket", "kalshi", or "limitless".
        limit: Number of markets to return.
        category: Optional category filter.

    Returns:
        Top markets sorted by volume (descending).
    """
    return discover_markets(
        exchange=exchange,
        active=True,
        limit=limit,
        sort_by="volume",
        category=category,
        gamma_url=gamma_url,
        kalshi_url=kalshi_url,
    )


# ---------------------------------------------------------------------------
# Market detail
# ---------------------------------------------------------------------------


def get_market_detail(
    slug_or_id: str,
    exchange: str = "polymarket",
    gamma_url: str = _GAMMA_API_URL,
) -> dict:
    """Fetch full info about a single market.

    Returns a dict with sections: market, prices, price_history,
    orderbook, recent_trades, top_holders.
    """
    if exchange == "polymarket":
        return _polymarket_detail(slug_or_id, gamma_url=gamma_url)
    elif exchange == "kalshi":
        return _kalshi_detail(slug_or_id)
    else:
        raise ValueError(f"Unknown exchange: {exchange}. Use 'polymarket' or 'kalshi'.")


def _polymarket_detail(slug: str, gamma_url: str = _GAMMA_API_URL) -> dict:
    """Aggregate Polymarket market info from Gamma + CLOB APIs."""
    # 1. Metadata from Gamma
    try:
        resp = requests.get(
            f"{gamma_url}/markets",
            params={"slug": slug, "limit": 1},
            timeout=15,
        )
        resp.raise_for_status()
        data = resp.json()
    except Exception as e:
        raise RuntimeError(f"Failed to fetch Polymarket market metadata: {e}") from e

    if not isinstance(data, list) or len(data) == 0:
        raise ValueError(f"Market not found: {slug}")

    item = data[0]
    name = item.get("question", slug)
    description = item.get("description", "")
    created_at = item.get("createdAt") or item.get("created_at")
    expiry = item.get("end_date_iso") or item.get("endDate")
    volume = float(item.get("volume", 0) or 0)
    liquidity = float(item.get("liquidityNum", 0) or item.get("liquidity", 0) or 0)
    image = item.get("image")
    condition_id = item.get("condition_id") or item.get("conditionId")
    is_active = bool(item.get("active", True))

    # Extract token IDs
    yes_token = None
    no_token = None
    tokens = item.get("tokens", [])
    for token in tokens:
        outcome = token.get("outcome", "").lower()
        tid = token.get("token_id")
        if outcome == "yes" and tid:
            yes_token = str(tid)
        elif outcome == "no" and tid:
            no_token = str(tid)

    if not yes_token:
        clob_ids = item.get("clobTokenIds") or []
        if isinstance(clob_ids, str):
            try:
                clob_ids = _json.loads(clob_ids)
            except (ValueError, TypeError):
                clob_ids = []
        if isinstance(clob_ids, list):
            if len(clob_ids) > 0 and clob_ids[0]:
                yes_token = str(clob_ids[0])
            if len(clob_ids) > 1 and clob_ids[1]:
                no_token = str(clob_ids[1])

    # outcomePrices
    outcome_prices_raw = item.get("outcomePrices", "")
    yes_price = 0.0
    no_price = 0.0
    if outcome_prices_raw:
        try:
            parsed = _json.loads(outcome_prices_raw) if isinstance(outcome_prices_raw, str) else outcome_prices_raw
            if isinstance(parsed, list) and len(parsed) >= 2:
                yes_price = float(parsed[0] or 0)
                no_price = float(parsed[1] or 0)
            elif isinstance(parsed, list) and len(parsed) == 1:
                yes_price = float(parsed[0] or 0)
                no_price = round(1.0 - yes_price, 4)
        except (ValueError, TypeError):
            pass

    result: dict[str, Any] = {
        "market": {
            "name": name,
            "description": description,
            "slug": slug,
            "exchange": "polymarket",
            "created_at": created_at,
            "expiry": expiry,
            "volume": round(volume, 2),
            "liquidity": round(liquidity, 2),
            "active": is_active,
            "image": image,
            "condition_id": condition_id,
        },
        "prices": {
            "yes_price": round(yes_price, 4),
            "no_price": round(no_price, 4),
        },
    }

    # 2. Price history (last 7 days) -- best effort
    if yes_token:
        try:
            from horizon._horizon import fetch_polymarket_prices
            now = time.time()
            week_ago = now - 7 * 86400
            bars = fetch_polymarket_prices(yes_token, week_ago, now, 60)
            result["price_history"] = [
                {"timestamp": b.timestamp, "price": round(b.close, 4)} for b in bars
            ]
        except Exception:
            result["price_history"] = []
    else:
        result["price_history"] = []

    # 3. Orderbook -- best effort
    if yes_token:
        try:
            from horizon._horizon import fetch_polymarket_book
            book = fetch_polymarket_book(yes_token)
            bids = [{"price": round(p, 4), "size": round(s, 2)} for p, s in book.bids()]
            asks = [{"price": round(p, 4), "size": round(s, 2)} for p, s in book.asks()]
            spread = round(book.spread(), 4)
            result["orderbook"] = {"bids": bids, "asks": asks, "spread": spread}
        except Exception:
            result["orderbook"] = {"bids": [], "asks": [], "spread": None}
    else:
        result["orderbook"] = {"bids": [], "asks": [], "spread": None}

    # 4. Recent trades -- best effort
    if condition_id:
        try:
            from horizon.flow import get_market_trades as _get_mkt_trades
            trades = _get_mkt_trades(condition_id, limit=20)
            result["recent_trades"] = [
                {
                    "side": t.side,
                    "price": round(t.price, 4),
                    "size": round(t.size, 4),
                    "timestamp": t.timestamp,
                }
                for t in trades
            ]
        except Exception:
            result["recent_trades"] = []
    else:
        result["recent_trades"] = []

    # 5. Top holders -- best effort
    if condition_id:
        try:
            from horizon.flow import get_top_holders as _get_holders
            holders = _get_holders(condition_id, limit=10)
            result["top_holders"] = [
                {
                    "wallet": h.wallet,
                    "amount": round(h.amount, 4),
                    "pseudonym": h.pseudonym,
                }
                for h in holders
            ]
        except Exception:
            result["top_holders"] = []
    else:
        result["top_holders"] = []

    return result


def _kalshi_detail(ticker: str) -> dict:
    """Aggregate Kalshi market info from REST API."""
    # Kalshi tickers are uppercase; normalize
    ticker = ticker.upper()

    # 1. Metadata
    try:
        from horizon._horizon import fetch_kalshi_market
        raw_json = fetch_kalshi_market(ticker)
        item = _json.loads(raw_json)
    except Exception as e:
        raise RuntimeError(f"Failed to fetch Kalshi market metadata: {e}") from e

    title = item.get("title", ticker)
    subtitle = item.get("subtitle", "")
    expiry = item.get("expiration_time") or item.get("close_time")
    status = item.get("status", "")
    volume = int(item.get("volume", 0) or 0)
    liquidity = float(item.get("liquidity", 0) or item.get("liquidity_dollars", 0) or 0)
    # Prefer dollar-denominated fields (0-1 range); fall back to cent-based / 100
    yes_price = float(item.get("yes_bid_dollars", 0) or 0)
    if yes_price == 0:
        yes_price = float(item.get("yes_bid", 0) or 0) / 100.0
    no_price = float(item.get("no_bid_dollars", 0) or 0)
    if no_price == 0:
        no_price = float(item.get("no_bid", 0) or 0) / 100.0
    if yes_price == 0 and no_price == 0:
        last = float(item.get("last_price_dollars", 0) or item.get("last_price", 0) or 0)
        if last > 0:
            yes_price = last
            no_price = round(1.0 - last, 4)

    result: dict[str, Any] = {
        "market": {
            "name": title,
            "description": subtitle,
            "slug": ticker.lower(),
            "exchange": "kalshi",
            "created_at": item.get("open_time"),
            "expiry": expiry,
            "volume": volume,
            "liquidity": round(liquidity, 2),
            "active": status in ("open", "active"),
            "image": None,
        },
        "prices": {
            "yes_price": round(yes_price, 4),
            "no_price": round(no_price, 4),
        },
        "price_history": [],
    }

    # 2. Orderbook -- best effort
    try:
        from horizon._horizon import fetch_kalshi_book
        book = fetch_kalshi_book(ticker)
        bids = [{"price": round(p, 4), "size": round(s, 2)} for p, s in book.bids()]
        asks = [{"price": round(p, 4), "size": round(s, 2)} for p, s in book.asks()]
        spread = round(book.spread(), 4)
        result["orderbook"] = {"bids": bids, "asks": asks, "spread": spread}
    except Exception:
        result["orderbook"] = {"bids": [], "asks": [], "spread": None}

    result["recent_trades"] = []
    result["top_holders"] = []

    return result
