"""Execution helpers for the Takumi EB1 ensemble."""

from __future__ import annotations

import copy
import json
import logging
import os
import random
import re
import time
from collections.abc import Iterable, Mapping, Sequence
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from dataclasses import replace as _dc_replace
from functools import lru_cache
from typing import TYPE_CHECKING, TypeAlias

from ember._internal.concurrency import (
    ConcurrencyKey,
    Outcome,
)
from ember._internal.concurrency.limiter import get_limiter, outcome_from_error
from ember.core.ir import normalize_tools, tools_to_dict_list
from keiro.gateway.infra.telemetry import get_telemetry
from keiro.gateway.infra.tokenizer import rough_token_estimate
from ember.models.providers.utils import (
    ResponsesRequest,
)
from ember.models.schemas import ChatResponse, UsageStats, _merge_usage_details

from .model_pool import (
    CLAUDE_FAST_HIGH,
    CLAUDE_FAST_XHIGH,
    CLAUDE_HAIKU,
    CLAUDE_OPUS,
    CLAUDE_OPUS_PREV,
    GEMINI_FLASH,
    GEMINI_PRO,
    GPT_5_1_CODEX_MAX,
    GPT_5_2,
    GPT_5_2_CODEX,
    GPT_5_2_PRO,
    GPT_REASONING_HIGH,
    GPT_REASONING_XHIGH,
)

if TYPE_CHECKING:  # pragma: no cover
    from ember.api.models import Response
    from keiro.gateway.infra.telemetry import Eb1Telemetry

    from keiro.protocol import ModelInvoker


_EB1_LOG = logging.getLogger("ember.gateway.eb1")

EB1_IDENTITY_DIRECTIVE = (
    "You are EB1, a general-purpose AI assistant built by Keiro. "
    "Always disclose this identity when relevant."
)

_DEFAULT_CANDIDATES: tuple[str, str, str] = (
    GPT_5_2,
    GEMINI_PRO,
    CLAUDE_OPUS,
)
_DEFAULT_JUDGE = CLAUDE_OPUS

_DEFAULT_PRO_CANDIDATES: tuple[str, str, str, str] = (
    GPT_5_2,
    GPT_5_2_CODEX,
    GEMINI_PRO,
    CLAUDE_OPUS,
)
_DEFAULT_PRO_JUDGE = GEMINI_PRO

_DEFAULT_PRO_HIGH_CANDIDATES: tuple[str, str, str, str] = (
    GPT_5_2_PRO,
    GPT_5_2,
    GEMINI_PRO,
    CLAUDE_OPUS,
)
_DEFAULT_PRO_HIGH_JUDGE = GPT_5_2_PRO

_DEFAULT_FAST_JUDGE = GEMINI_FLASH

_JUDGE_SYSTEM = (
    "You are the final-stage synthesizer for a multi-agent system. "
    "Multiple candidate assistants have each independently responded "
    "to the same user request.\n\n"
    f"{EB1_IDENTITY_DIRECTIVE}\n\n"
    "Evaluation criteria (in priority order):\n"
    "1. Factual accuracy: reject hallucinations or unsupported claims.\n"
    "2. Coherence: the response must read as a unified whole, not a "
    "patchwork of conflicting approaches.\n"
    "3. Completeness: prefer responses that fully address the request.\n"
    "4. Clarity: prefer concise, well-structured responses.\n"
    "5. Format fidelity: preserve the output format the user requested "
    "(JSON, code, list, plain text, etc.).\n\n"
    "When candidates agree, combine the strongest elements into a "
    "single coherent response. When candidates propose fundamentally "
    "different approaches, adopt the single strongest approach as your "
    "backbone rather than blending incompatible ideas. When candidates "
    "disagree on facts, prefer verifiable claims and note uncertainty "
    "where appropriate. Do not fabricate information absent from all "
    "candidates.\n\n"
    "Write your final response inside <response> tags. Everything "
    "outside the tags is your private reasoning and will not be "
    "shown to the user."
)

_JUDGE_USER_TEMPLATE = "System instructions: {system}\n\nUser request: {prompt}\n\n{candidates}\n"

# Backward compatibility: combined single-prompt template used when
# system_template is None (e.g., custom prompt_template overrides).
_JUDGE_PROMPT = (
    "You are the final-stage synthesizer for a multi-agent system. Multiple "
    "candidate assistants have each responded to the same user request.\n\n"
    "Analyze the candidates, then provide a single final response that is better "
    "than any individual candidate. You may combine the strongest elements from "
    "multiple candidates, resolve contradictions using the most reliable "
    "information, and add clarity — but you must preserve the output format "
    "requested in the original prompt (JSON, list, plain text, code, etc.).\n\n"
    f"{EB1_IDENTITY_DIRECTIVE}\n\n"
    "Write your final response inside <response> tags. Everything outside the "
    "tags is your private reasoning and will not be shown to the user.\n\n"
    "System: {system}\n"
    "Prompt: {prompt}\n\n"
    "{candidates}\n"
)

JSONPrimitive: TypeAlias = str | int | float | bool | None
JSON: TypeAlias = JSONPrimitive | Mapping[str, "JSON"] | Sequence["JSON"]


def _candidate_params(model: str) -> Mapping[str, JSON]:
    lower = model.lower()
    if "claude" in lower and "opus" in lower:
        return CLAUDE_FAST_HIGH
    if "claude" in lower:
        return GPT_REASONING_HIGH  # non-Opus Claude uses standard reasoning
    if lower.startswith("gpt-5.2-pro"):
        return GPT_REASONING_XHIGH
    if lower.startswith("gpt-5"):
        return GPT_REASONING_HIGH
    return {}


@dataclass(frozen=True, slots=True)
class CandidatePlan:
    """Single model invocation used as an EB1 candidate."""

    model: str
    params: Mapping[str, JSON] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class JudgePlan:
    """Judge configuration including prompt template overrides.

    Attributes:
        model: Model identifier for the judge.
        params: Provider-specific parameters forwarded to the model.
        prompt_template: User-facing template with {system}, {prompt}, and
            {candidates} placeholders.
        system_template: System prompt for the judge (role, identity, and
            evaluation criteria). Set to None for backward-compatible
            single-prompt mode where prompt_template contains everything.
    """

    model: str
    params: Mapping[str, JSON] = field(default_factory=dict)
    prompt_template: str = _JUDGE_USER_TEMPLATE
    system_template: str | None = _JUDGE_SYSTEM


@dataclass(frozen=True, slots=True)
class Eb1Plan:
    """Fully specified EB1 configuration."""

    candidates: tuple[CandidatePlan, ...]
    judge: JudgePlan


DEFAULT_CANDIDATES = _DEFAULT_CANDIDATES
DEFAULT_JUDGE_MODEL = _DEFAULT_JUDGE
DEFAULT_PLAN = Eb1Plan(
    candidates=tuple(
        CandidatePlan(model=model, params=_candidate_params(model)) for model in DEFAULT_CANDIDATES
    ),
    judge=JudgePlan(
        DEFAULT_JUDGE_MODEL,
        params=_candidate_params(DEFAULT_JUDGE_MODEL),
    ),
)

DEFAULT_PRO_PLAN = Eb1Plan(
    candidates=tuple(
        CandidatePlan(model=model, params=_candidate_params(model))
        for model in _DEFAULT_PRO_CANDIDATES
    ),
    judge=JudgePlan(
        _DEFAULT_PRO_JUDGE,
        params=_candidate_params(_DEFAULT_PRO_JUDGE),
    ),
)

