"""Read-only NIP-01 query against every active relay via ``RelayBus``.

Sends a REQ with the caller-supplied filter to each relay listed in the
``relays`` registry (where ``active = true``), collects EVENTs until EOSE
or ``timeout_s``, deduplicates by ``id`` across relays, and returns the
events. Nothing is written to storage.

Used to back the ``relay_query`` MCP tool — diagnostic / discovery, never
the data-ingest path.
"""

from __future__ import annotations

import logging
from concurrent.futures import ThreadPoolExecutor
from typing import TYPE_CHECKING

from relay_bus import RelayBusError, RelayConnectError, RelayTimeout

if TYPE_CHECKING:
    from ingest_sql import IngestDaemonSQL

log = logging.getLogger("relay_query")

# Bound parallelism so a single tool call doesn't queue 100+ REQs at once
# on the bus loop. Each worker just submits a coroutine and blocks for the
# result; the loop itself runs all of them concurrently.
FANOUT_WORKERS = 8


def raw_query(
    daemon: "IngestDaemonSQL", filter: dict, timeout_s: float = 5.0,
    relays: list[str] | None = None,
) -> dict:
    # ``relays`` (already normalized by the caller) pins the query to a
    # specific set — including ad-hoc relays not in the registry, since the
    # bus opens any ws(s):// URL on demand. Without it we fan out to every
    # active relay.
    if relays is not None:
        urls = sorted(set(relays))
    else:
        urls = sorted(daemon._read_active_relay_urls())
    if not urls:
        return {"unique_events": 0, "events": [], "relays": []}

    by_id: dict[str, dict] = {}
    per_relay: list[dict] = []

    def _one(url: str) -> dict:
        try:
            evts = daemon.bus.req_sync(url, filter, timeout=timeout_s)
        except (RelayConnectError, RelayTimeout, RelayBusError, TimeoutError) as e:
            # ``req_sync`` waits on a concurrent.futures Future, so a hung REQ
            # surfaces as a builtin ``TimeoutError`` — distinct from our
            # ``RelayTimeout`` and easy to miss. Catching it keeps one stalled
            # relay from killing the whole fan-out with an opaque error.
            # ``str(TimeoutError())`` is empty, so fall back to the class name.
            msg = str(e) or e.__class__.__name__
            return {"url": url, "status": "error",
                    "events_received": 0, "error": msg[:200]}
        added = 0
        for evt in evts:
            eid = evt.get("id") if isinstance(evt, dict) else None
            if isinstance(eid, str) and eid not in by_id:
                by_id[eid] = evt
                added += 1
        return {"url": url, "status": "ok",
                "events_received": added, "error": None}

    workers = min(FANOUT_WORKERS, len(urls))
    with ThreadPoolExecutor(max_workers=workers, thread_name_prefix="relay-query") as pool:
        per_relay = list(pool.map(_one, urls))

    return {
        "unique_events": len(by_id),
        "events": list(by_id.values()),
        "relays": per_relay,
    }
