"""Runs the analyst fleet: gather proposals across specialists and dedup them.

Counting authority is the HUNT (FleetOrchestrator), not the fleet.  `gather` is intentionally
read-only with respect to the store: it only calls `is_seen` to skip hypotheses already tested in
a prior cycle.  Within-batch dedup happens in `seen_now`.  No `add_trials` or `mark_seen` calls
are made here — both happen in FleetOrchestrator.hunt when a candidate is actually evaluated.

This removes the former double-count (fleet + hunt each incrementing trials) and the cross-namespace
key mismatch (fleet used ``market:kind:params``; hunt used ``kind|market|params``).  Both now share
`candidate_key` from `fxhnt.domain.factory`.

One bad analyst is logged and skipped — never fatal to the cycle.
"""
from __future__ import annotations

import logging
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from fxhnt.application.factory.analysts import Proposal

_log = logging.getLogger(__name__)


class AnalystFleet:
    def __init__(self, analysts: list, store) -> None:
        self._analysts = analysts
        self._store = store  # needs is_seen(key) — read-only; counting is done by the hunt

    def gather(self, *, universe: list[str]) -> list[Proposal]:
        """Return fresh (not-yet-seen) proposals from all analysts.

        Does NOT call add_trials or mark_seen — the FleetOrchestrator (hunt) is the single
        counting authority.  Proposals whose key is already in the store's seen set are filtered
        out so the hunt does not re-evaluate work from prior cycles.
        """
        fresh: list[Proposal] = []
        seen_now: set[str] = set()
        for analyst in self._analysts:
            try:
                proposals = analyst.propose(universe)
            except Exception as e:  # noqa: BLE001 — one bad specialist != no proposals
                _log.warning("analyst %s failed: %s", getattr(analyst, "name", "?"), e)
                continue
            for p in proposals:
                k = p.key()
                if k in seen_now or self._store.is_seen(k):
                    continue
                seen_now.add(k)
                fresh.append(p)
        return fresh