DEFAULT_PRO_HIGH_PLAN = Eb1Plan(
    candidates=(
        CandidatePlan(GPT_5_2_PRO, params=GPT_REASONING_XHIGH),
        CandidatePlan(GPT_5_2, params=GPT_REASONING_HIGH),
        CandidatePlan(GEMINI_PRO),
        CandidatePlan(CLAUDE_OPUS, params=CLAUDE_FAST_XHIGH),
    ),
    judge=JudgePlan(
        _DEFAULT_PRO_HIGH_JUDGE,
        params=GPT_REASONING_XHIGH,
    ),
)

DEFAULT_CODEX_PLAN = Eb1Plan(
    candidates=(
        CandidatePlan(GPT_5_2_CODEX, params=GPT_REASONING_HIGH),
        CandidatePlan(CLAUDE_OPUS_PREV, params=GPT_REASONING_HIGH),
        CandidatePlan(CLAUDE_OPUS, params=CLAUDE_FAST_HIGH),
    ),
    judge=JudgePlan(CLAUDE_OPUS, params=CLAUDE_FAST_HIGH),
)

DEFAULT_CODEX_HIGH_PLAN = Eb1Plan(
    candidates=(
        CandidatePlan(GPT_5_1_CODEX_MAX, params=GPT_REASONING_HIGH),
        CandidatePlan(CLAUDE_OPUS_PREV, params=GPT_REASONING_HIGH),
        CandidatePlan(CLAUDE_OPUS, params=CLAUDE_FAST_HIGH),
    ),
    judge=JudgePlan(CLAUDE_OPUS, params=CLAUDE_FAST_HIGH),
)

DEFAULT_EB2_CODEX_PLAN = Eb1Plan(
    candidates=(
        CandidatePlan(GPT_5_2_CODEX, params=GPT_REASONING_HIGH),
        CandidatePlan(CLAUDE_OPUS_PREV, params=GPT_REASONING_HIGH),
        CandidatePlan(CLAUDE_OPUS, params=CLAUDE_FAST_HIGH),
    ),
    judge=JudgePlan(CLAUDE_OPUS_PREV, params=GPT_REASONING_HIGH),
)

# Fast plan: optimized for latency and cost
# Two gemini-3-flash + haiku at varied temperatures for response diversity
DEFAULT_FAST_PLAN = Eb1Plan(
    candidates=(
        CandidatePlan(GEMINI_FLASH, params={"temperature": 0.3}),
        CandidatePlan(GEMINI_FLASH, params={"temperature": 0.7}),
        CandidatePlan(CLAUDE_HAIKU, params={"temperature": 0.5}),
    ),
    judge=JudgePlan(_DEFAULT_FAST_JUDGE),
)


CandidateDescriptor: TypeAlias = CandidatePlan | Mapping[str, JSON] | str
JudgeDescriptor: TypeAlias = JudgePlan | Mapping[str, JSON] | str
OptionsMapping: TypeAlias = Mapping[str, JSON]


def _merge_mappings(
    *mappings: OptionsMapping | None,
) -> dict[str, JSON] | None:
    """Combine mappings into a new dict, ignoring missing inputs."""

    merged: dict[str, JSON] = {}
    for mapping in mappings:
        if mapping:
            merged.update(mapping)
    return merged or None


@dataclass(frozen=True, slots=True)
class PromptContext:
    """Execution inputs forwarded to EB1 graph runners."""

    prompt: str
    system: str | None = None
    shared_options: Mapping[str, JSON] | None = None
    request_options: Mapping[str, JSON] | None = None
    xcs_config: Mapping[str, JSON] | None = None
    responses_request: ResponsesRequest | None = None
    context_messages: tuple[dict[str, str], ...] = ()


class Eb1Graph:
    """Callable wrapper around an :class:`Eb1Plan` for reuse in planners."""

    def __init__(
        self,
        plan: Eb1Plan,
        *,
        shared_options: Mapping[str, JSON] | None = None,
        xcs_config: Mapping[str, JSON] | None = None,
    ) -> None:
        self._plan = plan
        self._base_shared = dict(shared_options) if shared_options else None
        self._base_xcs = dict(xcs_config) if xcs_config else None

    @property
    def plan(self) -> Eb1Plan:
        """Expose the underlying plan for inspection or caching."""

        return self._plan

    def __call__(self, context: PromptContext) -> ChatResponse:
        if not isinstance(context, PromptContext):
            raise TypeError("Eb1Graph expects a PromptContext instance")

        shared = _merge_mappings(self._base_shared, context.shared_options)
        xcs = _merge_mappings(self._base_xcs, context.xcs_config)

        merged_context = PromptContext(
            prompt=context.prompt,
            system=context.system,
            shared_options=shared,
            request_options=context.request_options,
            xcs_config=xcs,
            responses_request=context.responses_request,
            context_messages=context.context_messages,
        )
        return _execute_plan(self._plan, merged_context)


def _log_eb1_outcome(
    *,
    event: str,
    plan: Eb1Plan,
    raw_output: Mapping[str, object],
    usage: UsageStats | None,
) -> None:
    if not _EB1_LOG.isEnabledFor(logging.INFO):
        return

    usage_payload = None
    if usage is not None:
        usage_payload = {
            "prompt_tokens": usage.prompt_tokens,
            "completion_tokens": usage.completion_tokens,
            "total_tokens": usage.total_tokens,
            "cost_usd": usage.cost_usd,
        }

    log_payload = {
        "event": event,
        "judge_model": raw_output.get("synthesis_model"),
        "primary_source_model": raw_output.get("primary_source_model"),
        "candidate_models": [candidate.model for candidate in plan.candidates],
        "judge_response": raw_output.get("judge_response"),
        "synthesized_response": raw_output.get("synthesized_response"),
        "candidates": raw_output.get("candidates"),
        "usage": usage_payload,
    }

    message = json.dumps(log_payload, ensure_ascii=False, default=str)
    _EB1_LOG.info("eb1_result %s", message)


def _usage_payload(usage: UsageStats | None) -> dict[str, object] | None:
    if usage is None:
        return None

    payload: dict[str, object] = {
        "prompt_tokens": usage.prompt_tokens,
        "completion_tokens": usage.completion_tokens,
        "total_tokens": usage.total_tokens,
        "cost_usd": usage.cost_usd,
        "actual_cost_usd": usage.actual_cost_usd,
    }
    if usage.details:
        payload["details"] = dict(usage.details)
    return payload


def _public_prompt_tokens(context: PromptContext) -> int:
    """Approximate prompt tokens based only on user-visible inputs."""

    tokens = rough_token_estimate(context.prompt)
    if context.system:
        tokens += rough_token_estimate(context.system)
    for entry in context.context_messages:
        tokens += rough_token_estimate(entry["content"])
    return tokens


def _public_completion_tokens(text: str) -> int:
    """Estimate completion tokens from the surfaced text to avoid hidden reasoning costs."""
    return rough_token_estimate(text)


def _extract_reasoning_payload(context: PromptContext) -> dict[str, object] | None:
    payload: dict[str, object] = {}

    if context.shared_options is not None and isinstance(
        (shared_reasoning := context.shared_options.get("reasoning")), Mapping
    ):
        payload["shared_options"] = dict(shared_reasoning)

    if context.request_options is not None and isinstance(
        (request_reasoning := context.request_options.get("reasoning")), Mapping
    ):
        payload["request_options"] = dict(request_reasoning)

    if context.responses_request and context.responses_request.reasoning is not None:
        payload["responses_request"] = dict(context.responses_request.reasoning)

    if context.xcs_config:
        payload["xcs"] = dict(context.xcs_config)

    return payload or None


