from __future__ import annotations

import copy
import json
import logging
import uuid
from collections.abc import Generator, Iterable, Mapping, MutableMapping, Sequence
from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable, Literal

_LOG = logging.getLogger("ember.gateway.eb1_codex")

from ember._internal.concurrency import with_limit
from ember._internal.exceptions import ProviderAPIError
from ember.core.ir import ToolPolicy, apply_tool_policy, normalize_tools, tools_to_dict_list
from ember.models.providers import resolve_model_id
from ember.models.providers.anthropic import AnthropicProvider
from ember.models.providers.base import BaseProvider
from ember.models.providers.openai import OpenAIProvider
from ember.models.providers.utils import (
    ResponsesMessage,
    ResponsesRequest,
    sanitize_responses_payload,
)
from ember.models.schemas import ChatResponse, UsageStats
from ..telemetry import get_telemetry
from keiro.ensemble.eb1 import EB1_IDENTITY_DIRECTIVE, CandidatePlan, Eb1Plan, JudgePlan
from keiro.ensemble.responses import ensure_responses_output

if TYPE_CHECKING:
    from ..providers.planner import ExecutablePlan

_WORKER_PERSONAS: tuple[str, ...] = (
    "Prioritize correctness and edge cases.",
    "Prioritize simplicity and minimal moving parts.",
    "Prioritize performance and streaming ergonomics.",
    "Prioritize typed APIs and invariants.",
    "Prioritize tool-call clarity and schema fidelity.",
)

_WORKER_SYSTEM_PREFIX_NO_TOOLS = (
    "You are one of several independent coding advisors. "
    "Provide a best-effort draft answer for the user. "
    "Do NOT call tools; output plain text advice only. "
    "Be concise, correct, and complete. "
    f"{EB1_IDENTITY_DIRECTIVE}"
)

_WORKER_SYSTEM_PREFIX_WITH_TOOLS = (
    "You are one of several independent coding advisors. "
    "Provide a best-effort draft answer for the user. "
    "You have access to read-only tools for information gathering. "
    "Use tools when they would improve your advice quality. "
    "Be concise, correct, and complete. "
    f"{EB1_IDENTITY_DIRECTIVE}"
)

# Backwards compatibility alias
_WORKER_SYSTEM_PREFIX = _WORKER_SYSTEM_PREFIX_NO_TOOLS

_JUDGE_SYSTEM_PREFIX = (
    "You are the final judge in a multi-advisor system. "
    "You receive multiple independent advisor drafts. "
    "Synthesize them into the single best response.\n\n"
    f"{EB1_IDENTITY_DIRECTIVE}\n\n"
    "Evaluation criteria (in priority order):\n"
    "1. Correctness: code must compile/run and handle edge cases.\n"
    "2. Completeness: fully address the user's request.\n"
    "3. Code quality: prefer readable, maintainable solutions.\n"
    "4. Tool call fidelity: when using tools, arguments must be "
    "valid JSON matching the tool schema.\n\n"
    "When advisors agree, synthesize the strongest elements. "
    "When advisors disagree, prefer the most defensible approach "
    "and note trade-offs where appropriate. Do not fabricate "
    "information absent from all advisors.\n\n"
    "If tools are provided and would help, call them. "
    "When calling tools, output only tool calls with valid JSON "
    "arguments. Otherwise, output a normal user-facing answer."
)

_PASSTHROUGH_KEYS: tuple[str, ...] = (
    "instructions",
    "tools",
    "tool_choice",
    "parallel_tool_calls",
    "top_p",
    "max_output_tokens",
    "response_format",
    "metadata",
    "user",
    "modalities",
    "reasoning",
    "previous_response_id",
)


@dataclass(frozen=True, slots=True)
class Eb1CodexConfig:
    eb1_model_name: str = "eb1-codex"
    worker_temperature: float = 0.8
    judge_temperature: float = 0.2
    system_fingerprint: str = "eb1-codex:v1"
    include_debug_metadata: bool = False
    advice_preview_chars: int = 0
    execution_mode: Literal["single", "ensemble"] = "single"


MessagePayload = dict[str, object]
MappingPayload = Mapping[str, object]


@dataclass(frozen=True, slots=True)
class _WorkerInvocation:
    model: str
    temperature: float | None
    advice: str
    usage: UsageStats
    output: list[dict[str, object]] | None = None
    reasoning_summary: str | None = None
    encrypted_reasoning: dict[str, object] | None = None


