"""Validation utilities for ReAct agent response guardrails.

This module provides configurable guardrails for assistant responses,
including length checks, substantive content validation, and tool error
detection in trajectories.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Literal


@dataclass
class ValidationConfig:
    """Configuration for assistant response validation.

    Attributes:
        guardrail_mode: How strictly to enforce guardrails.
            - "off": No enforcement, validation returns no warnings
            - "warn": Log warnings but don't block
            - "strict": Raise ValueError on hard violations
        max_output_chars: Maximum allowed response length
        min_substantive_chars: Minimum chars for substantive response
    """

    guardrail_mode: Literal["off", "warn", "strict"] = "off"
    max_output_chars: int = 10000
    min_substantive_chars: int = 20


def trajectory_has_tool_errors(trajectory: dict[str, Any] | None) -> bool:
    """Best-effort detector for tool error traces in trajectory payloads.

    Checks output fields in the trajectory for common error indicators.

    Args:
        trajectory: The trajectory dict from a ReAct prediction, or None

    Returns:
        True if any output field contains error-like text, False otherwise
    """
    if not trajectory or not isinstance(trajectory, dict):
        return False

    # Legacy DSPy ReAct flat keys: output_0, output_1, ...
    for key, value in trajectory.items():
        if not key.startswith("output_"):
            continue
        text = str(value).lower()
        if any(token in text for token in ("error", "exception", "traceback")):
            return True

    # Structured trajectory variants: {"steps": [...]} or {"trajectory": [...]}
    structured_steps: list[Any] = []
    maybe_steps = trajectory.get("steps")
    if isinstance(maybe_steps, list):
        structured_steps.extend(maybe_steps)
    maybe_trajectory = trajectory.get("trajectory")
    if isinstance(maybe_trajectory, list):
        structured_steps.extend(maybe_trajectory)

    for step in structured_steps:
        if not isinstance(step, dict):
            continue
        for field_name in ("output", "observation", "error"):
            field_value = step.get(field_name)
            if field_value is None:
                continue
            text = str(field_value).lower()
            if any(token in text for token in ("error", "exception", "traceback")):
                return True
    return False


def validate_assistant_response(
    *,
    assistant_response: str,
    trajectory: dict[str, Any] | None,
    config: ValidationConfig,
) -> tuple[str, list[str]]:
    """Apply configurable response guardrails.

    Validates the assistant response against configured constraints:
    - Non-empty response (hard check)
    - Response length under max_output_chars (hard check)
    - Minimum substantive content (soft warning)
    - Tool errors in trajectory (soft warning)

    Args:
        assistant_response: The response text to validate
        trajectory: The trajectory dict from a ReAct prediction
        config: Validation configuration settings

    Returns:
        Tuple of (sanitized_response, warning_messages)

    Raises:
        ValueError: In strict mode when hard guardrail violations occur
    """
    response = str(assistant_response or "").strip()
    mode = config.guardrail_mode

    hard_issues: list[str] = []
    warnings: list[str] = []

    if not response:
        hard_issues.append("empty assistant response")

    if len(response) > config.max_output_chars:
        hard_issues.append(
            f"assistant response length {len(response)} exceeds "
            f"max_output_chars={config.max_output_chars}"
        )

    if response and len(response) < config.min_substantive_chars:
        warnings.append(
            "assistant response appears brief; consider adding more substantive detail"
        )

    if trajectory_has_tool_errors(trajectory):
        warnings.append(
            "trajectory indicates at least one tool error; "
            "consider retrying or recovering before final response"
        )

    if hard_issues and mode == "strict":
        raise ValueError("guardrail violation: " + "; ".join(hard_issues))

    if hard_issues and mode == "warn":
        warnings.extend([f"guardrail warning: {issue}" for issue in hard_issues])

    return response, warnings if mode in {"warn", "strict"} else []