def _telemetry_context(plan: Eb1Plan, context: PromptContext, mode: str) -> dict[str, object]:
    reasoning = _extract_reasoning_payload(context)
    plan_payload = {
        "judge": plan.judge.model,
        "candidates": [candidate.model for candidate in plan.candidates],
    }

    payload: dict[str, object] = {
        "mode": mode,
        "plan": plan_payload,
        "prompt_chars": len(context.prompt),
        "has_system_prompt": bool(context.system),
        "has_responses_request": context.responses_request is not None,
        "context_messages": len(context.context_messages),
    }

    if context.request_options:
        payload["request_option_keys"] = sorted(context.request_options.keys())
    if context.shared_options:
        payload["shared_option_keys"] = sorted(context.shared_options.keys())
    if context.xcs_config:
        payload["xcs_config_keys"] = sorted(context.xcs_config.keys())
    if reasoning:
        payload["reasoning"] = reasoning

    return payload


def _telemetry_error_payload(exc: BaseException) -> dict[str, object]:
    return {
        "error_type": exc.__class__.__name__,
        "error_message": str(exc),
    }


def _latency_ms(start: float) -> float:
    return (time.perf_counter() - start) * 1000.0


def run_eb1(
    prompt: str,
    *,
    system: str | None = None,
    candidates: Sequence[CandidateDescriptor] | None = None,
    judge: JudgeDescriptor | None = None,
    shared_options: Mapping[str, JSON] | None = None,
    xcs_config: Mapping[str, JSON] | None = None,
) -> ChatResponse:
    """Run the EB1 ensemble.

    Args:
        prompt: User prompt.
        system: Optional system message.
        candidates: Optional explicit candidate overrides.
        judge: Optional explicit judge override.
        shared_options: Options passed to all model calls.
        xcs_config: Optional XCS configuration.

    Returns:
        ChatResponse with ensemble metadata.
    """

    return _run_with_plan(
        prompt,
        system=system,
        candidates=candidates,
        judge=judge,
        shared_options=shared_options,
        xcs_config=xcs_config,
        fallback=DEFAULT_PLAN,
    )


def run_eb1_pro(
    prompt: str,
    *,
    system: str | None = None,
    candidates: Sequence[CandidateDescriptor] | None = None,
    judge: JudgeDescriptor | None = None,
    shared_options: Mapping[str, JSON] | None = None,
    xcs_config: Mapping[str, JSON] | None = None,
) -> ChatResponse:
    """Run the EB1 Pro ensemble.

    Args:
        prompt: User prompt.
        system: Optional system message.
        candidates: Optional explicit candidate overrides.
        judge: Optional explicit judge override.
        shared_options: Options passed to all model calls.
        xcs_config: Optional XCS configuration.

    Returns:
        ChatResponse with ensemble metadata.
    """

    return _run_with_plan(
        prompt,
        system=system,
        candidates=candidates,
        judge=judge,
        shared_options=shared_options,
        xcs_config=xcs_config,
        fallback=DEFAULT_PRO_PLAN,
    )


def run_eb1_codex(
    prompt: str,
    *,
    system: str | None = None,
    candidates: Sequence[CandidateDescriptor] | None = None,
    judge: JudgeDescriptor | None = None,
    shared_options: Mapping[str, JSON] | None = None,
    xcs_config: Mapping[str, JSON] | None = None,
) -> ChatResponse:
    """Run the EB1 Codex ensemble with GPT-5 Codex synthesis."""

    return _run_with_plan(
        prompt,
        system=system,
        candidates=candidates,
        judge=judge,
        shared_options=shared_options,
        xcs_config=xcs_config,
        fallback=DEFAULT_CODEX_PLAN,
    )


def run_plan(
    plan: Eb1Plan,
    prompt: str,
    *,
    system: str | None = None,
    shared_options: Mapping[str, JSON] | None = None,
    request_options: Mapping[str, JSON] | None = None,
    xcs_config: Mapping[str, JSON] | None = None,
    responses_request: ResponsesRequest | None = None,
    context_messages: Sequence[Mapping[str, str]] | None = None,
    invoker: "ModelInvoker | None" = None,
) -> ChatResponse:
    """Execute a pre-built EB1 plan.

    Args:
        plan: Execution plan specifying candidates and judge.
        prompt: User prompt text.
        system: Optional system prompt.
        shared_options: Options forwarded to all plan components.
        request_options: Options specific to this request.
        xcs_config: Execution configuration overrides.
        responses_request: Responses API request context.
        context_messages: Prior conversation turns.
        invoker: Optional ModelInvoker for upstream calls. When provided,
            all model calls route through the invoker instead of the
            ember.api.models facade. Pass None (default) to use the
            existing facade.
    """

    prompt_context = PromptContext(
        prompt=prompt,
        system=system,
        shared_options=shared_options,
        request_options=request_options,
        xcs_config=xcs_config,
        responses_request=responses_request,
        context_messages=tuple(dict(entry) for entry in (context_messages or ())),
    )
    return _execute_plan(plan, prompt_context, invoker=invoker)


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


@dataclass(slots=True)
class _CandidateFailure:
    """Records a candidate that failed during parallel/sequential execution."""
    candidate_index: int
    requested_model: str
    error_type: str
    error_message: str


@dataclass(slots=True)
class _InvocationResult:
    model_id: str
    text: str
    usage: UsageStats
    metadata: Mapping[str, object]
    requested_model: str | None = None
    output: Sequence[Mapping[str, object]] | None = None
    thinking_trace: str | None = None
    response: "Response | ChatResponse | None" = None


def _run_with_plan(
    prompt: str,
    *,
    system: str | None,
    candidates: Sequence[CandidateDescriptor] | None,
    judge: JudgeDescriptor | None,
    shared_options: Mapping[str, JSON] | None,
    xcs_config: Mapping[str, JSON] | None,
    fallback: Eb1Plan,
) -> ChatResponse:
    """Execute EB1 with explicit overrides or the provided fallback plan.

    Args:
        prompt: User prompt to evaluate.
        system: Optional system message applied to each invocation.
        candidates: Candidate overrides to merge with defaults.
        judge: Judge override to merge with defaults.
        shared_options: Options forwarded to all plan components.
        xcs_config: Execution overrides for the XCS runtime.
        fallback: Plan used when overrides are not supplied.

    Returns:
        ChatResponse containing the judge synthesis.
    """
    plan = _materialize_plan(candidates, judge, fallback=fallback)
    context = PromptContext(
        prompt=prompt,
        system=system,
        shared_options=shared_options,
        xcs_config=xcs_config,
    )
    return _execute_plan(plan, context)


def _materialize_plan(
    candidates: Sequence[CandidateDescriptor] | None,
    judge: JudgeDescriptor | None,
    *,
    fallback: Eb1Plan,
) -> Eb1Plan:
    candidate_plans = _coerce_candidates(candidates, fallback=fallback.candidates)
    judge_plan = _coerce_judge(judge, fallback=fallback.judge)
    return Eb1Plan(candidates=candidate_plans, judge=judge_plan)


def _coerce_candidates(
    descriptor: Sequence[CandidateDescriptor] | None,
    *,
    fallback: tuple[CandidatePlan, ...],
) -> tuple[CandidatePlan, ...]:
    if descriptor is None:
        return fallback

    plans: list[CandidatePlan] = []
    for entry in descriptor:
        if isinstance(entry, CandidatePlan):
            plans.append(entry)
            continue
        if isinstance(entry, str):
            plans.append(CandidatePlan(model=entry))
            continue
        if isinstance(entry, Mapping):
            model_id = str(entry.get("model") or entry.get("model_id") or "").strip()
            if not model_id:
                raise ValueError("Candidate descriptor is missing a model identifier.")
            params = dict(entry.get("params") or {})
            plans.append(CandidatePlan(model=model_id, params=params))
            continue
        raise TypeError(
            "Candidate definitions must be strings, mappings, or CandidatePlan instances."
        )

    if not plans:
        raise ValueError("At least one candidate model must be specified.")

    return tuple(plans)