class Eb1CodexOrchestrator:
    """Synthesize Responses API results for eb1-codex locally."""

    def __init__(
        self,
        config: Eb1CodexConfig | None = None,
        *,
        provider: OpenAIProvider | None = None,
        judge_provider: BaseProvider | None = None,
        judge_provider_factory: Callable[[], BaseProvider] | None = None,
    ) -> None:
        self._cfg = config or Eb1CodexConfig()
        self._provider = provider
        self._judge_provider = judge_provider
        self._judge_provider_factory = judge_provider_factory
        self._providers: dict[str, BaseProvider] = {}
        if provider is not None:
            self._providers["openai"] = provider
        if judge_provider is not None:
            provider_key = "openai" if isinstance(judge_provider, OpenAIProvider) else "anthropic"
            self._providers[provider_key] = judge_provider

    def run(
        self,
        *,
        request: ResponsesRequest,
        prompt_fallback: str,
        system_prompt: str | None,
        plan: ExecutablePlan | Eb1Plan,
        shared_options: Mapping[str, object] | None,
        request_options: Mapping[str, object] | None,
    ) -> ChatResponse:
        plan_mode = _plan_mode_value(plan)

        # Begin telemetry run for observability
        telemetry = get_telemetry()
        run_id = telemetry.begin_run(context={
            "mode": plan_mode,
            "model": self._cfg.eb1_model_name,
            "execution_mode": self._cfg.execution_mode,
        })

        base_messages = self._compose_base_messages(request, prompt_fallback, system_prompt)

        # Single model execution for: single candidate OR judge-only tools policy
        if plan_mode in ("single", "judge_tools"):
            return self._run_single(
                base_messages,
                plan,
                shared_options=shared_options,
                request_options=request_options,
                request=request,
                telemetry_run_id=run_id,
            )

        if self._cfg.execution_mode != "ensemble":
            single_plan = _force_single_candidate_plan(plan)
            return self._run_single(
                base_messages,
                single_plan,
                shared_options=shared_options,
                request_options=request_options,
                request=request,
                telemetry_run_id=run_id,
            )

        worker_plan = self._filter_supported_plan(_eb1_plan(plan))

        worker_results, total_usage = self._run_workers(
            base_messages, worker_plan, shared_options, request=request
        )

        judge_payload, judge_temperature = self._build_judge_payload(
            base_messages,
            plan=worker_plan,
            shared_options=shared_options,
            request_options=request_options,
            request=request,
            advice_block=_format_advice(worker_results) if worker_results else None,
            worker_results=worker_results,
        )

        judge_provider = self._provider_for_model(worker_plan.judge.model)
        with with_limit(worker_plan.judge.model):
            judge_response = judge_provider.complete_responses_payload(judge_payload)
        judge_response, _ = self._finalize_judge_response(
            judge_response,
            plan=worker_plan,
            worker_results=worker_results,
            total_usage=total_usage,
            judge_payload=judge_payload,
            judge_temperature=judge_temperature,
            plan_mode=plan_mode,
            telemetry_run_id=run_id,
        )
        return judge_response

    def stream(
        self,
        *,
        request: ResponsesRequest,
        prompt_fallback: str,
        system_prompt: str | None,
        plan: ExecutablePlan,
        shared_options: Mapping[str, object] | None,
        request_options: Mapping[str, object] | None,
        enable_judge_stream: bool = False,
    ) -> Generator[str, None, ChatResponse]:
        plan_mode = _plan_mode_value(plan)

        if plan_mode in ("single", "judge_tools") or self._cfg.execution_mode != "ensemble":
            effective_plan: ExecutablePlan | Eb1Plan
            if plan_mode in ("single", "judge_tools"):
                effective_plan = plan
            else:
                effective_plan = _force_single_candidate_plan(plan)

            def _single_generator() -> Generator[str, None, ChatResponse]:
                plan_outline = _serialize_plan(_eb1_plan(effective_plan))
                response_id = f"resp_eb_{uuid.uuid4().hex}"

                created_event = json.dumps(
                    {
                        "type": "response.created",
                        "response": {
                            "id": response_id,
                            "model": self._cfg.eb1_model_name,
                            "status": "in_progress",
                            "system_fingerprint": self._cfg.system_fingerprint,
                        },
                    },
                    ensure_ascii=False,
                )
                yield created_event

                response = self.run(
                    request=request,
                    prompt_fallback=prompt_fallback,
                    system_prompt=system_prompt,
                    plan=effective_plan,
                    shared_options=shared_options,
                    request_options=request_options,
                )
                if not response.response_id:
                    response.response_id = response_id
                event = self._serialize_completion_event(
                    response,
                    metadata=None,
                    plan_outline=plan_outline,
                    system_fingerprint=self._cfg.system_fingerprint,
                )
                yield event
                return response

            return _single_generator()

        # Begin telemetry run for ensemble streaming
        telemetry = get_telemetry()
        run_id = telemetry.begin_run(context={
            "mode": plan_mode,
            "model": self._cfg.eb1_model_name,
            "execution_mode": "ensemble",
            "streaming": True,
        })

        base_messages = self._compose_base_messages(request, prompt_fallback, system_prompt)
        worker_plan = self._filter_supported_plan(_eb1_plan(plan))

        worker_results, total_usage = self._run_workers(
            base_messages, worker_plan, shared_options, request=request
        )

        judge_payload, judge_temperature = self._build_judge_payload(
            base_messages,
            plan=worker_plan,
            shared_options=shared_options,
            request_options=request_options,
            request=request,
            advice_block=_format_advice(worker_results) if worker_results else None,
            worker_results=worker_results,
        )

        judge_provider = self._provider_for_model(worker_plan.judge.model)
        judge_provider_name = self._provider_name_for_model(worker_plan.judge.model)
        supports_streaming = enable_judge_stream and judge_provider_name == "openai"

        stream = (
            judge_provider.stream_responses_payload(judge_payload) if supports_streaming else None
        )
        plan_outline = _serialize_plan(worker_plan)
        system_fingerprint = self._cfg.system_fingerprint

        def _generator() -> Generator[str, None, ChatResponse]:
            if stream is None:
                with with_limit(worker_plan.judge.model):
                    judge_response = judge_provider.complete_responses_payload(judge_payload)
                judge_response, metadata = self._finalize_judge_response(
                    judge_response,
                    plan=worker_plan,
                    worker_results=worker_results,
                    total_usage=total_usage,
                    judge_payload=judge_payload,
                    judge_temperature=judge_temperature,
                    plan_mode=plan_mode,
                    telemetry_run_id=run_id,
                )
                event = self._serialize_completion_event(
                    judge_response,
                    metadata=metadata,
                    plan_outline=plan_outline,
                    system_fingerprint=system_fingerprint,
                )
                yield event
                return judge_response

            iterator = iter(stream)
            while True:
                try:
                    chunk = next(iterator)
                except StopIteration as stop:
                    judge_response = stop.value
                    judge_response, metadata = self._finalize_judge_response(
                        judge_response,
                        plan=worker_plan,
                        worker_results=worker_results,
                        total_usage=total_usage,
                        judge_payload=judge_payload,
                        judge_temperature=judge_temperature,
                        plan_mode=plan_mode,
                        telemetry_run_id=run_id,
                    )
                    event = self._serialize_completion_event(
                        judge_response,
                        metadata=metadata,
                        plan_outline=plan_outline,
                        system_fingerprint=system_fingerprint,
                    )
                    yield event
                    return judge_response
                except Exception as exc:
                    raise ProviderAPIError(
                        "Judge stream failed",
                        context={"error": str(exc)},
                    ) from exc
                yield self._rewrite_stream_chunk(chunk)

        return _generator()

    def _provider_name_for_model(self, model: str) -> str:
        provider_name, _ = resolve_model_id(model)
        if provider_name in {"takumi", "eb1", "keiro"}:
            return "openai"
        if provider_name == "unknown":
            raise ProviderAPIError(
                "Unknown provider for model",
                context={"model": model},
            )
        return provider_name

    def _ensure_provider(self, provider_name: str) -> BaseProvider:
        if provider_name in self._providers:
            return self._providers[provider_name]
        if provider_name == "openai":
            openai_provider = self._provider or OpenAIProvider()
            self._provider = openai_provider
            self._providers["openai"] = openai_provider
            return openai_provider
        if provider_name == "anthropic":
            anthropic_provider: BaseProvider
            if self._judge_provider is not None and not isinstance(
                self._judge_provider, OpenAIProvider
            ):
                anthropic_provider = self._judge_provider
            elif self._judge_provider_factory is not None:
                anthropic_provider = self._judge_provider_factory()
                self._judge_provider = anthropic_provider
            elif self._provider is not None and not isinstance(self._provider, OpenAIProvider):
                anthropic_provider = self._provider
            else:
                anthropic_provider = AnthropicProvider()
                self._judge_provider = anthropic_provider
            self._providers["anthropic"] = anthropic_provider
            return anthropic_provider
        raise ProviderAPIError(
            "EB1 codex does not support the requested provider",
            context={"provider": provider_name},
        )

    def _provider_for_model(self, model: str) -> BaseProvider:
        provider_name = self._provider_name_for_model(model)
        return self._ensure_provider(provider_name)

    def _filter_supported_plan(self, plan: Eb1Plan) -> Eb1Plan:
        supported: list[CandidatePlan] = []
        unsupported: list[str] = []
        for candidate in plan.candidates:
            provider_name = self._provider_name_for_model(candidate.model)
            try:
                self._ensure_provider(provider_name)
            except ProviderAPIError:
                unsupported.append(candidate.model)
                continue
            supported.append(candidate)

        if unsupported:
            _LOG.warning(
                "EB1 codex: dropped %d/%d unsupported candidates: %s",
                len(unsupported),
                len(plan.candidates),
                unsupported,
            )

        if not supported:
            raise ProviderAPIError(
                "EB1 codex local backend cannot reach any candidate providers",
                context={"unsupported_candidates": unsupported},
            )

        judge = plan.judge
        try:
            self._ensure_provider(self._provider_name_for_model(judge.model))
        except ProviderAPIError:
            _LOG.warning(
                "EB1 codex: judge model %s unavailable, falling back to %s",
                judge.model,
                supported[0].model,
            )
            judge = JudgePlan(
                model=supported[0].model,
                params=judge.params,
                prompt_template=judge.prompt_template,
                system_template=judge.system_template,
            )

        return Eb1Plan(candidates=tuple(supported), judge=judge)

    def _compose_base_messages(
        self,
        request: ResponsesRequest,
        prompt_fallback: str,
        system_prompt: str | None,
    ) -> list[MessagePayload]:
        base_messages = _normalize_input_messages(request.input)
        if not base_messages and prompt_fallback:
            base_messages = [_text_message("user", prompt_fallback)]
        system_chunks: list[str] = []
        if system_prompt:
            system_chunks.append(system_prompt)
        if request.instructions:
            system_chunks.append(request.instructions)
        if system_chunks:
            base_messages = [_text_message("system", "\n".join(system_chunks))] + base_messages
        return base_messages

    def _run_workers(
        self,
        base_messages: list[MessagePayload],
        plan: Eb1Plan,
        shared_options: Mapping[str, object] | None,
        request: ResponsesRequest | None = None,
    ) -> tuple[list[_WorkerInvocation], UsageStats]:
        workers = plan.candidates or ()
        if not workers:
            raise ProviderAPIError(
                "EB1 codex plan must include at least one candidate",
                context={"model": self._cfg.eb1_model_name},
            )

        worker_results: list[_WorkerInvocation] = []
        total_usage = UsageStats()

        worker_reasoning_source: Mapping[str, object] | None = None
        if request and isinstance(request.reasoning, Mapping):
            worker_reasoning_source = request.reasoning
        elif shared_options:
            candidate_reasoning = shared_options.get("reasoning")
            if isinstance(candidate_reasoning, Mapping):
                worker_reasoning_source = candidate_reasoning
        worker_reasoning = _ensure_reasoning_summary_cfg(worker_reasoning_source)

        # Filter tools for workers: only read-only (x-side-effects: "none") allowed
        original_tools = None
        if request and request.original_payload:
            original_tools = request.original_payload.get("tools")
        worker_tools = None
        if original_tools:
            normalized = normalize_tools(original_tools, model=self._cfg.eb1_model_name)
            filtered = apply_tool_policy(normalized, ToolPolicy.READ_ONLY)
            worker_tools = tools_to_dict_list(filtered) if filtered else None
        worker_has_tools = bool(worker_tools)

        worker_system_prefix = (
            _WORKER_SYSTEM_PREFIX_WITH_TOOLS if worker_has_tools else _WORKER_SYSTEM_PREFIX_NO_TOOLS
        )

        first_error: Exception | None = None

        for index, candidate in enumerate(workers):
            persona = _WORKER_PERSONAS[index % len(_WORKER_PERSONAS)]
            worker_input = [
                _text_message("system", f"{worker_system_prefix} [Advisor persona: {persona}]")
            ] + _clone_messages(base_messages)
            extra_options: dict[str, object] = dict(candidate.params) if candidate.params else {}
            if worker_reasoning:
                extra_options["reasoning"] = worker_reasoning

            if worker_has_tools:
                extra_options["tools"] = worker_tools
                extra_options["tool_choice"] = "auto"

            sanitized_payload, requested_temperature = _build_and_sanitize_payload(
                candidate.model,
                input_messages=worker_input,
                temperature=self._cfg.worker_temperature,
                shared_options=shared_options,
                extra_options=extra_options,
            )

            try:
                provider = self._provider_for_model(candidate.model)
                with with_limit(candidate.model):
                    response = provider.complete_responses_payload(sanitized_payload)
            except Exception as exc:
                if first_error is None:
                    first_error = exc
                _LOG.warning(
                    "EB1 codex: worker %d (%s) failed: %s",
                    index,
                    candidate.model,
                    exc,
                )
                continue

            usage = response.usage or UsageStats()
            total_usage.add(usage)

            full_output = _extract_output_messages(response)
            reasoning_summary = _extract_reasoning_summary_from_output(full_output)
            encrypted_reasoning = _extract_encrypted_reasoning(response)

            worker_results.append(
                _WorkerInvocation(
                    model=response.model_id or candidate.model,
                    temperature=requested_temperature,
                    advice=response.text.strip(),
                    usage=usage,
                    output=full_output,
                    reasoning_summary=reasoning_summary,
                    encrypted_reasoning=encrypted_reasoning,
                )
            )

        if not worker_results:
            raise first_error or ProviderAPIError(
                "All EB1 codex workers failed",
                context={"model": self._cfg.eb1_model_name},
            )

        if first_error is not None:
            _LOG.warning(
                "EB1 codex: %d/%d workers failed; synthesizing from %d successful",
                len(workers) - len(worker_results),
                len(workers),
                len(worker_results),
            )

        return worker_results, total_usage

    def _build_judge_payload(
        self,
        base_messages: list[MessagePayload],
        *,
        plan: Eb1Plan,
        shared_options: Mapping[str, object] | None,
        request_options: Mapping[str, object] | None,
        request: ResponsesRequest,
        advice_block: str | None,
        worker_results: Sequence[_WorkerInvocation],
    ) -> tuple[MessagePayload, float | None]:
        judge_input = [
            _text_message("system", _JUDGE_SYSTEM_PREFIX),
            *_clone_messages(base_messages),
        ]
        if advice_block:
            judge_input.append(_text_message("system", advice_block))
        extra_options: dict[str, object] = dict(plan.judge.params) if plan.judge.params else {}
        sanitized_payload, requested_temperature = _build_and_sanitize_payload(
            plan.judge.model,
            input_messages=judge_input,
            temperature=self._cfg.judge_temperature,
            shared_options=shared_options,
            extra_options=extra_options,
            passthrough_from=request.original_payload,
        )
        _merge_payload(sanitized_payload, request_options)
        existing_metadata = sanitized_payload.get("metadata")
        metadata_dict: dict[str, object] = (
            dict(existing_metadata) if isinstance(existing_metadata, Mapping) else {}
        )
        metadata_dict["ensemble"] = {
            "workers": [
                {
                    "model": worker.model,
                    "temperature": worker.temperature,
                    "reasoning_summary": worker.reasoning_summary,
                }
                for worker in worker_results
            ]
        }
        sanitized_payload["metadata"] = metadata_dict
        return sanitized_payload, requested_temperature

    def _finalize_judge_response(
        self,
        judge_response: ChatResponse,
        *,
        plan: Eb1Plan,
        worker_results: Sequence[_WorkerInvocation],
        total_usage: UsageStats,
        judge_payload: MappingPayload,
        judge_temperature: float | None,
        plan_mode: str,
        telemetry_run_id: str | None = None,
    ) -> tuple[ChatResponse, dict[str, object]]:
        """Finalize judge response with separated internal vs public usage.

        Internal usage (for Ember's COGS): All worker tokens + judge tokens.
        Public usage (user-facing): Approximated user prompt + judge completion.

        This separation prevents users from being billed for internal ensemble
        computations (advisor drafts, reasoning overhead).
        """
        judge_usage = judge_response.usage or UsageStats()

        # Internal usage: full ensemble cost for COGS tracking
        internal_usage = total_usage + judge_usage

        # Public usage: user-facing tokens only
        # - prompt: First worker's prompt approximates user's input (includes small
        #   system prefix, but excludes advice and ensemble overhead)
        # - completion: Judge's output is what the user receives
        public_prompt_tokens = (
            worker_results[0].usage.prompt_tokens if worker_results else judge_usage.prompt_tokens
        )
        public_usage = UsageStats(
            prompt_tokens=public_prompt_tokens,
            completion_tokens=judge_usage.completion_tokens,
            total_tokens=public_prompt_tokens + judge_usage.completion_tokens,
        )
        judge_response.usage = public_usage

        judge_model = judge_response.model_id or str(
            judge_payload.get("model", self._cfg.eb1_model_name)
        )
        metadata: dict[str, object] = {
            "ensemble": {
                "mode": plan_mode,
                "workers": [
                    _worker_metadata(
                        worker,
                        include_debug=self._cfg.include_debug_metadata,
                        advice_preview_chars=self._cfg.advice_preview_chars,
                    )
                    for worker in worker_results
                ],
                "judge": {
                    "model": judge_model,
                    "temperature": judge_temperature,
                },
            }
        }

        # Only include synthesis metadata in user-visible payload when debug is enabled
        # This prevents ensemble details from leaking to clients
        user_visible_metadata = metadata if self._cfg.include_debug_metadata else None
        ensure_responses_output(
            judge_response,
            default_model=self._cfg.eb1_model_name,
            metadata=user_visible_metadata,
        )

        if not isinstance(judge_response.raw_output, MutableMapping):
            raise ProviderAPIError(
                "Invalid response structure after ensure_responses_output",
                context={"model": self._cfg.eb1_model_name},
            )

        raw_payload = judge_response.raw_output
        raw_payload["local_backend"] = True
        raw_payload["plan"] = _serialize_plan(plan)
        raw_payload["system_fingerprint"] = self._cfg.system_fingerprint
        # Debug metadata always goes to raw_payload for internal use
        raw_payload.setdefault("metadata", {}).update(metadata)
        # Store internal usage for COGS tracking (not exposed to users)
        raw_payload["internal_usage"] = _usage_payload(internal_usage)

        if "responses_payload" not in raw_payload:
            raise ProviderAPIError(
                "Missing responses_payload after ensure_responses_output",
                context={"model": self._cfg.eb1_model_name},
            )
        responses_payload = raw_payload["responses_payload"]
        if not isinstance(responses_payload, MutableMapping):
            raise ProviderAPIError(
                "Invalid responses_payload type",
                context={
                    "model": self._cfg.eb1_model_name,
                    "type": type(responses_payload).__name__,
                },
            )
        # Only include synthesis metadata in responses_payload when debug is enabled.
        # This prevents ensemble details from leaking to clients in the API response.
        if self._cfg.include_debug_metadata:
            existing_metadata = responses_payload.get("metadata")
            if isinstance(existing_metadata, MutableMapping):
                existing_metadata.update(metadata)
            else:
                responses_payload["metadata"] = metadata

        _patch_top_level_model(raw_payload, self._cfg.eb1_model_name)
        judge_response.model_id = self._cfg.eb1_model_name

        # Store telemetry run_id and end the run
        if telemetry_run_id:
            raw_payload["telemetry_run_id"] = telemetry_run_id
            get_telemetry().end_run(telemetry_run_id, outcome={
                "status": "completed",
                "mode": plan_mode,
                "worker_count": len(worker_results),
                "judge_model": judge_model,
                "usage": _usage_payload(internal_usage),
            })

        return judge_response, metadata

    def _run_single(
        self,
        base_messages: list[MessagePayload],
        plan: ExecutablePlan | Eb1Plan,
        *,
        shared_options: Mapping[str, object] | None,
        request_options: Mapping[str, object] | None,
        request: ResponsesRequest,
        telemetry_run_id: str | None = None,
    ) -> ChatResponse:
        eb1_plan = _eb1_plan(plan)
        if not eb1_plan.candidates or len(eb1_plan.candidates) != 1:
            raise ProviderAPIError(
                "EB1 codex single-mode plan requires exactly one candidate",
                context={"model": self._cfg.eb1_model_name},
            )

        candidate = eb1_plan.candidates[0]
        provider = self._provider_for_model(candidate.model)
        extra_options: dict[str, object] = dict(candidate.params) if candidate.params else {}
        _merge_payload(extra_options, request_options)
        sanitized_payload, _ = _build_and_sanitize_payload(
            candidate.model,
            input_messages=_clone_messages(base_messages),
            temperature=self._cfg.worker_temperature,
            shared_options=shared_options,
            extra_options=extra_options,
            passthrough_from=request.original_payload,
        )

        with with_limit(candidate.model):
            response = provider.complete_responses_payload(sanitized_payload)
        metadata = {
            "ensemble": {
                "mode": "single",
                "workers": [],
                "judge": {
                    "model": candidate.model,
                    "temperature": self._cfg.worker_temperature,
                },
            }
        }
        # Only include synthesis metadata in user-visible payload when debug is enabled
        user_visible_metadata = metadata if self._cfg.include_debug_metadata else None
        ensure_responses_output(
            response,
            default_model=self._cfg.eb1_model_name,
            metadata=user_visible_metadata,
        )

        if not isinstance(response.raw_output, MutableMapping):
            raise ProviderAPIError(
                "Invalid response structure after ensure_responses_output",
                context={"model": self._cfg.eb1_model_name},
            )

        raw_output = response.raw_output
        raw_output["local_backend"] = True
        raw_output["system_fingerprint"] = self._cfg.system_fingerprint
        raw_output["plan"] = _serialize_plan(eb1_plan)
        # Debug metadata always goes to raw_output for internal use
        raw_output.setdefault("metadata", {}).update(metadata)
        _patch_top_level_model(raw_output, self._cfg.eb1_model_name)
        response.model_id = self._cfg.eb1_model_name

        # Store telemetry run_id and end the run
        if telemetry_run_id:
            raw_output["telemetry_run_id"] = telemetry_run_id
            get_telemetry().end_run(telemetry_run_id, outcome={
                "status": "completed",
                "mode": "single",
                "model": candidate.model,
                "usage": _usage_payload(response.usage) if response.usage else {},
            })

        return response

    def _serialize_completion_event(
        self,
        response: ChatResponse,
        *,
        metadata: Mapping[str, object] | None,
        plan_outline: Mapping[str, object],
        system_fingerprint: str,
    ) -> str:
        if not isinstance(response.raw_output, MutableMapping):
            raise ProviderAPIError(
                "Invalid response structure",
                context={"model": self._cfg.eb1_model_name},
            )

        raw_output = response.raw_output
        if "responses_payload" not in raw_output:
            raise ProviderAPIError(
                "Missing responses_payload in response",
                context={"model": self._cfg.eb1_model_name},
            )
        responses_payload = raw_output["responses_payload"]
        if not isinstance(responses_payload, MutableMapping):
            raise ProviderAPIError(
                "Invalid responses_payload type",
                context={
                    "model": self._cfg.eb1_model_name,
                    "type": type(responses_payload).__name__,
                },
            )

        final_payload: dict[str, object] = dict(responses_payload)
        final_payload["model"] = self._cfg.eb1_model_name
        final_payload["system_fingerprint"] = system_fingerprint
        final_payload["plan_outline"] = plan_outline
        # Only include synthesis metadata in streamed events when debug is enabled.
        # This prevents ensemble details from leaking to clients.
        if metadata and self._cfg.include_debug_metadata:
            existing_metadata = final_payload.get("metadata")
            if isinstance(existing_metadata, MutableMapping):
                existing_metadata.update(metadata)
            else:
                final_payload["metadata"] = metadata

        status = final_payload.get("status", "completed")
        if status == "requires_action" and "required_action" in final_payload:
            event_type = "response.required_action"
        elif status == "incomplete":
            event_type = "response.incomplete"
        elif status == "failed":
            event_type = "response.failed"
        elif status == "cancelled":
            event_type = "response.cancelled"
        else:
            event_type = "response.completed"

        return json.dumps(
            {
                "type": event_type,
                "response": final_payload,
            },
            ensure_ascii=False,
        )

    def _rewrite_stream_chunk(self, chunk: str) -> str:
        if not chunk.strip() or chunk.strip() == "[DONE]" or not chunk.strip().startswith("{"):
            return chunk

        try:
            event: dict[str, object] = json.loads(chunk.strip())
        except json.JSONDecodeError:
            return chunk

        event_type = event.get("type")

        _HAS_RESPONSE = {
            "response.created",
            "response.completed",
            "response.incomplete",
            "response.failed",
            "response.cancelled",
            "response.required_action",
        }

        if event_type in _HAS_RESPONSE:
            response_obj = event.get("response")
            if isinstance(response_obj, MutableMapping):
                _patch_top_level_model(response_obj, self._cfg.eb1_model_name)
                response_obj["system_fingerprint"] = self._cfg.system_fingerprint
            elif isinstance(event.get("model"), str):
                event["model"] = self._cfg.eb1_model_name
            return json.dumps(event, ensure_ascii=False)

        if event_type == "response.text.delta":
            event["type"] = "response.output_text.delta"
            return json.dumps(event, ensure_ascii=False)

        return chunk


