"""TakumiAdapter and associated helpers for EB1 ensemble execution.

This module contains the TakumiAdapter (which proxies TakumiProvider and
hosts local EB1 execution) along with all plan selection, context building,
usage normalization, and credential helpers.
"""

from __future__ import annotations

import json
import sys as _sys
import time
import uuid
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Any, Final, Generator, Mapping, MutableMapping, Sequence
from urllib.parse import urlparse

from ember._internal.context.runtime import EmberContext
from ember._internal.exceptions import ProviderAPIError
from ember.core.credentials import CredentialNotFoundError
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.takumi import TakumiProvider
from ember.models.providers.utils import (
    ResponsesMessage,
    ResponsesRequest,
    build_prompt_components,
    parse_responses_request,
)
from ember.models.schemas import ChatResponse, UsageStats

from keiro.ensemble.config import get_takumi_settings
from keiro.ensemble.eb1 import (
    CandidatePlan,
    Eb1Plan,
    JudgePlan,
    _telemetry_error_payload,
    run_plan,
)
from keiro.ensemble.eb1_a import run_eb1_a
from keiro.ensemble.engine import _ensure_identity
from keiro.ensemble.responses import ensure_responses_output
from keiro.protocol import parse_responses_tool_calls

from ..local import Eb1CodexConfig, Eb1CodexOrchestrator
from ..providers.features import feature_names
from ..providers.planner import ExecutablePlan, compile_plan
from .adapters import (
    ProviderClientAdapter,
    _adapter_begin_run,
    _adapter_record_error,
    _normalize_request_tools_ir,
    _record_telemetry_outcome,
    _response_to_tuple,
    _validate_streaming_tools_ir,
)

_EB1_COMPLETION_SOFT_CAP: Final[int] = 8_192  # keep local calls under SDK cap

# Module path for the adapters module.  Tests monkeypatch patchable
# functions (``_run_local_eb1``, ``compile_plan``, etc.) on the
# ``adapters`` module rather than on this module.  The ``_pa`` helper
# resolves names from ``adapters`` at call time so patches take effect.
_ADAPTERS_MODULE = 'keiro.gateway.infra.providers.adapters'


def _pa(name: str) -> Any:
    """Look up *name* on the adapters module (late-bound for monkeypatch)."""
    return getattr(_sys.modules[_ADAPTERS_MODULE], name)


ContextPayload = Sequence[Mapping[str, str]] | Mapping[str, str] | str | None


@dataclass(slots=True)
class _TakumiCallContext:
    """Aggregates per-call state so _invoke can delegate cleanly."""

    model: str
    prompt_text: str
    temperature: float
    streaming: bool
    params: dict[str, Any]
    enable_judge_stream: bool
    responses_payload: Mapping[str, Any] | None
    api_key_override: str | None
    system_prompt: str | None
    context_payload: ContextPayload
    is_local_backend: bool
    is_eb1_model: bool
    selected_plan: Eb1Plan
    org_limits: Mapping[str, Any] | None
    requested_reasoning_effort: str | None
    provider: BaseProvider | None = None