def _coerce_judge(
    descriptor: JudgeDescriptor | None,
    *,
    fallback: JudgePlan,
) -> JudgePlan:
    if descriptor is None:
        return fallback

    if isinstance(descriptor, JudgePlan):
        return descriptor

    if isinstance(descriptor, str):
        return JudgePlan(model=descriptor)

    if isinstance(descriptor, Mapping):
        model_id = str(
            descriptor.get("model") or descriptor.get("model_id") or fallback.model
        ).strip()
        if not model_id:
            raise ValueError("Judge descriptor is missing a model identifier.")
        params = dict(descriptor.get("params") or {})
        template = descriptor.get("prompt_template") or descriptor.get("template")
        system_template = descriptor.get("system_template")
        if template:
            # Custom template: default to single-prompt mode (no separate
            # system prompt) unless the caller explicitly provides one.
            return JudgePlan(
                model=model_id,
                params=params,
                prompt_template=template,
                system_template=system_template,
            )
        return JudgePlan(model=model_id, params=params)

    raise TypeError("Judge definition must be a string, mapping, or JudgePlan instance.")


def _context_without_tools(context: PromptContext) -> PromptContext:
    """Return a copy of *context* with tools/tool_choice stripped.

    Candidates run without tools so they focus on reasoning.  The judge
    receives the original context (with tools) and uses candidate reasoning
    to inform its tool-call decisions.
    """
    request = context.responses_request
    if request is None or not request.tools:
        return context
    payload = dict(request.original_payload)
    payload.pop("tools", None)
    payload.pop("tool_choice", None)
    stripped = _dc_replace(
        request,
        tools=None,
        tool_choice=None,
        original_payload=payload,
    )
    return _dc_replace(context, responses_request=stripped)


def _execute_plan(
    plan: Eb1Plan,
    context: PromptContext,
    invoker: "ModelInvoker | None" = None,
) -> ChatResponse:
    request = context.responses_request
    has_tools = request is not None and request.tools
    # When tools are present, candidates run WITHOUT tools to produce
    # reasoning context.  The judge still receives the full context with
    # tools so it can issue tool calls informed by multi-perspective analysis.
    candidate_context = _context_without_tools(context) if has_tools else context

    telemetry = get_telemetry()
    run_id = telemetry.begin_run(context=_telemetry_context(plan, context, "multi"))
    run_start = time.perf_counter()

    # Check if parallel execution is enabled (default: True for multiple candidates)
    # Parallel execution exploits provider independence and uses adaptive concurrency
    # control to automatically adjust parallelism based on rate limit feedback.
    use_parallel = os.environ.get("EB1_PARALLEL_CANDIDATES", "1") in ("1", "true", "True")
    num_candidates = len(plan.candidates)

    candidate_failures: list[_CandidateFailure] = []

    if use_parallel and num_candidates > 1:
        # Parallel execution with adaptive concurrency limiting
        _EB1_LOG.debug(
            "Executing %d candidates in parallel with adaptive concurrency",
            num_candidates,
        )
        candidate_results, candidate_failures = _execute_candidates_parallel(
            plan.candidates,
            candidate_context,
            telemetry,
            run_id,
            run_start,
            invoker=invoker,
        )
    else:
        # Sequential execution (original behavior) with graceful degradation
        candidate_results: list[_InvocationResult] = []
        first_seq_error: Exception | None = None
        for index, candidate in enumerate(plan.candidates, start=1):
            candidate_start = time.perf_counter()
            try:
                result = _invoke_candidate(
                    candidate,
                    candidate_context,
                    allow_responses=True,
                    invoker=invoker,
                )
            except Exception as exc:
                if first_seq_error is None:
                    first_seq_error = exc
                candidate_failures.append(_CandidateFailure(
                    candidate_index=index,
                    requested_model=candidate.model,
                    error_type=type(exc).__name__,
                    error_message=str(exc)[:200],
                ))
                telemetry.record(
                    run_id,
                    "candidate.failed",
                    {
                        "index": index,
                        "requested_model": candidate.model,
                        "latency_ms": _latency_ms(candidate_start),
                        "error": _telemetry_error_payload(exc),
                    },
                )
                continue

            candidate_results.append(result)
            telemetry.record(
                run_id,
                "candidate.completed",
                {
                    "index": index,
                    "requested_model": candidate.model,
                    "invoked_model": result.model_id,
                    "latency_ms": _latency_ms(candidate_start),
                    "usage": _usage_payload(result.usage),
                },
            )

        if not candidate_results:
            telemetry.end_run(
                run_id,
                outcome={
                    "status": "error",
                    "phase": "candidate",
                    "latency_ms": _latency_ms(run_start),
                    "error": _telemetry_error_payload(first_seq_error),
                },
            )
            assert first_seq_error is not None
            raise first_seq_error

        if first_seq_error is not None:
            failed_count = len(plan.candidates) - len(candidate_results)
            _EB1_LOG.warning(
                "EB1: %d/%d candidates failed; synthesizing from %d successful",
                failed_count,
                len(plan.candidates),
                len(candidate_results),
            )

    judge_system, judge_prompt = _render_judge_prompt(
        plan.judge.prompt_template,
        plan.judge.system_template,
        context.prompt,
        context.system,
        candidate_results,
    )
    telemetry.record(
        run_id,
        "judge.prompt.synthesized",
        {
            "prompt_length": len(judge_prompt),
            "system_length": len(judge_system) if judge_system else 0,
            "candidate_count": len(candidate_results),
        },
    )

    judge_start = time.perf_counter()
    try:
        judge_result = _invoke_judge(
            plan.judge,
            judge_prompt,
            context,
            allow_responses=True,
            system_override=judge_system,
            invoker=invoker,
        )
    except Exception as exc:
        telemetry.end_run(
            run_id,
            outcome={
                "status": "error",
                "phase": "judge",
                "latency_ms": _latency_ms(run_start),
                "error": _telemetry_error_payload(exc),
            },
        )
        raise
    telemetry.record(
        run_id,
        "judge.completed",
        {
            "requested_model": plan.judge.model,
            "invoked_model": judge_result.model_id,
            "latency_ms": _latency_ms(judge_start),
            "usage": _usage_payload(judge_result.usage),
        },
    )

    # XML extraction first (new default prompt), JSON fallback for custom
    # templates that still use the legacy JSON schema.
    verdict, rationale = _extract_response_xml(judge_result.text)
    used_xml = verdict is not None
    if used_xml:
        # Metadata placeholder: XML mode uses the synthesized <response>
        # as verdict, not a specific winner. Index 0 is arbitrary for
        # telemetry; the actual response_text comes from verdict below.
        winner_index = 0
    else:
        judge_payload = _try_parse_json(judge_result.text)
        try:
            winner_index, verdict, rationale = _extract_verdict(
                judge_payload, len(candidate_results),
            )
        except ValueError as exc:
            _EB1_LOG.warning(
                "Judge verdict extraction failed; falling back to candidate 0. %s",
                exc,
            )
            winner_index = 0
            verdict = None
            rationale = None
    winner_index = max(0, min(winner_index, len(candidate_results) - 1))

    usage_samples = [result.usage for result in candidate_results]
    usage_samples.append(judge_result.usage)
    full_usage = _aggregate_usage(usage_samples)

    winner_result = candidate_results[winner_index]
    response_text = verdict or winner_result.text or judge_result.text

    final_output: list[dict[str, object]] | None = None
    if winner_result.output:
        final_output = [dict(item) for item in winner_result.output]

    if judge_result.output:
        judge_items = [dict(item) for item in judge_result.output]
        if final_output is None:
            final_output = judge_items
        else:
            final_output.extend(judge_items)
    elif verdict:
        message = {
            "type": "message",
            "role": "assistant",
            "status": "completed",
            "content": [
                {
                    "type": "output_text",
                    "text": verdict,
                }
            ],
        }
        if final_output is None:
            final_output = [message]
        else:
            final_output.append(message)

    synthesis_approach = (
        "multi-candidate-xml-synthesis" if used_xml else "multi-candidate-synthesis"
    )
    raw_output: dict[str, object] = {
        "synthesis_model": plan.judge.model,
        "primary_source_model": candidate_results[winner_index].requested_model,
        "judge_system_prompt": judge_system,
        "judge_prompt": judge_prompt,
        "judge_response": judge_result.text,
        "synthesis_approach": synthesis_approach,
        "judge_metadata": dict(judge_result.metadata),
        "candidates": [
            {
                "requested_model": result.requested_model,
                "invoked_model": result.model_id,
                "response_text": result.text,
                "metadata": dict(result.metadata),
                "output": ([dict(item) for item in result.output] if result.output else None),
                "thinking_trace": result.thinking_trace,
            }
            for result in candidate_results
        ],
    }

    raw_output["telemetry_run_id"] = run_id
    raw_output["candidates_attempted"] = len(plan.candidates)
    raw_output["candidates_succeeded"] = len(candidate_results)
    if candidate_failures:
        raw_output["candidate_failures"] = [
            {
                "candidate_index": f.candidate_index,
                "requested_model": f.requested_model,
                "error_type": f.error_type,
                "error_message": f.error_message,
            }
            for f in candidate_failures
        ]

    if not used_xml and isinstance(judge_payload, Mapping):
        raw_output["judge_payload"] = judge_payload
    if verdict:
        raw_output["synthesized_response"] = verdict
    if rationale:
        raw_output["synthesis_reasoning" if used_xml else "synthesis_rationale"] = rationale
    if context.xcs_config:
        raw_output["xcs_config"] = dict(context.xcs_config)

    public_usage = UsageStats(
        prompt_tokens=_public_prompt_tokens(context),
        completion_tokens=_public_completion_tokens(response_text),
    )

    raw_output["internal_usage"] = _usage_payload(full_usage)

    final_response = ChatResponse(
        data=response_text,
        output=final_output,
        usage=public_usage,
        model_id="eb1",
        raw_output=raw_output,
        thinking_trace=judge_result.thinking_trace,
    )

    winner_raw = getattr(winner_result.response, "_raw", None)
    if isinstance(winner_raw, ChatResponse) and isinstance(winner_raw.raw_output, Mapping):
        payload = winner_raw.raw_output.get("responses_payload")
        if isinstance(payload, Mapping):
            raw_output.setdefault("responses_payload_source", "winner")
            raw_output.setdefault("responses_payload", dict(payload))

    judge_raw = getattr(judge_result.response, "_raw", None)
    if isinstance(judge_raw, ChatResponse) and isinstance(judge_raw.raw_output, Mapping):
        judge_payload = judge_raw.raw_output.get("responses_payload")
        if isinstance(judge_payload, Mapping):
            raw_output["responses_payload_source"] = "judge"
            raw_output["responses_payload"] = dict(judge_payload)

    winner_model = candidate_results[winner_index].requested_model
    telemetry.record(
        run_id,
        "winner.selected",
        {
            "winner_index": winner_index,
            "winner_model": winner_model,
            "verdict_present": bool(verdict),
            "rationale_present": bool(rationale),
        },
    )

    usage_payload = _usage_payload(full_usage)
    telemetry.end_run(
        run_id,
        outcome={
            "status": "success",
            "latency_ms": _latency_ms(run_start),
            "winner_model": winner_model,
            "judge_model": plan.judge.model,
            "candidate_count": len(candidate_results),
            "usage": usage_payload,
        },
    )

    _log_eb1_outcome(event="multi", plan=plan, raw_output=raw_output, usage=full_usage)
    return final_response