def _eb1_plan(plan: "ExecutablePlan" | Eb1Plan) -> Eb1Plan:
    return plan.to_plan() if hasattr(plan, "to_plan") else plan


def _force_single_candidate_plan(plan: "ExecutablePlan" | Eb1Plan) -> Eb1Plan:
    eb1_plan = _eb1_plan(plan)
    if not eb1_plan.candidates:
        raise ProviderAPIError(
            "EB1 codex plan must include at least one candidate",
            context={"model": "eb1-codex"},
        )
    return Eb1Plan(candidates=(eb1_plan.candidates[0],), judge=eb1_plan.judge)


def _plan_mode_value(plan: "ExecutablePlan" | Eb1Plan) -> str:
    mode = getattr(plan, "mode", None)
    if mode is None:
        candidates = getattr(plan, "candidates", ())
        return "single" if len(tuple(candidates)) <= 1 else "orchestrate"
    return getattr(mode, "value", str(mode))


def _ensure_reasoning_summary_cfg(
    cfg: Mapping[str, object] | None,
) -> Mapping[str, object] | None:
    if cfg is None:
        return None
    out = dict(cfg)
    out.setdefault("summary", "auto")
    return out


def _build_and_sanitize_payload(
    model: str,
    *,
    input_messages: list[MessagePayload],
    temperature: float | None,
    shared_options: Mapping[str, object] | None = None,
    extra_options: Mapping[str, object] | None = None,
    passthrough_from: Mapping[str, object] | None = None,
) -> tuple[MessagePayload, float | None]:
    payload: MessagePayload = {"model": model, "input": input_messages}
    if temperature is not None:
        payload["temperature"] = temperature
    _merge_payload(payload, shared_options)
    _merge_payload(payload, extra_options)
    if passthrough_from:
        for key in _PASSTHROUGH_KEYS:
            if key in passthrough_from:
                payload[key] = copy.deepcopy(passthrough_from[key])
    requested_temperature = _read_temperature(payload)
    sanitized = sanitize_responses_payload(str(payload.get("model", model)), payload)
    return sanitized, requested_temperature