class TakumiAdapter(ProviderClientAdapter):
    """Adapter that proxies TakumiProvider and hosts local EB1 execution."""

    supports_streaming = True

    def __init__(self) -> None:
        super().__init__("takumi", TakumiProvider)

    # Public entrypoints -------------------------------------------------

    def complete(
        self,
        model: str,
        prompt_text: str,
        temperature: float = 0.0,
        **kwargs: Any,
    ) -> tuple[str, int, int, object | None]:
        return self._invoke(model, prompt_text, temperature, streaming=False, **kwargs)

    def stream_complete(
        self,
        model: str,
        prompt_text: str,
        temperature: float = 0.0,
        **kwargs: Any,
    ) -> Generator[str, None, ChatResponse]:
        result = self._invoke(model, prompt_text, temperature, streaming=True, **kwargs)
        assert isinstance(result, Generator)
        return result

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

    def _invoke(
        self,
        model: str,
        prompt_text: str,
        temperature: float,
        *,
        streaming: bool,
        **kwargs: Any,
    ) -> Any:
        ctx = self._build_call_context(
            model,
            prompt_text,
            temperature,
            streaming=streaming,
            **kwargs,
        )

        if ctx.responses_payload is not None:
            return self._handle_responses_payload(ctx)

        if ctx.is_local_backend:
            return self._invoke_local(ctx)

        return self._invoke_remote(ctx)

    def _build_call_context(
        self,
        model: str,
        prompt_text: str,
        temperature: float,
        *,
        streaming: bool,
        **kwargs: Any,
    ) -> _TakumiCallContext:
        params: dict[str, Any] = dict(kwargs)
        responses_payload = params.pop("responses_payload", None)
        api_key_override = params.pop("api_key_override", None)
        org_limits = params.pop("org_limits", None)

        system_value = params.get("system")
        if system_value is None:
            system_prompt = None
        elif isinstance(system_value, str):
            system_prompt = system_value
        else:
            raise TypeError("system must be a string when provided")

        context_value = params.get("context")
        if context_value is None:
            context_payload: ContextPayload = None
        elif isinstance(context_value, str):
            context_payload = context_value
        elif isinstance(context_value, Mapping):
            context_payload = context_value
        elif isinstance(context_value, Sequence) and not isinstance(
            context_value, (str, bytes, bytearray)
        ):
            context_payload = context_value
        else:
            raise TypeError("context must be a string, mapping, or sequence of mappings")

        if responses_payload is not None and not isinstance(responses_payload, Mapping):
            raise TypeError("responses_payload must be a mapping when provided")
        if api_key_override is not None and not isinstance(api_key_override, str):
            raise TypeError("api_key_override must be a string when provided")
        if org_limits is not None and not isinstance(org_limits, Mapping):
            raise TypeError("org_limits must be a mapping when provided")

        base_url = _pa('_takumi_base_url')()
        parsed_url = urlparse(base_url) if base_url else None
        host = parsed_url.hostname.lower() if parsed_url and parsed_url.hostname else ""
        is_local_backend = host in {"127.0.0.1", "localhost"}

        model_lower = model.lower()
        is_eb1_model = "eb1" in model_lower or model_lower == "takumi"
        selected_plan = _select_default_plan(model)
        reasoning_config, requested_effort = _extract_reasoning_mode(params)
        if reasoning_config is not None:
            params["reasoning"] = reasoning_config
        if requested_effort in {"high", "xhigh", "max"} and _canonicalize_eb1_model(model).endswith(
            "eb1-pro"
        ):
            selected_plan = _select_default_plan("takumi/eb1-pro-high")
        enable_judge_stream = bool(params.pop("enable_judge_stream", False))

        return _TakumiCallContext(
            model=model,
            prompt_text=prompt_text,
            temperature=temperature,
            streaming=streaming,
            params=params,
            enable_judge_stream=enable_judge_stream,
            responses_payload=responses_payload,
            api_key_override=api_key_override,
            system_prompt=system_prompt,
            context_payload=context_payload,
            is_local_backend=is_local_backend,
            is_eb1_model=is_eb1_model,
            selected_plan=selected_plan,
            org_limits=org_limits,
            requested_reasoning_effort=requested_effort,
        )

    def _handle_responses_payload(self, ctx: _TakumiCallContext) -> Any:
        payload = dict(ctx.responses_payload)
        payload.setdefault("model", ctx.model)
        payload.pop("stream", None)

        # Validate streaming with tools for ensemble variants (fail closed)
        tools = payload.get("tools")
        _validate_streaming_tools_ir(ctx.model, ctx.streaming, tools)

        if ctx.is_local_backend and ctx.is_eb1_model:
            return self._run_local_responses(ctx, payload)
        if ctx.is_local_backend:
            raise ProviderAPIError(
                "Local Takumi backend only supports EB1 family models for Responses payloads.",
                context={"model": ctx.model},
            )

        _require_takumi_api_key(ctx.api_key_override)
        provider = self._create_provider(ctx.api_key_override)
        if not provider.supports_responses_api:
            raise ProviderAPIError(
                "Takumi provider does not support responses payload streaming",
                context={"model": ctx.model},
            )

        if ctx.streaming:
            payload = dict(payload)
            if ctx.enable_judge_stream:
                payload["enable_judge_stream"] = True
            return provider.stream_responses_payload(payload)

        response = provider.complete_responses_payload(payload)
        return _response_to_tuple(response)

    def _run_local_responses(
        self,
        ctx: _TakumiCallContext,
        payload: Mapping[str, Any],
    ) -> Any:
        request = parse_responses_request(payload)

        # Apply IR normalization and validation unconditionally
        _validate_streaming_tools_ir(ctx.model, ctx.streaming, request.tools)
        request = _normalize_request_tools_ir(request, ctx.model)

        components = build_prompt_components(request)

        prompt_for_local = (
            _prompt_from_responses_input(request.input)
            or components.prompt
            or ctx.prompt_text
            or ""
        )

        context_messages = _context_from_responses_messages(request.messages)
        if not context_messages:
            context_messages = tuple(components.context)

        system_for_local = (
            components.system or _system_from_context(context_messages) or ctx.system_prompt
        )

        system_for_eb1 = _ensure_identity(system_for_local)
        shared_options = _build_local_shared_options(ctx.params, request)
        local_request_options = _build_local_request_options(
            ctx.params,
            request.response_format,
        )

        compiled_plan = _pa('compile_plan')(
            ctx.selected_plan,
            request,
            has_credentials=_pa('_provider_has_credentials'),
        )

        if ctx.streaming:
            return self._local_stream(
                prompt_for_local,
                system=system_for_eb1,
                shared_options=shared_options,
                request_options=local_request_options,
                responses_request=request,
                context_messages=context_messages,
                plan=ctx.selected_plan,
                requested_model=ctx.model,
                org_limits=ctx.org_limits,
                enable_judge_stream=ctx.enable_judge_stream,
                compiled_plan=compiled_plan,
                api_key_override=ctx.api_key_override,
            )
        return self._local_sync(
            prompt_for_local,
            system=system_for_eb1,
            shared_options=shared_options,
            request_options=local_request_options,
            responses_request=request,
            context_messages=context_messages,
            plan=ctx.selected_plan,
            requested_model=ctx.model,
            org_limits=ctx.org_limits,
            compiled_plan=compiled_plan,
            api_key_override=ctx.api_key_override,
        )

    def _invoke_local(self, ctx: _TakumiCallContext) -> Any:
        if not ctx.is_eb1_model:
            raise ProviderAPIError(
                "Local Takumi backend fallback is disabled; unsupported model for local execution",
                context={"model": ctx.model, "backend": "local"},
            )

        context_entries = _coerce_context_entries(ctx.context_payload)
        prompt_for_local = _compose_prompt_for_eb1(ctx.prompt_text, context_entries)
        system_for_eb1 = _ensure_identity(ctx.system_prompt)
        shared_options = _build_local_shared_options(ctx.params, None)
        local_request_options = _build_local_request_options(ctx.params, None)

        if ctx.streaming:
            return self._local_stream(
                prompt_for_local,
                system=system_for_eb1,
                shared_options=shared_options,
                request_options=local_request_options,
                responses_request=None,
                context_messages=None,
                plan=ctx.selected_plan,
                requested_model=ctx.model,
                org_limits=ctx.org_limits,
                enable_judge_stream=ctx.enable_judge_stream,
                api_key_override=ctx.api_key_override,
            )
        return self._local_sync(
            prompt_for_local,
            system=system_for_eb1,
            shared_options=shared_options,
            request_options=local_request_options,
            responses_request=None,
            context_messages=None,
            plan=ctx.selected_plan,
            requested_model=ctx.model,
            org_limits=ctx.org_limits,
            api_key_override=ctx.api_key_override,
        )

    def _invoke_remote(self, ctx: _TakumiCallContext) -> Any:
        ctx.params.pop("stream", None)
        ctx.params.setdefault("temperature", ctx.temperature)

        if ctx.provider is None:
            _require_takumi_api_key(ctx.api_key_override)
            provider = self._create_provider(ctx.api_key_override)
            ctx.provider = provider
        else:
            provider = ctx.provider

        if ctx.streaming:
            return provider.stream_complete(
                ctx.prompt_text,
                ctx.model,
                **ctx.params,
            )

        response = provider.complete(
            ctx.prompt_text,
            ctx.model,
            **ctx.params,
        )
        return _response_to_tuple(response)

    @staticmethod
    def _local_sync(
        prompt_text: str,
        *,
        system: str | None,
        shared_options: Mapping[str, Any] | None,
        request_options: Mapping[str, Any] | None,
        responses_request: ResponsesRequest | None,
        context_messages: Sequence[Mapping[str, str]] | None,
        plan: Eb1Plan,
        requested_model: str,
        org_limits: Mapping[str, Any] | None,
        compiled_plan: ExecutablePlan | None = None,
        api_key_override: str | None = None,
    ) -> tuple[str, int, int, object | None]:
        run_kwargs = TakumiAdapter._build_local_run_kwargs(
            system=system,
            shared_options=shared_options,
            request_options=request_options,
            responses_request=responses_request,
            context_messages=context_messages,
            org_limits=org_limits,
        )
        return _response_to_tuple(
            _pa('_run_local_eb1')(
                _resolve_plan(compiled_plan, plan),
                prompt_text,
                requested_model=requested_model,
                api_key_override=api_key_override,
                **run_kwargs,
            )
        )

    @staticmethod
    def _local_stream(
        prompt_text: str,
        *,
        system: str | None,
        shared_options: Mapping[str, Any] | None,
        request_options: Mapping[str, Any] | None,
        responses_request: ResponsesRequest | None,
        context_messages: Sequence[Mapping[str, str]] | None,
        plan: Eb1Plan,
        requested_model: str,
        org_limits: Mapping[str, Any] | None,
        enable_judge_stream: bool = False,
        compiled_plan: ExecutablePlan | None = None,
        api_key_override: str | None = None,
    ) -> Generator[str, None, ChatResponse]:
        run_kwargs = TakumiAdapter._build_local_run_kwargs(
            system=system,
            shared_options=shared_options,
            request_options=request_options,
            responses_request=responses_request,
            context_messages=context_messages,
            org_limits=org_limits,
        )

        stream_generator: Generator[str, None, ChatResponse] | None = None
        if enable_judge_stream:
            stream_generator = _pa('_run_local_eb1_stream')(
                _resolve_plan(compiled_plan, plan),
                prompt_text,
                requested_model=requested_model,
                enable_judge_stream=enable_judge_stream,
                compiled_plan=compiled_plan,
                api_key_override=api_key_override,
                **run_kwargs,
            )

        if stream_generator is not None:
            while True:
                try:
                    chunk = next(stream_generator)
                except StopIteration as stop:
                    return stop.value
                else:
                    yield chunk

        response = _pa('_run_local_eb1')(
            _resolve_plan(compiled_plan, plan),
            prompt_text,
            requested_model=requested_model,
            compiled_plan=compiled_plan,
            api_key_override=api_key_override,
            **run_kwargs,
        )
        final_payload = _build_sync_final_payload(response)
        for event in _build_sync_stream_events(response, final_payload):
            yield event
        return response

    # Utility ------------------------------------------------------------

    @staticmethod
    def _build_local_run_kwargs(
        *,
        system: str | None,
        shared_options: Mapping[str, Any] | None,
        request_options: Mapping[str, Any] | None,
        responses_request: ResponsesRequest | None,
        context_messages: Sequence[Mapping[str, str]] | None,
        org_limits: Mapping[str, Any] | None,
    ) -> dict[str, Any]:
        run_kwargs: dict[str, Any] = {"system": system}
        if shared_options is not None:
            run_kwargs["shared_options"] = shared_options
        if request_options is not None:
            run_kwargs["request_options"] = request_options
        if context_messages:
            run_kwargs["context_messages"] = context_messages
        if responses_request is not None:
            run_kwargs["responses_request"] = responses_request
        if org_limits is not None:
            run_kwargs["org_limits"] = dict(org_limits)
        return run_kwargs