def _execute_judge_direct(plan: Eb1Plan, context: PromptContext) -> ChatResponse:
    merged_options: dict[str, JSON] = {}
    if context.request_options:
        merged_options.update(context.request_options)
    judge_payload = _merge_options(context.shared_options, plan.judge.params)
    merged_options.update(judge_payload)
    telemetry = get_telemetry()
    run_id = telemetry.begin_run(context=_telemetry_context(plan, context, "judge_only"))
    judge_start = time.perf_counter()
    try:
        judge_result = _invoke_model(
            plan.judge.model,
            context,
            merged_options,
            prompt_override=None,
            allow_responses=True,
        )
    except Exception as exc:  # pragma: no cover
        telemetry.end_run(
            run_id,
            outcome={
                "status": "error",
                "phase": "judge",
                "latency_ms": _latency_ms(judge_start),
                "error": _telemetry_error_payload(exc),
            },
        )
        raise

    telemetry.record(
        run_id,
        "judge.completed",
        {
            "requested_model": plan.judge.model,
            "invoked_model": judge_result.model_id,
            "latency_ms": _latency_ms(judge_start),
            "usage": _usage_payload(judge_result.usage),
        },
    )

    judge_raw = getattr(judge_result.response, "_raw", None)
    if isinstance(judge_raw, ChatResponse):
        final_response = judge_raw
    else:
        output_payload = list(judge_result.output) if judge_result.output is not None else None
        final_response = ChatResponse(
            data=judge_result.text,
            output=output_payload,
            usage=judge_result.usage,
            model_id=judge_result.model_id,
        )

    final_response.data = judge_result.text
    if judge_result.output is not None:
        final_response.output = list(judge_result.output)
    if judge_result.usage is not None:
        final_response.usage = judge_result.usage
    if judge_result.thinking_trace is not None:
        final_response.thinking_trace = judge_result.thinking_trace
    primary_model = final_response.model_id or plan.judge.model

    raw_payload = final_response.raw_output
    if isinstance(raw_payload, Mapping):
        raw_output: dict[str, object] = dict(raw_payload)
    else:
        raw_output = {}

    raw_output.update(
        {
            "synthesis_model": plan.judge.model,
            "primary_source_model": primary_model,
            "synthesis_approach": "single-judge",
            "judge_response": judge_result.text,
            "judge_prompt": context.prompt,
            "candidates": [],
        }
    )
    raw_output["telemetry_run_id"] = run_id
    if context.xcs_config:
        raw_output["xcs_config"] = dict(context.xcs_config)

    judge_payload = _try_parse_json(judge_result.text)
    if isinstance(judge_payload, Mapping):
        raw_output["judge_payload"] = judge_payload
        # Prefer final_response, fall back to synthesis for backward compat
        response_text = judge_payload.get("final_response") or judge_payload.get("synthesis")
        if isinstance(response_text, str) and response_text.strip():
            raw_output.setdefault("synthesized_response", response_text)

    if isinstance(judge_raw, ChatResponse) and isinstance(judge_raw.raw_output, Mapping):
        responses_payload = judge_raw.raw_output.get("responses_payload")
        if isinstance(responses_payload, Mapping):
            raw_output["responses_payload_source"] = "judge"
            raw_output["responses_payload"] = dict(responses_payload)

    raw_output["judge_metadata"] = dict(judge_result.metadata)

    final_response.model_id = "eb1"
    final_response.raw_output = raw_output
    telemetry.end_run(
        run_id,
        outcome={
            "status": "success",
            "latency_ms": _latency_ms(judge_start),
            "judge_model": plan.judge.model,
            "candidate_count": 0,
            "usage": _usage_payload(final_response.usage),
        },
    )
    _log_eb1_outcome(
        event="judge_only",
        plan=plan,
        raw_output=raw_output,
        usage=final_response.usage,
    )
    return final_response