def _normalize_input_messages(
    input_value: Sequence[ResponsesMessage] | str | None,
) -> list[MessagePayload]:
    if input_value is None:
        return []
    if isinstance(input_value, str):
        text = input_value.strip()
        return [_text_message("user", text)] if text else []
    return [_serialize_message(message) for message in input_value]


def _serialize_message(message: ResponsesMessage) -> MessagePayload:
    content = [{"type": part.type, **dict(part.fields)} for part in message.content]
    entry: MessagePayload = {"role": message.role, "content": content}
    if message.status is not None:
        entry["status"] = message.status
    return entry


def _text_message(role: str, text: str) -> MessagePayload:
    return {"role": role, "content": [{"type": "input_text", "text": text}]}


def _clone_messages(messages: Iterable[MessagePayload]) -> list[MessagePayload]:
    return [copy.deepcopy(message) for message in messages]


def _merge_payload(
    target: MutableMapping[str, object],
    source: Mapping[str, object] | None,
) -> None:
    if source is None:
        return
    for key, value in source.items():
        target[key] = copy.deepcopy(value)


def _format_advice(worker_results: Sequence[_WorkerInvocation]) -> str:
    segments: list[str] = []
    for idx, worker in enumerate(worker_results, start=1):
        parts = [worker.advice]
        if worker.reasoning_summary:
            parts.append(f"[Reasoning: {worker.reasoning_summary}]")
        body = "\n\n".join(p for p in parts if p)
        segments.append(
            f'<advisor index="{idx}" model="{worker.model}">\n'
            f"{body}\n"
            f"</advisor>"
        )
    block = "\n\n".join(segments).strip()
    prefix = "Advisor drafts (for judge only):"
    if not block:
        return prefix
    return f"{prefix}\n{block}"