# ---------------------------------------------------------------------------
# Adapter registry helpers
# ---------------------------------------------------------------------------


def _require_takumi_api_key(api_key_override: str | None) -> None:
    """Raise when the current context cannot authenticate to Takumi Cloud."""

    if api_key_override is not None:
        return
    EmberContext.current().get_credential("takumi")


def _provider_has_credentials(provider: str) -> bool:
    """Return ``True`` when the current context has credentials for ``provider``."""
    # Gateway providers (takumi/eb1) and unknown providers bypass credential checks
    # since they route to underlying providers or are resolved elsewhere.
    if provider in {"takumi", "eb1", "keiro", "unknown"}:
        return True

    if provider not in {"openai", "anthropic", "google", "azure_openai"}:
        raise KeyError(f"Unhandled provider credential probe for '{provider}'")

    ctx = EmberContext.current()
    try:
        ctx.get_credential(provider)
    except CredentialNotFoundError:
        return False
    return True


# ---------------------------------------------------------------------------
# EB1 plan selection & local fallback helpers
# ---------------------------------------------------------------------------


def _filter_plan_for_local(plan: Eb1Plan) -> Eb1Plan:
    """Filter an EB1 plan to providers that have credentials locally configured."""

    _has_creds = _pa('_provider_has_credentials')
    available = {
        provider
        for provider in {"openai", "anthropic", "google", "azure_openai"}
        if _has_creds(provider)
    }

    filtered_candidates: list[CandidatePlan] = []
    missing_providers: set[str] = set()

    for candidate in plan.candidates:
        provider, _ = resolve_model_id(candidate.model)
        if provider in {"takumi", "eb1", "keiro", "unknown"}:
            filtered_candidates.append(candidate)
            continue
        if provider in available:
            filtered_candidates.append(candidate)
        else:
            missing_providers.add(provider)

    if not filtered_candidates:
        raise ProviderAPIError(
            "No EB1 candidate models are available with the configured provider credentials.",
            context={"required_providers": sorted(missing_providers)},
        )

    judge = plan.judge
    judge_provider, _ = resolve_model_id(judge.model)
    if judge_provider not in {"takumi", "eb1", "keiro", "unknown"} and judge_provider not in available:
        judge = JudgePlan(
            model=filtered_candidates[0].model,
            params=judge.params,
            prompt_template=judge.prompt_template,
            system_template=judge.system_template,
        )

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