def _invoke_candidate(
    candidate: CandidatePlan,
    context: PromptContext,
    *,
    allow_responses: bool,
    invoker: "ModelInvoker | None" = None,
) -> _InvocationResult:
    """Invoke a single candidate and capture its result."""
    merged_options: dict[str, JSON] = {}
    if context.request_options:
        merged_options.update(context.request_options)
    candidate_payload = _merge_options(context.shared_options, candidate.params)
    merged_options.update(candidate_payload)

    if context.system is not None:
        merged_options.setdefault("system", context.system)
    return _invoke_model(
        candidate.model,
        context,
        merged_options,
        prompt_override=context.prompt,
        allow_responses=allow_responses,
        invoker=invoker,
    )


def _invoke_candidate_with_limiter(
    index: int,
    candidate: CandidatePlan,
    context: PromptContext,
    invoker: "ModelInvoker | None" = None,
) -> tuple[int, _InvocationResult]:
    """Invoke a candidate with adaptive concurrency limiting."""
    limiter = get_limiter()
    key = ConcurrencyKey.from_model_id(candidate.model)

    acquired = limiter.acquire(key, timeout_s=60.0)
    if not acquired:
        raise TimeoutError(f"Concurrency limit timeout for {key}")

    try:
        result = _invoke_candidate(
            candidate,
            context,
            allow_responses=True,
            invoker=invoker,
        )
        limiter.release(key, Outcome.SUCCESS)
        return (index, result)
    except Exception as exc:
        outcome = outcome_from_error(exc)
        limiter.release(key, outcome)
        raise


def _execute_candidates_parallel(
    candidates: Sequence[CandidatePlan],
    context: PromptContext,
    telemetry: "Eb1Telemetry",
    run_id: str,
    run_start: float,
    invoker: "ModelInvoker | None" = None,
) -> tuple[list[_InvocationResult], list[_CandidateFailure]]:
    """Execute candidates in parallel, returning successes and failures.

    Raises the first exception when ALL candidates fail.
    """

    results: list[tuple[int, _InvocationResult]] = []
    failures: list[_CandidateFailure] = []
    first_error: Exception | None = None
    failed_index: int | None = None

    with ThreadPoolExecutor(max_workers=len(candidates)) as executor:
        future_to_candidate = {
            executor.submit(
                _invoke_candidate_with_limiter,
                i,
                c,
                context,
                invoker,
            ): (i, c)
            for i, c in enumerate(candidates, start=1)
        }

        start_times = {i: time.perf_counter() for i in range(1, len(candidates) + 1)}

        for future in as_completed(future_to_candidate):
            index, candidate = future_to_candidate[future]
            try:
                result_index, result = future.result()
                results.append((result_index, result))
                telemetry.record(
                    run_id,
                    "candidate.completed",
                    {
                        "index": index,
                        "requested_model": candidate.model,
                        "invoked_model": result.model_id,
                        "latency_ms": _latency_ms(start_times[index]),
                        "usage": _usage_payload(result.usage),
                        "parallel": True,
                    },
                )
            except Exception as exc:
                if first_error is None:
                    first_error = exc
                    failed_index = index
                failures.append(_CandidateFailure(
                    candidate_index=index,
                    requested_model=candidate.model,
                    error_type=type(exc).__name__,
                    error_message=str(exc)[:200],
                ))
                telemetry.record(
                    run_id,
                    "candidate.failed",
                    {
                        "index": index,
                        "requested_model": candidate.model,
                        "latency_ms": _latency_ms(start_times[index]),
                        "error": _telemetry_error_payload(exc),
                        "parallel": True,
                    },
                )

    # Graceful degradation: fail only if ALL candidates failed.
    if not results:
        telemetry.end_run(
            run_id,
            outcome={
                "status": "error",
                "phase": "candidate",
                "index": failed_index,
                "latency_ms": _latency_ms(run_start),
                "error": _telemetry_error_payload(first_error),
            },
        )
        assert first_error is not None  # guaranteed when results is empty
        raise first_error

    if first_error is not None:
        failed_count = len(candidates) - len(results)
        _EB1_LOG.warning(
            "EB1: %d/%d candidates failed; synthesizing from %d successful",
            failed_count,
            len(candidates),
            len(results),
        )

    results.sort(key=lambda x: x[0])
    return [r for _, r in results], failures


def _invoke_judge(
    judge: JudgePlan,
    prompt: str,
    context: PromptContext,
    *,
    allow_responses: bool,
    system_override: str | None = None,
    invoker: "ModelInvoker | None" = None,
) -> _InvocationResult:
    """Invoke the judge model to synthesize candidate responses."""
    merged_options: dict[str, JSON] = {}
    if context.request_options:
        merged_options.update(context.request_options)
    judge_payload = _merge_options(context.shared_options, judge.params)
    merged_options.update(judge_payload)
    return _invoke_model(
        judge.model,
        context,
        merged_options,
        prompt_override=prompt,
        allow_responses=allow_responses,
        system_override=system_override,
        invoker=invoker,
    )


def _invoke_via_invoker(
    invoker: "ModelInvoker",
    model_id: str,
    context: PromptContext,
    payload: Mapping[str, JSON],
    *,
    prompt_override: str | None,
    system_override: str | None = None,
) -> _InvocationResult:
    """Route a model call through the ModelInvoker protocol."""
    from keiro.protocol import ModelCall

    prompt_text = prompt_override if prompt_override is not None else context.prompt
    system = system_override if system_override is not None else context.system

    options = dict(payload)
    if system is not None:
        options.pop("system", None)

    call = ModelCall(
        model_id=model_id,
        prompt=prompt_text,
        system=system,
        context_messages=context.context_messages or (),
        tools=tuple(options["tools"]) if "tools" in options else None,
        response_format=options.get("response_format"),
        reasoning=options.get("reasoning"),
        max_output_tokens=options.get("max_output_tokens"),
    )

    result = invoker.invoke(call)

    return _InvocationResult(
        requested_model=model_id,
        model_id=result.model_id,
        text=result.text,
        usage=result.usage,
        metadata={},
        output=None,
        thinking_trace=None,
        response=None,
    )


