"""Domain-analyst agents: each specialist proposes hypotheses in its mechanism family. An analyst is any
object with `name: str` and `propose(universe) -> list[Proposal]`. The LLM-backed analysts reason over the
feature warehouse; the FakeAnalyst is for tests (no LLM). All proposals clear the SAME gauntlet downstream —
no special treatment."""
from __future__ import annotations

import math
from dataclasses import dataclass, field

from fxhnt.domain.factory import candidate_key as _candidate_key


@dataclass(frozen=True, slots=True)
class Proposal:
    market: str
    kind: str
    params: dict[str, float] = field(default_factory=dict)

    def __post_init__(self) -> None:
        object.__setattr__(self, "params", {k: float(v) for k, v in self.params.items()})

    def key(self) -> str:
        return _candidate_key(self.market, self.kind, self.params)


class FakeAnalyst:
    """Deterministic analyst for tests — returns a fixed proposal list."""

    def __init__(self, name: str, proposals: list[Proposal]) -> None:
        self.name = name
        self._p = proposals

    def propose(self, universe: list[str]) -> list[Proposal]:
        return list(self._p)


class LlmAnalyst:
    """A domain specialist backed by an LLM. `specialty` shapes the prompt (momentum, carry, ...).
    Reads nothing here beyond the universe + specialty; the warehouse-feature context is injected via
    `context`. Output is parsed into Proposals; malformed lines are dropped (the fleet skips on error)."""

    def __init__(self, specialty: str, chat_model, *, kinds: list[str], context: str = "") -> None:
        self.name = specialty
        self._chat = chat_model
        self._kinds = [k.lower() for k in kinds]   # ONLY backtestable strategy kinds the engine supports
        self._ctx = context

    def propose(self, universe: list[str]) -> list[Proposal]:
        prompt = (
            f"You are a {self.name} strategy specialist. Universe (markets): {universe}.\n"
            f"kind MUST be EXACTLY one of: {self._kinds} — never invent other kinds. {self._ctx}\n"
            "Propose up to 5 testable hypotheses, one per line, EXACTLY as MARKET|KIND|key=val,key=val "
            "(numeric params only, e.g. window=100). Output only such lines, nothing else."
        )
        resp = self._chat.invoke(prompt)
        text = getattr(resp, "content", "")
        out: list[Proposal] = []
        for line in str(text).splitlines():
            parts = line.split("|")
            if len(parts) != 3 or parts[0].strip() not in universe:
                continue
            kind = parts[1].strip().lower()
            if kind not in self._kinds:               # drop hallucinated/unbacktestable kinds
                continue
            params: dict[str, float] = {}
            for kv in parts[2].split(","):
                if "=" in kv:
                    k, v = kv.split("=", 1)
                    try:
                        val = float(v)
                    except ValueError:
                        continue
                    if math.isfinite(val):
                        params[k.strip()] = val
            out.append(Proposal(parts[0].strip(), kind, params))
        return out
