"""G7/U3 — Three-tier fallback chain (main → Recovery → Emergency).

Wired at chat.py REST send_message endpoint. Composes U2's EmergencyRules
with existing ReflexionEngine for the Recovery layer.

Scope (KTD5): Only the chat REST path is wrapped. CLI / ReWOO / Reflexion
internal ReAct calls are NOT wrapped (would create recursive loop).
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from typing import Any

from agentkit.core.exceptions import (
    LLMProviderError,
    LoopDetectedError,
    TaskCancelledError,
    TaskTimeoutError,
)
from agentkit.core.fallback import EmergencyError, EmergencyRules
from agentkit.core.react import ReActEngine, ReActResult
from agentkit.core.reflexion import ReflexionEngine
from agentkit.llm.gateway import LLMGateway

logger = logging.getLogger(__name__)


# ReActResult.status values that indicate soft failure → trigger Recovery.
# "success" is the only clean-pass; everything else is fallback-worthy.
_SOFT_FAILURE_STATUSES = frozenset({"empty_fallback", "verify_failed", "timeout"})


@dataclass
class ChatExecutionResult:
    """Wrapper produced by execute_with_fallback_chain.

    Carries a ReActResult-like ``output`` field plus an optional
    ``error_struct`` (set only when Emergency tier fires). The chat
    handler reads ``.output`` for the assistant reply and ``.error_struct``
    for the optional structured error payload.
    """

    output: str
    status: str  # "success" | "recovered" | "emergency"
    error_struct: dict[str, Any] | None = None
    trajectory: list[Any] = field(default_factory=list)
    total_steps: int = 0
    total_tokens: int = 0
    fallback_strategy: str | None = None


def _react_to_chat_result(react: ReActResult) -> ChatExecutionResult:
    return ChatExecutionResult(
        output=react.output,
        status="success",
        trajectory=react.trajectory,
        total_steps=react.total_steps,
        total_tokens=react.total_tokens,
        fallback_strategy=react.fallback_strategy,
    )


def _reflexion_to_chat_result(reflexion_result: Any) -> ChatExecutionResult:
    """Best-effort conversion from ReflexionResult to ChatExecutionResult."""
    output = getattr(reflexion_result, "output", None) or getattr(
        reflexion_result, "final_answer", ""
    )
    return ChatExecutionResult(
        output=output or "",
        status="recovered",
        trajectory=getattr(reflexion_result, "trajectory", []) or [],
        total_steps=getattr(reflexion_result, "total_steps", 0),
        total_tokens=getattr(reflexion_result, "total_tokens", 0),
        fallback_strategy="reflexion_recovery",
    )


def _to_emergency(exc: Exception, config: dict | None) -> ChatExecutionResult:
    emergency: EmergencyError = EmergencyRules.classify(exc, config)
    return ChatExecutionResult(
        output=emergency.to_error_message(),
        status="emergency",
        error_struct=emergency.to_dict(),
        fallback_strategy="emergency",
    )


async def execute_with_fallback_chain(
    *,
    react_engine: ReActEngine,
    llm_gateway: LLMGateway,
    messages: list[dict[str, str]],
    tools: list[Any] | None,
    model: str,
    agent_name: str,
    system_prompt: str | None,
    fallback_chain_config: dict | None = None,
) -> ChatExecutionResult:
    """Three-tier fallback chain: Main → Recovery (ReflexionEngine) → Emergency.

    KTD5: only this entry point wraps the chain. ReflexionEngine's internal
    ReAct call bypasses the chain (no recursive loop possible).

    Returns ChatExecutionResult with status:
    - "success": main agent succeeded
    - "recovered": main failed, ReflexionEngine recovery succeeded
    - "emergency": main failed, recovery failed/exhausted, Emergency layer fired
    """
    config = fallback_chain_config or {}
    recovery_cfg = config.get("recovery", {}) if isinstance(config, dict) else {}
    emergency_cfg = config.get("emergency", {}) if isinstance(config, dict) else {}
    recovery_enabled = recovery_cfg.get("enabled", True) if isinstance(recovery_cfg, dict) else True
    emergency_enabled = (
        emergency_cfg.get("enabled", True) if isinstance(emergency_cfg, dict) else True
    )
    max_reflections = recovery_cfg.get("max_retries", 1) if isinstance(recovery_cfg, dict) else 1

    # ── Tier 1: Main ──────────────────────────────────────────────
    main_exc: Exception | None = None
    try:
        result = await react_engine.execute(
            messages=messages,
            tools=tools,
            model=model,
            agent_name=agent_name,
            system_prompt=system_prompt,
        )
        if result.status == "success":
            return _react_to_chat_result(result)
        # Soft failure (empty_fallback / verify_failed / timeout) → trigger Recovery
        if result.status in _SOFT_FAILURE_STATUSES:
            main_exc = AgentSoftFailureError(
                f"main agent status={result.status}: {result.output[:200]}"
            )
        else:
            # Unknown status — treat as success-like (don't trigger recovery)
            return _react_to_chat_result(result)
    except TaskCancelledError:
        # KTD3: TaskCancelledError propagates as-is, NOT routed to Emergency.
        raise
    except (TaskTimeoutError, LoopDetectedError, LLMProviderError) as exc:
        main_exc = exc
    except Exception as exc:  # noqa: BLE001 - last-resort catch for Emergency routing
        main_exc = exc

    # ── Tier 2: Recovery (ReflexionEngine) ────────────────────────
    if recovery_enabled and main_exc is not None:
        try:
            reflexion = ReflexionEngine(
                llm_gateway=llm_gateway,
                max_reflections=max_reflections,
            )
            recovery_result = await reflexion.execute(
                messages=messages,
                tools=tools,
                model=model,
                agent_name=agent_name,
                system_prompt=system_prompt,
            )
            # Recovery succeeds if Reflexion reports success or produces output.
            recovery_status = getattr(recovery_result, "status", "")
            if recovery_status == "success" or getattr(recovery_result, "output", None):
                return _reflexion_to_chat_result(recovery_result)
            logger.warning(
                f"Recovery layer did not succeed (status={recovery_status}), "
                f"falling through to Emergency"
            )
        except TaskCancelledError:
            raise
        except Exception as recovery_exc:  # noqa: BLE001
            logger.warning(f"Recovery layer raised: {recovery_exc}; falling through to Emergency")

    # ── Tier 3: Emergency ─────────────────────────────────────────
    if not emergency_enabled:
        # Re-raise original exception if Emergency disabled.
        if main_exc is not None:
            raise main_exc
        # No exception but no success either — synthesise an emergency-style result.
        return ChatExecutionResult(
            output="Agent 未返回有效结果且 Emergency 层已禁用。",
            status="emergency",
            fallback_strategy="emergency_disabled",
        )

    # main_exc may be None if main returned soft-failure status without raising.
    # Synthesize a generic exception for Emergency classification.
    exc_for_emergency = main_exc or AgentSoftFailureError("soft failure without exception")
    return _to_emergency(exc_for_emergency, config)


class AgentSoftFailureError(Exception):
    """Internal marker — main agent returned a soft-failure status without raising.

    Used to feed the Emergency classifier when main status was e.g.
    ``empty_fallback`` (no exception raised, but result not usable).
    Classified as ``internal_error`` by EmergencyRules (generic fallback).
    """