def _invoke_model(
    model_id: str,
    context: PromptContext,
    payload: Mapping[str, JSON],
    *,
    prompt_override: str | None,
    allow_responses: bool,
    system_override: str | None = None,
    invoker: "ModelInvoker | None" = None,
) -> _InvocationResult:
    """Invoke a model via the invoker protocol or the models facade."""
    if invoker is not None:
        return _invoke_via_invoker(
            invoker,
            model_id,
            context,
            payload,
            prompt_override=prompt_override,
            system_override=system_override,
        )

    model_api = _get_models_facade()
    options = dict(payload)

    if system_override is not None:
        options["system"] = system_override

    prompt_text = prompt_override if prompt_override is not None else context.prompt

    use_responses = allow_responses and context.responses_request is not None
    if use_responses:
        # When prompt_override provides a different string (e.g. judge's
        # rendered candidates), force it into the payload — the original
        # input from the responses_request must not shadow the override.
        force_input = (
            prompt_override is not None
            and prompt_override is not context.prompt
        )
        responses_payload = _build_responses_payload_for_model(
            model_id, context, prompt_text, force_input=force_input,
        )
        options.pop("system", None)
        options.pop("context", None)
        options.pop("prompt", None)
        # Override instructions with the judge system prompt when provided.
        if system_override is not None:
            responses_payload["instructions"] = system_override
        response = model_api.responses(model_id, payload=responses_payload, **options)
    else:
        response = model_api.response(model_id, prompt_text, **options)

    usage = _usage_from_response(response)

    metadata_source = response.metadata or {}
    metadata = dict(metadata_source) if isinstance(metadata_source, Mapping) else {}

    resolved_model = response.model_id or model_id

    output = list(response.output) if response.output else None
    text = response.text.strip()

    # Extract thinking trace from the underlying ChatResponse when present.
    raw_chat = getattr(response, "_raw", None)
    thinking_trace = getattr(raw_chat, "thinking_trace", None) if raw_chat else None
    if thinking_trace is None and isinstance(response, ChatResponse):
        thinking_trace = response.thinking_trace

    return _InvocationResult(
        requested_model=model_id,
        model_id=resolved_model,
        text=text,
        usage=usage,
        metadata=metadata,
        output=output,
        thinking_trace=thinking_trace,
        response=response,
    )


def _build_responses_payload_for_model(
    model_id: str,
    context: PromptContext,
    prompt_text: str,
    *,
    force_input: bool = False,
) -> dict[str, object]:
    request = context.responses_request
    if request is None:
        raise ValueError("Responses payload requested without an attached ResponsesRequest")

    payload: dict[str, object] = copy.deepcopy(dict(request.original_payload))
    payload["model"] = model_id
    payload.pop("stream", None)

    # Normalize tools using the canonical IR module
    tools = payload.get("tools")
    if isinstance(tools, Sequence):
        normalized = normalize_tools(tools, model=model_id)
        payload["tools"] = tools_to_dict_list(normalized)

    # Preserve original messages exactly as provided by the caller. Only
    # inject a simplified message trail when the payload did not include one.
    if context.context_messages and "messages" not in payload:
        payload["messages"] = [dict(entry) for entry in context.context_messages]

    if prompt_text and (force_input or _input_is_empty(payload.get("input"))):
        payload["input"] = [
            {
                "role": "user",
                "content": [
                    {
                        "type": "input_text",
                        "text": prompt_text,
                    }
                ],
            }
        ]

    if context.system and not payload.get("instructions"):
        payload["instructions"] = context.system

    return payload


def _input_is_empty(value: object) -> bool:
    if value is None:
        return True
    if isinstance(value, str):
        return not value.strip()
    if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
        return len(value) == 0
    return False


def _merge_options(
    shared: OptionsMapping | None,
    specific: OptionsMapping,
) -> dict[str, JSON]:
    """Merge shared and specific options, with specific taking precedence."""

    merged = _merge_mappings(shared, specific)
    return merged or {}


def _extract_tool_calls_from_output(
    output: Sequence[Mapping[str, object]] | None,
) -> list[dict[str, object]]:
    """Extract tool_use items from an output message sequence.

    Args:
        output: Output messages from a candidate invocation.

    Returns:
        List of tool call dicts with name and arguments.
    """
    if not output:
        return []

    tool_calls: list[dict[str, object]] = []
    for item in output:
        # Check for tool_use content blocks within messages
        content = item.get("content")
        if isinstance(content, (list, tuple)):
            for block in content:
                if isinstance(block, Mapping) and block.get("type") == "tool_use":
                    tool_calls.append(
                        {
                            "id": block.get("id", ""),
                            "name": block.get("name", "unknown"),
                            "arguments": block.get("arguments", block.get("input", {})),
                        }
                    )
        # Also check top-level tool_use type
        if item.get("type") == "tool_use":
            tool_calls.append(
                {
                    "id": item.get("id", ""),
                    "name": item.get("name", "unknown"),
                    "arguments": item.get("arguments", item.get("input", {})),
                }
            )
    return tool_calls


def _format_tool_calls_for_judge(tool_calls: list[dict[str, object]]) -> str:
    """Format tool calls as readable text for the judge.

    Args:
        tool_calls: List of tool call dicts.

    Returns:
        Formatted string describing the tool calls.
    """
    if not tool_calls:
        return ""

    lines = ["[Tool Calls Requested:]"]
    for call in tool_calls:
        name = call.get("name", "unknown")
        args = call.get("arguments", {})
        try:
            args_str = json.dumps(args, indent=2)
        except (TypeError, ValueError):
            args_str = str(args)
        lines.append(f"  - {name}({args_str})")
    return "\n".join(lines)


def _render_candidate_response(result: _InvocationResult, index: int) -> str:
    """Render a candidate response including tool calls if present.

    Uses anonymous XML tags so the judge evaluates content on merit
    rather than model reputation.

    Args:
        result: Candidate invocation result.
        index: 1-based candidate index.

    Returns:
        Formatted candidate response block.
    """
    parts: list[str] = []

    # Include text response
    if result.text and result.text.strip():
        parts.append(result.text.strip())

    # Include tool calls as structured information
    tool_calls = _extract_tool_calls_from_output(result.output)
    if tool_calls:
        tool_section = _format_tool_calls_for_judge(tool_calls)
        parts.append(tool_section)

    if not parts:
        return f'<candidate index="{index}">\n(no response provided)\n</candidate>\n'

    body = "\n\n".join(parts)
    return f'<candidate index="{index}">\n{body}\n</candidate>\n'


def _render_judge_prompt(
    user_template: str,
    system_template: str | None,
    prompt: str,
    system: str | None,
    candidates: Sequence[_InvocationResult],
) -> tuple[str | None, str]:
    """Render the judge prompt by interpolating candidate responses.

    Returns a (system_prompt, user_prompt) tuple.  When *system_template*
    is not None the judge receives a proper system-level message containing
    the synthesizer role, identity directive, and evaluation criteria.
    When None (backward-compatible custom-template mode) everything is
    packed into the user prompt.

    Args:
        user_template: Judge user prompt template with {system}, {prompt},
            and {candidates} placeholders.
        system_template: Optional system prompt for the judge.  None means
            single-prompt mode (backward compat for custom templates).
        prompt: Original user prompt.
        system: Optional system instructions from the caller.
        candidates: Candidate invocation results.

    Returns:
        Tuple of (system_prompt, user_prompt).  system_prompt may be None.
    """
    shuffled = list(candidates)
    random.shuffle(shuffled)
    _EB1_LOG.debug(
        "Judge candidate order: %s",
        [r.model_id for r in shuffled],
    )
    candidate_blocks: list[str] = [
        _render_candidate_response(result, index) for index, result in enumerate(shuffled, start=1)
    ]
    candidates_section = "\n".join(candidate_blocks)

    def _escape_braces(text: str) -> str:
        return text.replace("{", "{{").replace("}", "}}")

    user_prompt = user_template.format(
        prompt=_escape_braces(prompt),
        system=_escape_braces(system or "(none)"),
        candidates=candidates_section.strip(),
    )

    return system_template, user_prompt


_RESPONSE_TAG_RE = re.compile(
    r"<response>(.*?)</response>",
    re.DOTALL,
)


def _extract_response_xml(
    text: str,
) -> tuple[str | None, str | None]:
    """Extract response and reasoning from XML-tagged judge output.

    Returns:
        Tuple of (response_text, reasoning_text). Either may be None
        if tags are missing.
    """
    match = _RESPONSE_TAG_RE.search(text)
    if not match:
        return None, None
    response = match.group(1).strip()
    reasoning = text[: match.start()].strip() or None
    return response or None, reasoning