def _plan_debug(executable: ExecutablePlan) -> Mapping[str, Any]:
    """Return a serializable summary of the compiled plan."""

    return {
        "mode": executable.mode.value,
        "judge": executable.judge.model,
        "candidates": [candidate.model for candidate in executable.candidates],
        "reason": executable.reason,
        "required_features": feature_names(executable.features),
    }


def _select_default_plan(model: str) -> Eb1Plan:
    """Delegate to the canonical plan selector in ``ember.takumi.plans``."""
    from keiro.ensemble.plans import select_plan

    return select_plan(model)


def _is_codex_plan(plan: Eb1Plan) -> bool:
    """Check if plan uses a GPT-5 codex variant (gpt-5-codex, gpt-5.1-codex, etc.)."""
    return any(
        "gpt-5" in candidate.model.lower() and "codex" in candidate.model.lower()
        for candidate in plan.candidates
    )


_CODEX_ORCHESTRATOR = Eb1CodexOrchestrator(
    Eb1CodexConfig(execution_mode="single"),
)
_EB2_CODEX_ORCHESTRATOR = Eb1CodexOrchestrator(
    Eb1CodexConfig(
        eb1_model_name="eb2-codex",
        system_fingerprint="eb2-codex:v1",
        execution_mode="ensemble",
    ),
    judge_provider_factory=AnthropicProvider,
)


def _is_codex_model(model: str | None) -> bool:
    if not model:
        return False
    canonical = _canonicalize_eb1_model(model)
    suffix = canonical.split("/", 1)[-1]
    return suffix.startswith("eb1-codex") or suffix.startswith("eb2-codex")


def _codex_orchestrator(
    model: str | None,
    *,
    api_key_override: str | None = None,
) -> Eb1CodexOrchestrator:
    """Return an orchestrator for the given model, using a singleton when no override."""
    canonical = _canonicalize_eb1_model(model)
    is_eb2 = canonical.endswith("eb2-codex")

    if api_key_override is None:
        return _EB2_CODEX_ORCHESTRATOR if is_eb2 else _CODEX_ORCHESTRATOR

    override_provider = OpenAIProvider(api_key_override)
    if is_eb2:
        return Eb1CodexOrchestrator(
            Eb1CodexConfig(
                eb1_model_name="eb2-codex",
                system_fingerprint="eb2-codex:v1",
                execution_mode="ensemble",
            ),
            provider=override_provider,
            judge_provider_factory=AnthropicProvider,
        )
    return Eb1CodexOrchestrator(
        Eb1CodexConfig(execution_mode="single"),
        provider=override_provider,
    )


def _canonicalize_eb1_model(model: str | None) -> str:
    if not model:
        return "takumi/eb1"
    provider, resolved_model = resolve_model_id(model)
    if provider in {"takumi", "eb1", "keiro"}:
        return f"takumi/{resolved_model}"
    return model


def _is_eb1_a_model(model: str | None) -> bool:
    if not model:
        return False
    candidate = model.strip().lower()
    if candidate.startswith("takumi/"):
        candidate = candidate.split("/", 1)[1]
    return candidate.startswith("eb1-a")


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