def _extract_output_messages(response: ChatResponse) -> list[dict[str, object]] | None:
    if not isinstance(response.output, Sequence):
        return None
    messages: list[dict[str, object]] = []
    for item in response.output:
        if isinstance(item, Mapping):
            messages.append(dict(item))
    return messages if messages else None


def _extract_reasoning_summary_from_output(
    output: Sequence[Mapping[str, object]] | None,
) -> str | None:
    if output is None:
        return None
    for message in output:
        if message.get("type") != "message":
            continue
        content = message.get("content", [])
        if not isinstance(content, Iterable):
            continue
        for chunk in content:
            if isinstance(chunk, Mapping) and chunk.get("type") == "reasoning_summary_text":
                text = chunk.get("text")
                if isinstance(text, str) and text.strip():
                    return text.strip()
    return None


def _extract_encrypted_reasoning(response: ChatResponse) -> dict[str, object] | None:
    raw_output = response.raw_output
    if not isinstance(raw_output, Mapping):
        return None
    encrypted = raw_output.get("reasoning")
    if isinstance(encrypted, Mapping):
        return dict(encrypted)
    return None


def _worker_metadata(
    worker: _WorkerInvocation,
    *,
    include_debug: bool,
    advice_preview_chars: int,
) -> dict[str, object]:
    metadata: dict[str, object] = {
        "model": worker.model,
        "temperature": worker.temperature,
        "usage": _usage_payload(worker.usage),
    }
    if include_debug and worker.advice:
        preview = worker.advice[:advice_preview_chars] if advice_preview_chars else worker.advice
        metadata["advice_preview"] = preview
    if worker.reasoning_summary:
        metadata["reasoning_summary"] = worker.reasoning_summary
    if worker.encrypted_reasoning:
        metadata["reasoning_encrypted"] = worker.encrypted_reasoning
    return metadata


