"""The verifiers v1 taskset.

Optional: everything else in this package runs, and is gated in CI, without
verifiers installed. Only this file needs it, so the correctness of the game,
the reward and the probe never depends on an RL stack being resolvable.

Install with:  uv sync --all-packages --extra verifiers
"""

from __future__ import annotations

from typing import Any

from .engine import MAX_GUESSES, Game, answer_for_seed
from .protocol import parse_guess, render_feedback, render_rejection, system_prompt
from .reward import ROLES, WEIGHTS, Episode, metrics, score
from .solver import reference_depth

TASKSET_ID = "wordle-five"


def play_episode(seed: int, respond, max_guesses: int = MAX_GUESSES) -> dict[str, Any]:
    """Drive one full episode against a callable that returns reply text.

    Split out from the verifiers plumbing on purpose: this function is the
    whole game loop, it has no RL dependency, and it is what the probe, the
    tests and the fixture generator all use. The verifiers Env below is a thin
    adapter over it, so the thing being evaluated and the thing being tested
    cannot drift apart.
    """
    game = Game(seed=seed, max_guesses=max_guesses)
    transcript: list[dict[str, Any]] = []
    prompt = system_prompt()

    while not game.over:
        turns_left = max_guesses - len(game.history)
        reply = respond(prompt, transcript)
        guess = parse_guess(reply)

        if guess is None:
            game.rejected += 1
            observation = render_rejection("no bracketed guess found", turns_left)
        else:
            pattern, rejection = game.play(guess)
            if rejection is not None:
                observation = render_rejection(rejection, turns_left)
            else:
                assert pattern is not None
                observation = render_feedback(guess, pattern, max_guesses - len(game.history))

        transcript.append({"reply": reply, "guess": guess, "observation": observation})

        # A model that only ever emits refusals would otherwise loop until the
        # process is killed; the game itself has no notion of a wasted turn.
        if game.rejected >= max_guesses:
            break

    episode = Episode(
        answer=game.answer,
        guesses=[g for g, _ in game.history],
        patterns=[p for _, p in game.history],
        rejected=game.rejected,
        reference_depth=reference_depth(game.answer),
        max_guesses=max_guesses,
    )
    return {
        "seed": seed,
        "answer": game.answer,
        "solved": game.solved,
        "outcome": "solved" if game.solved else "failed",
        "transcript": transcript,
        "rewards": score(episode),
        "weights": WEIGHTS,
        "roles": ROLES,
        "metrics": metrics(episode),
    }


def build_taskset():  # pragma: no cover - requires the optional extra
    """The verifiers v1 Taskset. Imported lazily so the module stays optional."""
    import verifiers as vf

    class WordleFiveEnv(vf.Env):
        """One agent seat, `player`, playing against the engine host-side.

        The engine plays the user role rather than the model conversing with a
        second model — the feedback is computed, not generated, which is the
        whole reason this reward is verifiable.
        """

        async def run(self, task, agent, trace):
            game = Game(seed=task.data.info["seed"])
            turns = 0
            async with agent.interaction(system=system_prompt()) as session:
                message = "Enter your guess to begin."
                while not game.over and turns < MAX_GUESSES * 2:
                    reply = await session.turn(message)
                    guess = parse_guess(reply)
                    left = MAX_GUESSES - len(game.history)
                    if guess is None:
                        game.rejected += 1
                        message = render_rejection("no bracketed guess found", left)
                    else:
                        pattern, rejection = game.play(guess)
                        message = (
                            render_rejection(rejection, left)
                            if rejection is not None
                            else render_feedback(guess, pattern or "", MAX_GUESSES - len(game.history))
                        )
                    turns += 1
                    if game.rejected >= MAX_GUESSES:
                        break

                episode = Episode(
                    answer=game.answer,
                    guesses=[g for g, _ in game.history],
                    patterns=[p for _, p in game.history],
                    rejected=game.rejected,
                    reference_depth=reference_depth(game.answer),
                )
                # Graded INSIDE the interaction, so the trace carries the
                # rewards rather than having them stapled on afterwards.
                for key, value in score(episode).items():
                    trace.record_reward(key, value, WEIGHTS[key])
                for key, value in metrics(episode).items():
                    trace.record_reward(key, value, 0.0)
                trace.info["answer"] = game.answer
                trace.info["outcome"] = "solved" if game.solved else "failed"

    return WordleFiveEnv