def _apply_completion_limit(
    options: Mapping[str, Any] | None,
    *,
    completion_limit: int | None,
    responses_request: ResponsesRequest | None,
) -> Mapping[str, Any] | None:
    if completion_limit is None:
        return options

    bound = min(completion_limit, _EB1_COMPLETION_SOFT_CAP)
    updated: dict[str, Any] = dict(options) if options is not None else {}

    def _clamp(option_key: str) -> None:
        if option_key not in updated:
            return
        value = updated[option_key]
        candidate = int(value)
        updated[option_key] = bound if candidate <= 0 or candidate > bound else candidate

    _clamp("max_output_tokens")
    _clamp("max_tokens")

    requested_tokens = (
        responses_request.max_output_tokens if responses_request is not None else None
    )
    if requested_tokens is not None and requested_tokens > bound:
        updated["max_output_tokens"] = bound

    updated.setdefault("max_output_tokens", bound)
    return updated


def _extract_limits(
    limits: Mapping[str, Any] | None,
) -> tuple[int | None, int | None, int | None]:
    if limits is None:
        return None, None, None
    try:
        prompt = int(limits["prompt_tokens"])
        completion = int(limits["completion_tokens"])
        multiplier = int(limits["prompt_multiplier"])
    except (KeyError, TypeError, ValueError) as exc:  # pragma: no cover
        raise ValueError("org_limits payload is malformed") from exc
    return prompt, completion, multiplier


def _build_limits_payload(
    plan_outline: Mapping[str, Any],
    *,
    prompt_limit: int | None,
    completion_limit: int | None,
    prompt_multiplier: int | None,
) -> dict[str, Any]:
    """Build the org-limits payload attached to raw_output."""
    fanout = len(plan_outline.get("candidates", ())) + 1
    payload: dict[str, Any] = {
        "fanout": fanout,
        "prompt_multiplier": prompt_multiplier or fanout,
    }
    if prompt_limit is not None:
        payload["prompt_tokens"] = prompt_limit
    if completion_limit is not None:
        payload["completion_tokens"] = completion_limit
    return payload