def _serialize_plan(plan: Eb1Plan) -> dict[str, object]:
    return {
        "candidates": [candidate.model for candidate in plan.candidates],
        "judge": plan.judge.model,
    }


def _ensure_dict(value: object) -> dict[str, object]:
    if not isinstance(value, MutableMapping):
        raise TypeError(f"Expected mapping, received {type(value).__name__}")
    return dict(value)


def _usage_payload(usage: UsageStats) -> dict[str, int]:
    return {
        "prompt_tokens": usage.prompt_tokens,
        "completion_tokens": usage.completion_tokens,
        "total_tokens": usage.total_tokens,
    }


def _patch_top_level_model(obj: MutableMapping[str, object], model_name: str) -> None:
    if "model" in obj and isinstance(obj["model"], str):
        obj["model"] = model_name
    responses_payload = obj.get("responses_payload")
    if isinstance(responses_payload, MutableMapping) and isinstance(
        responses_payload.get("model"), str
    ):
        responses_payload["model"] = model_name


def _read_temperature(payload: MappingPayload) -> float | None:
    value = payload.get("temperature")
    if value is None:
        return None
    if isinstance(value, (int, float)):
        return float(value)
    raise ProviderAPIError(
        "Invalid temperature in EB1 codex payload",
        context={"temperature": value},
    )


__all__ = ["Eb1CodexConfig", "Eb1CodexOrchestrator"]
