"""Shared fallback messages for empty/failed responses.

All layers (ReActEngine, Portal, Chat) should use these constants
to ensure consistent user-facing messages.

G7/U2: Also hosts ``EmergencyError`` and ``EmergencyRules`` for the
three-tier fallback chain's Emergency layer (rule-based classifier,
no LLM). See ``docs/plans/2026-06-29-003-feat-agent-wave2-medium-coupling-plan.md``.
"""

from dataclasses import dataclass

from agentkit.core.exceptions import (
    LLMProviderError,
    LoopDetectedError,
    TaskCancelledError,
    TaskTimeoutError,
)

# When LLM returns empty content after all fallback models exhausted
EMPTY_LLM_RESPONSE = (
    "模型未返回有效内容，已尝试备用模型仍未成功。"
    "可能原因：1) 服务暂时过载；2) 请求内容超出模型处理范围。"
    "建议：1) 稍后重试；2) 简化或缩短输入内容。"
)

# When ReActEngine reaches max steps without a complete answer
MAX_STEPS_REACHED = "已达到最大推理步数，但仍未得到完整结论。建议：1) 简化问题后重试；2) 将任务拆分为更小的步骤。"

# When a shell command succeeds but produces no output
SHELL_NO_OUTPUT = "[命令执行成功，无输出内容]"


# ── G7/U2: Emergency layer ──────────────────────────────────────


@dataclass
class EmergencyError:
    """Structured error produced by the Emergency layer (rule-classified).

    Carries a stable ``error_code`` for programmatic dispatch (frontend
    retry UI, telemetry), a human-readable ``message`` mirroring
    ``EMPTY_LLM_RESPONSE`` style, actionable ``suggestions``, and the
    original exception string for traceability.

    The ``retryable`` flag distinguishes recoverable user errors
    (timeout, loop, LLM hiccup) from internal bugs (retryable=False).
    """

    error_code: str  # "timeout" | "loop_detected" | "llm_failure" | "internal_error"
    message: str  # human-readable Chinese message
    suggestions: list[str]  # actionable user-facing suggestions
    retryable: bool  # whether a user retry might succeed
    original_error: str  # str(exc) for traceability

    def to_dict(self) -> dict[str, object]:
        return {
            "error_code": self.error_code,
            "message": self.message,
            "suggestions": list(self.suggestions),
            "retryable": self.retryable,
            "original_error": self.original_error,
        }

    def to_error_message(self) -> str:
        """Format as a single human-readable string with suggestions.

        Mirrors ``EMPTY_LLM_RESPONSE`` style: ``<message>建议：1) ... 2) ...``
        """
        if not self.suggestions:
            return self.message
        suggestion_str = "".join(f"{i}) {s}；" for i, s in enumerate(self.suggestions, 1))
        # Strip trailing "；" and prefix with "建议："
        suggestion_str = suggestion_str.rstrip("；")
        return f"{self.message}建议：{suggestion_str}。"


# Default rule set: (exception_type, error_code, message, suggestions, retryable)
# ponytail: ceiling — rule-based, no LLM. Adding a new rule = append a tuple.
# Upgrade path: LLM-driven suggestions would require a separate classifier
# class (out of scope per brainstorm KTD4).
_DEFAULT_RULES: list[tuple[type[Exception], str, str, list[str], bool]] = [
    (
        TaskTimeoutError,
        "timeout",
        "任务执行超时。",
        ["稍后重试", "简化任务范围"],
        True,
    ),
    (
        LoopDetectedError,
        "loop_detected",
        "检测到推理循环。",
        ["拆分任务", "检查工具参数"],
        True,
    ),
    (
        LLMProviderError,
        "llm_failure",
        "LLM 服务调用失败。",
        ["稍后重试", "切换模型"],
        True,
    ),
]

_DEFAULT_ERROR_CODE = "internal_error"
_DEFAULT_MESSAGE = "Agent 执行内部错误。"
_DEFAULT_SUGGESTIONS: list[str] = ["联系管理员"]
_DEFAULT_RETRYABLE = False


class EmergencyRules:
    """Rule-based classifier for the Emergency layer.

    Maps exception types to ``EmergencyError`` instances. No LLM, no I/O —
    pure function of ``(exception, config)``.

    Caller responsibility: ``TaskCancelledError`` MUST propagate as-is
    (per KTD3); the caller checks ``isinstance(exc, TaskCancelledError)``
    before invoking :meth:`classify`. Calling :meth:`classify` with a
    ``TaskCancelledError`` raises ``ValueError`` to surface the bug.

    Config override shape (optional ``config`` arg):

    ```python
    {
        "timeout": {"suggestions": ["自定义建议"], "retryable": False},
        "llm_failure": {"message": "自定义消息"},
    }
    ```
    """

    @staticmethod
    def classify(exc: Exception, config: dict | None = None) -> EmergencyError:
        if isinstance(exc, TaskCancelledError):
            # Contract: caller must check before invoking classify.
            raise ValueError(
                "TaskCancelledError must propagate as-is; caller must check "
                "before invoking EmergencyRules.classify"
            )

        config = config or {}

        for exc_type, code, message, suggestions, retryable in _DEFAULT_RULES:
            if isinstance(exc, exc_type):
                override = config.get(code, {}) if isinstance(config, dict) else {}
                return EmergencyError(
                    error_code=code,
                    message=override.get("message", message),
                    suggestions=override.get("suggestions", list(suggestions)),
                    retryable=override.get("retryable", retryable),
                    original_error=str(exc),
                )

        # Generic fallback
        override = config.get(_DEFAULT_ERROR_CODE, {}) if isinstance(config, dict) else {}
        return EmergencyError(
            error_code=_DEFAULT_ERROR_CODE,
            message=override.get("message", _DEFAULT_MESSAGE),
            suggestions=override.get("suggestions", list(_DEFAULT_SUGGESTIONS)),
            retryable=override.get("retryable", _DEFAULT_RETRYABLE),
            original_error=str(exc),
        )