def _decorate_raw_output(
    raw_output: object,
    *,
    plan_outline: Mapping[str, Any],
    canonical_model: str,
    run_id: str,
    limits_payload: dict[str, Any] | None = None,
    plan_debug: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
    """Stamp shared metadata into raw_output and return a mutable dict."""
    if isinstance(raw_output, Mapping):
        payload = dict(raw_output)
    else:
        payload = {}
    payload.setdefault("plan", plan_outline)
    payload.setdefault("requested_model", canonical_model)
    if plan_debug is not None:
        payload.setdefault("plan_debug", plan_debug)
    if limits_payload is not None:
        payload.setdefault("org_limits", limits_payload)
    payload.setdefault("adapter_telemetry_run_id", run_id)
    return payload


def _run_local_eb1_stream(
    plan: Eb1Plan,
    prompt_text: str,
    *,
    system: str | None,
    shared_options: Mapping[str, Any] | None = None,
    request_options: Mapping[str, Any] | None = None,
    responses_request: ResponsesRequest | None = None,
    context_messages: Sequence[Mapping[str, str]] | None = None,
    requested_model: str | None = None,
    org_limits: Mapping[str, Any] | None = None,
    enable_judge_stream: bool = False,
    compiled_plan: ExecutablePlan | None = None,
    api_key_override: str | None = None,
) -> Generator[str, None, ChatResponse] | None:
    canonical_model = _canonicalize_eb1_model(requested_model)
    if _is_eb1_a_model(canonical_model):
        return None
    if not _is_codex_model(canonical_model):
        return None

    prompt_limit, completion_limit, prompt_multiplier = _extract_limits(org_limits)
    effective_request_options = _apply_completion_limit(
        request_options,
        completion_limit=completion_limit,
        responses_request=responses_request,
    )

    if responses_request is None:
        return None

    compiled = (
        compiled_plan
        if compiled_plan is not None
        else _pa('compile_plan')(
            plan,
            responses_request,
            has_credentials=_pa('_provider_has_credentials'),
        )
    )
    effective_plan = compiled.to_plan()
    if not _is_codex_plan(effective_plan):
        return None

    orchestrator = _pa('_codex_orchestrator')(canonical_model, api_key_override=api_key_override)
    stream = orchestrator.stream(
        request=responses_request,
        prompt_fallback=prompt_text,
        system_prompt=system,
        plan=compiled,
        shared_options=shared_options,
        request_options=effective_request_options,
        enable_judge_stream=enable_judge_stream,
    )

    plan_outline = _plan_outline(effective_plan)
    plan_debug = _plan_debug(compiled)
    limits_payload = _build_limits_payload(
        plan_outline,
        prompt_limit=prompt_limit,
        completion_limit=completion_limit,
        prompt_multiplier=prompt_multiplier,
    )

    telemetry, run_id = _adapter_begin_run(
        plan_outline,
        canonical_model,
        mode="stream",
        org_limits=org_limits,
    )
    start_time = time.perf_counter()

    def _iterator() -> Generator[str, None, ChatResponse]:
        iterator = iter(stream)
        while True:
            try:
                chunk = next(iterator)
            except StopIteration as stop:
                response = stop.value
                response.raw_output = _decorate_raw_output(
                    response.raw_output,
                    plan_outline=plan_outline,
                    canonical_model=canonical_model,
                    run_id=run_id,
                    limits_payload=limits_payload,
                    plan_debug=plan_debug,
                )
                _record_telemetry_outcome(
                    telemetry, run_id, "codex_stream", start_time, response,
                )
                return response
            except Exception as exc:
                _adapter_record_error(
                    telemetry,
                    run_id,
                    "codex_stream",
                    canonical_model,
                    _telemetry_error_payload(exc),
                    start_time,
                )
                raise
            else:
                yield chunk

    return _iterator()


def _resolve_plan(
    compiled: ExecutablePlan | None,
    plan: Eb1Plan,
) -> Eb1Plan:
    """Return the compiled plan if available, otherwise the raw plan."""
    return compiled.to_plan() if compiled is not None else plan


@contextmanager
def _telemetry_span(
    telemetry: Any,
    run_id: str,
    path_label: str,
    canonical_model: str,
    start_time: float,
) -> Generator[None, None, None]:
    """Wrap a call with telemetry error recording on failure."""
    try:
        yield
    except Exception as exc:
        _adapter_record_error(
            telemetry, run_id, path_label, canonical_model,
            _telemetry_error_payload(exc), start_time,
        )
        raise


def _run_local_eb1(
    plan: Eb1Plan,
    prompt_text: str,
    *,
    system: str | None,
    shared_options: Mapping[str, Any] | None = None,
    request_options: Mapping[str, Any] | None = None,
    responses_request: ResponsesRequest | None = None,
    context_messages: Sequence[Mapping[str, str]] | None = None,
    requested_model: str | None = None,
    org_limits: Mapping[str, Any] | None = None,
    compiled_plan: ExecutablePlan | None = None,
    api_key_override: str | None = None,
) -> ChatResponse:
    canonical_model = _canonicalize_eb1_model(requested_model)

    compiled = compiled_plan
    if compiled is None and responses_request is not None:
        compiled = _pa('compile_plan')(
            plan,
            responses_request,
            has_credentials=_pa('_provider_has_credentials'),
        )
    effective_plan = (
        compiled.to_plan() if compiled is not None
        else _pa('_filter_plan_for_local')(plan)
    )
    plan_outline = _plan_outline(effective_plan)
    plan_debug = _plan_debug(compiled) if compiled is not None else None

    prompt_limit, completion_limit, prompt_multiplier = _extract_limits(org_limits)
    effective_request_options = _apply_completion_limit(
        request_options,
        completion_limit=completion_limit,
        responses_request=responses_request,
    )

    telemetry, run_id = _adapter_begin_run(
        plan_outline,
        canonical_model,
        mode="stream" if responses_request is not None else "sync",
        org_limits=org_limits,
    )
    start_time = time.perf_counter()

    if _is_eb1_a_model(canonical_model):
        with _telemetry_span(telemetry, run_id, "eb1_a", canonical_model, start_time):
            response = _pa('run_eb1_a')(
                prompt_text,
                system=system,
                metadata={"requested_model": canonical_model, "plan": plan_outline},
                shared_options=shared_options,
                request_options=effective_request_options,
                responses_request=responses_request,
                context_messages=context_messages,
            )
        limits_payload = _build_limits_payload(
            plan_outline,
            prompt_limit=prompt_limit,
            completion_limit=completion_limit,
            prompt_multiplier=prompt_multiplier,
        )
        raw_payload = _decorate_raw_output(
            response.raw_output,
            plan_outline=plan_outline,
            canonical_model=canonical_model,
            run_id=run_id,
            limits_payload=limits_payload,
        )
        # eb1-a: also stamp plan/model into responses_payload.metadata
        responses_payload = raw_payload.get("responses_payload")
        if isinstance(responses_payload, MutableMapping):
            metadata_payload = responses_payload.setdefault("metadata", {})
            if isinstance(metadata_payload, MutableMapping):
                metadata_payload.setdefault("plan", plan_outline)
                metadata_payload.setdefault("requested_model", canonical_model)
        response.raw_output = raw_payload
        _record_telemetry_outcome(telemetry, run_id, "eb1_a", start_time, response)
        return response

    is_codex_model = _is_codex_model(canonical_model)
    if is_codex_model and compiled is not None:
        if _is_codex_plan(effective_plan):
            orchestrator = _pa('_codex_orchestrator')(canonical_model, api_key_override=api_key_override)
            with _telemetry_span(telemetry, run_id, "codex", canonical_model, start_time):
                result = orchestrator.run(
                    request=responses_request,
                    prompt_fallback=prompt_text,
                    system_prompt=system,
                    plan=compiled,
                    shared_options=shared_options,
                    request_options=effective_request_options,
                )
            result.raw_output = _decorate_raw_output(
                result.raw_output,
                plan_outline=plan_outline,
                canonical_model=canonical_model,
                run_id=run_id,
                plan_debug=plan_debug,
            )
            _record_telemetry_outcome(telemetry, run_id, "codex", start_time, result)
            return result

    with _telemetry_span(telemetry, run_id, "plan", canonical_model, start_time):
        response = _pa('run_plan')(
            effective_plan,
            prompt_text,
            system=system,
            shared_options=shared_options,
            request_options=effective_request_options,
            responses_request=responses_request,
            context_messages=context_messages,
        )

    raw_output = response.raw_output
    if not isinstance(raw_output, Mapping):
        raise TypeError("EB1 local execution expected mapping raw_output")

    # Synthesis metadata (synthesis_model, primary_source_model, etc.) stays in
    # raw_output for admin-only access via debug payloads. We do NOT merge it into
    # responses_payload.metadata, which is visible to all users. This ensures EB1
    # responses look identical to regular model responses (GPT-5, Claude, etc.).
    ensure_responses_output(
        response,
        default_model="takumi/eb1",
        metadata=None,
    )

    raw_payload = response.raw_output
    if not isinstance(raw_payload, dict):
        raise TypeError("EB1 local execution produced non-dict raw_output")
    raw_payload.setdefault("local_backend", True)
    filtered_outline = _plan_outline(effective_plan)
    limits_payload = _build_limits_payload(
        filtered_outline,
        prompt_limit=prompt_limit,
        completion_limit=completion_limit,
        prompt_multiplier=prompt_multiplier,
    )
    response.raw_output = _decorate_raw_output(
        raw_payload,
        plan_outline=filtered_outline,
        canonical_model=canonical_model,
        run_id=run_id,
        limits_payload=limits_payload,
        plan_debug=plan_debug,
    )
    _record_telemetry_outcome(telemetry, run_id, "plan", start_time, response)
    return response


def _normalize_usage_mapping(
    response: ChatResponse,
    existing: Mapping[str, Any] | None,
) -> dict[str, Any]:
    """Return a usage mapping containing all required token counters."""

    base_usage = response.usage

    def _extract_int(mapping: Mapping[str, Any] | None, *keys: str) -> int | None:
        if mapping is None:
            return None
        for key in keys:
            if key in mapping:
                value = mapping[key]
                if value is None:
                    return None
                try:
                    return int(value)
                except (TypeError, ValueError) as exc:
                    raise ProviderAPIError(
                        f"Invalid token count for '{key}' in Responses payload"
                    ) from exc
        return None

    prompt_tokens = _extract_int(existing, "prompt_tokens", "input_tokens")
    completion_tokens = _extract_int(existing, "completion_tokens", "output_tokens")
    total_tokens = _extract_int(existing, "total_tokens")

    if prompt_tokens is None:
        if base_usage is None or base_usage.prompt_tokens is None:
            raise ProviderAPIError("Responses usage payload missing prompt token count")
        prompt_tokens = int(base_usage.prompt_tokens)

    if completion_tokens is None:
        if base_usage is None or base_usage.completion_tokens is None:
            raise ProviderAPIError("Responses usage payload missing completion token count")
        completion_tokens = int(base_usage.completion_tokens)

    if total_tokens is None:
        if base_usage is not None and base_usage.total_tokens is not None:
            total_tokens = int(base_usage.total_tokens)
        else:
            total_tokens = prompt_tokens + completion_tokens

    return {
        "prompt_tokens": prompt_tokens,
        "completion_tokens": completion_tokens,
        "input_tokens": prompt_tokens,
        "output_tokens": completion_tokens,
        "total_tokens": total_tokens,
    }


def _extract_reasoning_summary_text(payload: Mapping[str, Any]) -> str | None:
    """Return the reasoning summary text from a Responses payload, if available."""

    summary_value = payload.get("reasoning_summary")
    if isinstance(summary_value, str):
        summary = summary_value.strip()
        if summary:
            return summary

    output_value = payload.get("output")
    if isinstance(output_value, Sequence):
        for item in output_value:
            if not isinstance(item, Mapping):
                continue
            content = item.get("content")
            if not isinstance(content, Sequence):
                continue
            for part in content:
                if not isinstance(part, Mapping):
                    continue
                if part.get("type") != "reasoning_summary":
                    continue
                text_val = part.get("text")
                if isinstance(text_val, str):
                    summary = text_val.strip()
                    if summary:
                        return summary
    return None


def _build_sync_final_payload(response: ChatResponse) -> dict[str, Any]:
    """Build a Responses-shaped payload from a synchronous ChatResponse."""
    raw_payload = response.raw_output if isinstance(response.raw_output, Mapping) else {}
    responses_payload = raw_payload.get("responses_payload")
    if isinstance(responses_payload, Mapping):
        final_payload = dict(responses_payload)
        raw_usage = final_payload.get("usage")
        usage_field = raw_usage if isinstance(raw_usage, Mapping) else None
        final_payload["usage"] = _normalize_usage_mapping(response, usage_field)
        plan_debug = raw_payload.get("plan_debug")
        if isinstance(plan_debug, Mapping):
            final_payload.setdefault("plan_debug", dict(plan_debug))
    else:
        final_payload = {
            "id": response.response_id or f"resp_local_{uuid.uuid4().hex}",
            "model": response.model_id or "takumi/eb1",
            "status": "completed",
            "object": "response",
            "output": response.output or [],
            "output_text": response.text,
            "usage": _normalize_usage_mapping(response, None),
        }
        plan_debug = raw_payload.get("plan_debug")
        if isinstance(plan_debug, Mapping):
            final_payload["plan_debug"] = dict(plan_debug)
    return final_payload


def _build_sync_stream_events(
    response: ChatResponse,
    final_payload: dict[str, Any],
) -> list[str]:
    """Build the list of SSE-like JSON event strings for a sync-to-stream path."""
    events: list[str] = [
        json.dumps({"type": "response.created", "response": final_payload}),
    ]

    summary_text = _extract_reasoning_summary_text(final_payload)
    if summary_text:
        events.append(
            json.dumps({"type": "response.reasoning_summary_text.delta", "delta": summary_text})
        )
        events.append(json.dumps({"type": "response.reasoning_summary_text.done"}))

    delta_text = (final_payload.get("output_text") or response.data or "") or ""
    if delta_text:
        events.append(json.dumps({"type": "response.output_text.delta", "delta": delta_text}))
    events.append(json.dumps({"type": "response.output_text.done"}))
    events.append(json.dumps({"type": "response.completed", "response": final_payload}))
    return events


def _build_local_request_options(
    params: Mapping[str, Any],
    response_format: Mapping[str, Any] | None,
) -> Mapping[str, Any] | None:
    """Return request options for local EB1 execution, including response_format."""
    _EXCLUDED = {"context", "system", "stream"}
    allowed = {k: v for k, v in params.items() if k not in _EXCLUDED} if params else {}
    if response_format is not None:
        allowed["response_format"] = dict(response_format)
    return allowed or None


def _build_local_shared_options(
    params: MutableMapping[str, Any],
    responses_request: ResponsesRequest | None,
) -> Mapping[str, Any] | None:
    """Merge shared options derived from params and an optional Responses request."""
    merged: dict[str, Any] = {}

    # Extract reasoning from Responses request (primary source).
    if responses_request is not None and responses_request.reasoning is not None:
        merged["reasoning"] = dict(responses_request.reasoning)

    # Extract reasoning from params (secondary, wins on conflict via update).
    if "reasoning" in params:
        value = params.pop("reasoning")
        if not isinstance(value, Mapping):
            raise ValueError("EB1 reasoning parameters must be a mapping")
        merged["reasoning"] = dict(value)

    return merged or None


def _prompt_from_responses_input(
    input_value: Sequence[ResponsesMessage] | str | None,
) -> str | None:
    if input_value is None:
        return None
    if isinstance(input_value, str):
        candidate = input_value.strip()
        return candidate or None
    for message in reversed(input_value):
        text = _responses_message_text(message)
        if text:
            return text
    return None


def _system_from_context(
    context_messages: Sequence[Mapping[str, str]] | None,
) -> str | None:
    if not context_messages:
        return None
    for entry in context_messages:
        if entry["role"].strip().lower() != "system":
            continue
        candidate = entry["content"].strip()
        if candidate:
            return candidate
    return None


def _responses_message_text(message: ResponsesMessage) -> str | None:
    fragments: list[str] = []
    for part in message.content:
        value = part.fields.get("text")
        if isinstance(value, str):
            fragments.append(value)
    combined = "".join(fragments).strip()
    return combined or None


def _context_from_responses_messages(
    messages: Sequence[ResponsesMessage] | None,
) -> tuple[dict[str, str], ...]:
    if not messages:
        return tuple()
    return tuple(
        {"role": message.role, "content": text}
        for message in messages
        if (text := _responses_message_text(message))
    )


def _takumi_base_url() -> str | None:
    return get_takumi_settings().base_url


def _coerce_context_entries(context: ContextPayload) -> tuple[dict[str, str], ...]:
    if context is None:
        return tuple()
    if isinstance(context, str):
        return ({"role": "system", "content": context.strip()},)
    if isinstance(context, Mapping):
        return (_validate_context_entry(context),)
    if isinstance(context, Sequence):
        return tuple(_validate_context_entry(item) for item in context)
    raise TypeError("EB1 context must be a string or a sequence of message mappings")


def _validate_context_entry(entry: Mapping[str, str]) -> dict[str, str]:
    if "role" not in entry:
        raise ValueError("EB1 context entries require 'role'")
    role = entry["role"]
    if not isinstance(role, str) or not role.strip():
        raise ValueError("EB1 context role must be non-empty text")
    # Allow empty/None content for assistant messages in tool-use
    # conversations where content is in tool_calls, not text.
    content = entry.get("content")
    if content is None or not isinstance(content, str):
        content = ""
    return {"role": role.strip(), "content": content.strip()}


def _compose_prompt_for_eb1(prompt: str, context: Sequence[Mapping[str, str]] | None) -> str:
    entries = tuple(dict(entry) for entry in (context or ()))
    if not entries:
        return prompt

    def _role_label(entry: Mapping[str, str]) -> str:
        role = entry["role"].strip()
        return role[0].upper() + role[1:] if role else "User"

    segments = [f"{_role_label(e)}: {e['content'].strip()}" for e in entries]
    prompt_text = prompt.strip()
    if prompt_text:
        segments.append(f"User: {prompt_text}")
    return "\n\n".join(segments)




def _extract_reasoning_mode(
    params: MutableMapping[str, Any],
) -> tuple[Mapping[str, Any] | None, str | None]:
    """Return a normalized reasoning payload and requested effort, if supplied."""

    raw = params.get("reasoning")
    if raw is None:
        effort_override = params.get("reasoning_effort")
        if isinstance(effort_override, str):
            cleaned = effort_override.strip().lower()
            if cleaned:
                return {"effort": cleaned}, cleaned
        return None, None

    # Accept simple effort strings, nested {"reasoning": {...}}, or mapping payloads.
    if isinstance(raw, str):
        cleaned = raw.strip().lower()
        if not cleaned:
            return None, None
        return {"effort": cleaned}, cleaned

    if isinstance(raw, Mapping):
        payload: Mapping[str, Any] | None
        if "reasoning" in raw and isinstance(raw["reasoning"], Mapping):
            payload = dict(raw["reasoning"])
        else:
            payload = dict(raw)

        effort_value = payload.get("effort")
        cleaned_effort = effort_value.strip().lower() if isinstance(effort_value, str) else None
        if cleaned_effort:
            # Persist normalized effort for downstream consumers.
            payload = dict(payload)
            payload["effort"] = cleaned_effort
        return payload, cleaned_effort

    return None, None