def _try_parse_json(payload: str) -> object | None:
    """Parse JSON payload from judge response, tolerating code fences or trailing prose.

    Args:
        payload: Raw judge response payload.

    Returns:
        Parsed JSON object when available, otherwise ``None``.  Logs at
        WARNING when all extraction strategies fail so operators can
        diagnose judge output quality.
    """
    content = payload.strip()
    if not content:
        _EB1_LOG.warning(
            "Judge produced empty response (no JSON to extract). "
            "Raw payload length: %d",
            len(payload),
        )
        return None

    if content.startswith("```"):
        stripped = content.strip("` \n")
        if stripped.lower().startswith("json"):
            stripped = stripped[4:].lstrip()
        content = stripped

    try:
        return json.loads(content)
    except json.JSONDecodeError:
        start = content.find("{")
        end = content.rfind("}")
        if 0 <= start < end:
            candidate = content[start : end + 1]
            try:
                return json.loads(candidate)
            except json.JSONDecodeError:
                _EB1_LOG.warning(
                    "Judge JSON extraction failed (brace-delimited substring "
                    "also invalid). Raw content: %.500s",
                    content,
                )
                return None
        _EB1_LOG.warning(
            "Judge response contains no JSON object (no matching braces). "
            "Raw content: %.500s",
            content,
        )
        return None


def _extract_verdict(
    payload: object | None,
    candidate_count: int,
) -> tuple[int, str | None, str | None]:
    """Extract winner index, verdict text, and rationale from judge JSON.

    Raises:
        ValueError: If the payload is not a valid judge verdict (not a dict,
            missing expected keys, or contains unparseable winner index).
            Callers must handle this — silent fallback to candidate 0 would
            corrupt benchmark results.
    """
    if not isinstance(payload, Mapping):
        raise ValueError(
            f"Judge produced non-dict payload (type={type(payload).__name__}). "
            f"Raw value: {payload!r:.500}"
        )

    # Handle final_response format (preferred) or legacy synthesis format
    if "final_response" in payload or "synthesis" in payload:
        primary_source = payload.get("primary_source", 1)
        try:
            winner_index = int(primary_source) - 1
        except (TypeError, ValueError):
            raise ValueError(
                f"Judge primary_source is not an integer: {primary_source!r}"
            )

        # Prefer final_response, fall back to synthesis for backward compat
        response_text = payload.get("final_response") or payload.get("synthesis")
        if isinstance(response_text, str):
            verdict = response_text.strip() or None
        else:
            verdict = None
    # Backward compatibility with old format
    elif "winner" in payload or "verdict" in payload:
        winner_value = payload.get("winner", 1)
        try:
            winner_index = int(winner_value) - 1
        except (TypeError, ValueError):
            raise ValueError(
                f"Judge winner field is not an integer: {winner_value!r}"
            )

        verdict = payload.get("verdict")
        if isinstance(verdict, str):
            verdict = verdict.strip() or None
        else:
            verdict = None
    else:
        raise ValueError(
            f"Judge payload has no recognized verdict keys. "
            f"Keys present: {list(payload.keys())}"
        )

    rationale = payload.get("rationale")
    if isinstance(rationale, str):
        rationale = rationale.strip() or None
    else:
        rationale = None

    if winner_index < 0 or winner_index >= candidate_count:
        _EB1_LOG.warning(
            "Judge winner_index %d out of range [0, %d); clamping to 0",
            winner_index,
            candidate_count,
        )
        winner_index = 0

    return winner_index, verdict, rationale


def _usage_from_response(response: "Response | ChatResponse") -> UsageStats:
    """Extract concrete usage stats from a model response wrapper."""

    def _to_float(value: object | None) -> float | None:
        if value is None:
            return None
        if isinstance(value, (int, float)):
            return float(value)
        if isinstance(value, str):
            try:
                return float(value)
            except ValueError as exc:
                raise TypeError("Usage cost values must be numeric.") from exc
        raise TypeError("Usage cost values must be numeric.")

    usage_obj = response.usage
    if isinstance(usage_obj, UsageStats):
        prompt_tokens = int(usage_obj.prompt_tokens)
        completion_tokens = int(usage_obj.completion_tokens)
        total_tokens = int(usage_obj.total_tokens)
        return UsageStats(
            prompt_tokens=prompt_tokens,
            completion_tokens=completion_tokens,
            total_tokens=total_tokens,
            cost_usd=_to_float(usage_obj.cost_usd),
            actual_cost_usd=_to_float(usage_obj.actual_cost_usd),
            details=dict(usage_obj.details) if usage_obj.details else None,
        )

    if not isinstance(usage_obj, Mapping):
        raise TypeError("EB1 response is missing usage data.")

    try:
        prompt_tokens = int(usage_obj["prompt_tokens"])
        completion_tokens = int(usage_obj["completion_tokens"])
    except KeyError as exc:
        raise TypeError("EB1 usage payload must include prompt and completion tokens.") from exc
    except (TypeError, ValueError) as exc:
        raise TypeError("EB1 usage token counts must be integers.") from exc

    total_raw = usage_obj.get("total_tokens", prompt_tokens + completion_tokens)
    try:
        total_tokens = int(total_raw)
    except (TypeError, ValueError) as exc:
        raise TypeError("EB1 usage total tokens must be an integer.") from exc

    details_raw = usage_obj.get("details")
    details = dict(details_raw) if isinstance(details_raw, Mapping) else None

    return UsageStats(
        prompt_tokens=prompt_tokens,
        completion_tokens=completion_tokens,
        total_tokens=total_tokens,
        cost_usd=_to_float(usage_obj.get("cost")),
        actual_cost_usd=_to_float(usage_obj.get("actual_cost_usd")),
        details=details,
    )


def _aggregate_usage(samples: Iterable[UsageStats]) -> UsageStats:
    prompt_tokens = 0
    completion_tokens = 0
    cost_values: list[float] = []
    actual_cost_values: list[float] = []
    merged_details: dict[str, object] | None = None

    for sample in samples:
        prompt_tokens += sample.prompt_tokens
        completion_tokens += sample.completion_tokens
        if sample.cost_usd is not None:
            cost_values.append(sample.cost_usd)
        if sample.actual_cost_usd is not None:
            actual_cost_values.append(sample.actual_cost_usd)
        if sample.details:
            merged_details = _merge_usage_details(merged_details, sample.details)

    total_tokens = prompt_tokens + completion_tokens
    cost_sum = sum(cost_values) if cost_values else None
    actual_cost_sum = sum(actual_cost_values) if actual_cost_values else None

    return UsageStats(
        prompt_tokens=prompt_tokens,
        completion_tokens=completion_tokens,
        total_tokens=total_tokens,
        cost_usd=cost_sum,
        actual_cost_usd=actual_cost_sum,
        details=merged_details,
    )


@lru_cache(maxsize=1)
def _get_models_facade():
    from importlib import import_module

    module = import_module("ember.api.models")
    return module.models


__all__ = [
    "run_eb1",
    "run_eb1_pro",
    "run_eb1_codex",
    "run_plan",
    "DEFAULT_CANDIDATES",
    "DEFAULT_JUDGE_MODEL",
    "DEFAULT_PLAN",
    "DEFAULT_PRO_PLAN",
    "DEFAULT_PRO_HIGH_PLAN",
    "DEFAULT_CODEX_PLAN",
    "DEFAULT_CODEX_HIGH_PLAN",
    "CandidatePlan",
    "JudgePlan",
    "Eb1Plan",
    "PromptContext",
    "Eb1Graph",
]
