"""StructuredMW: D14 五级阶梯的编排层(遥测→缓存→**结构化**→重试)。

重问 = 再次调用 call_next(内层重试循环)——天然照过限流/熔断门并逐次
遥测;`ResultInvalidError` 不会进入重试循环的失败计数(RetryMW 对其记
成功后上抛,由本层决定是否带反馈重问)。缓存在本层之外,只固化阶梯
通过的最终响应。
"""

from __future__ import annotations

import dataclasses
from typing import TYPE_CHECKING

from polygateway.errors import ResultInvalidError

if TYPE_CHECKING:
    from polygateway.ports import CallNext, StructuredOutputStrategy
    from polygateway.types import ChatRequest, LLMResponse

# 反馈模板: 库内常量,英文、零业务词(设计 §5 细则 1)
_FEEDBACK_TEMPLATE = (
    "Your previous reply was not valid JSON matching the required schema. "
    "Errors: {errors}. Reply with ONLY the corrected JSON object."
)
_MAX_FEEDBACK_ERRORS = 3
_MAX_ERROR_CHARS = 200


def _format_errors(errors: list[str]) -> str:
    clipped = [e[:_MAX_ERROR_CHARS] for e in errors[:_MAX_FEEDBACK_ERRORS]]
    return "; ".join(clipped) if clipped else "output could not be parsed"


class StructuredMW:
    """三档分派: 不传直通 / "json" 仅修复 / pydantic 模型走完整阶梯。"""

    def __init__(
        self,
        *,
        strategy: StructuredOutputStrategy,
        max_retries: int = 1,
        escalation: StructuredOutputStrategy | None = None,
    ) -> None:
        if max_retries < 0:
            raise ValueError("max_structured_retries 不能为负(0 = 不重问转人工)")
        self._strategy = strategy
        self._max_retries = max_retries
        self._escalation = escalation

    async def __call__(self, request: ChatRequest, call_next: CallNext) -> LLMResponse:
        if request.structured is None:
            return await call_next(request)
        if request.structured == "json":
            response = await call_next(self._shape(request, self._strategy, schema=None))
            return dataclasses.replace(
                response, structured_data=self._strategy.parse(response.content)
            )
        return await self._run_ladder(request, call_next)

    async def _run_ladder(self, request: ChatRequest, call_next: CallNext) -> LLMResponse:
        model_cls = request.structured
        schema = model_cls.model_json_schema()
        current = self._shape(request, self._strategy, schema=schema)
        response = await call_next(current)
        reasks = 0
        while True:
            errors: list[str] = []
            repair_error: str | None = None
            try:
                parsed = self._strategy.parse(response.content)
            except ResultInvalidError as exc:
                repair_error = exc.repair_error
                errors.append(exc.repair_error or "not parseable as JSON")
            else:
                try:
                    validated = model_cls.model_validate(parsed)
                except ValueError as exc:  # pydantic ValidationError 继承 ValueError
                    errors.append(str(exc))
                else:
                    return dataclasses.replace(response, structured_data=validated)
            if reasks >= self._max_retries:
                raise ResultInvalidError(
                    "结构化输出阶梯耗尽",
                    raw_text=response.content,
                    repair_error=repair_error,
                    validation_errors=tuple(errors),
                )
            reasks += 1
            current = self._with_feedback(current, response.content, errors, schema)
            response = await call_next(current)

    def _shape(
        self, request: ChatRequest, strategy: StructuredOutputStrategy, *, schema: dict | None
    ) -> ChatRequest:
        overlay = strategy.request_overlay(schema)
        if not overlay:
            return request
        return dataclasses.replace(request, overlay={**request.overlay, **overlay})

    def _with_feedback(
        self, current: ChatRequest, bad_content: str, errors: list[str], schema: dict | None
    ) -> ChatRequest:
        """构造带反馈的重问请求;可升级到原生 schema 策略(设计 §5 细则 2)。"""
        messages = [
            *current.messages,
            {"role": "assistant", "content": bad_content},
            {"role": "user", "content": _FEEDBACK_TEMPLATE.format(errors=_format_errors(errors))},
        ]
        reask = dataclasses.replace(current, messages=messages)
        if self._escalation is not None:
            reask = self._shape(reask, self._escalation, schema=schema)
        return reask
