"""Error handling validation rules.

These rules require calling tools with known-bad input and inspecting the
error responses.  They accept an optional ``simulator`` keyword argument;
when running against static tool definitions only (no live server), they
produce informational skip results.
"""

from __future__ import annotations

import re
from typing import Any

from hatchdx.validator.models import ValidationResult, ValidationRule
from hatchdx.validator.registry import register_rule

# Patterns that indicate internal errors leaking to the client.
_INTERNAL_ERROR_PATTERNS = [
    re.compile(r"Traceback \(most recent call last\)", re.IGNORECASE),
    re.compile(r"File \"[^\"]+\", line \d+", re.IGNORECASE),
    re.compile(r"at .+\.py:\d+"),
    re.compile(r"at .+\.js:\d+"),
    re.compile(r"at .+\.ts:\d+"),
    re.compile(r"/home/[^\s]+"),
    re.compile(r"/usr/[^\s]+/[^\s]+\.py"),
    re.compile(r"[A-Z]:\\\\[^\s]+"),
    re.compile(r"node_modules/"),
    re.compile(r"NullPointerException"),
    re.compile(r"ENOENT|EACCES|EPERM"),
    re.compile(r"panic:"),
]


def _extract_text(content: list[dict[str, Any]]) -> str:
    """Concatenate all text content items into a single string."""
    parts = []
    for item in content:
        if isinstance(item, dict) and item.get("type") == "text":
            parts.append(item.get("text", ""))
    return "\n".join(parts)


def _has_internal_patterns(text: str) -> str | None:
    """Return the matching pattern description if internal errors are detected."""
    for pattern in _INTERNAL_ERROR_PATTERNS:
        if pattern.search(text):
            return pattern.pattern
    return None


@register_rule(
    name="actionable_error_messages",
    category="errors",
    severity="warning",
    description="Tools should return actionable error messages, not empty or generic ones.",
)
def check_actionable_error_messages(
    tools: list[Any],
    *,
    error_results: dict[str, Any] | None = None,
    **kwargs: Any,
) -> list[ValidationResult]:
    """Check that error responses contain meaningful messages.

    ``error_results`` is a dict mapping tool names to ``ToolCallResult`` objects
    obtained by calling each tool with invalid input.  If not provided, the
    check is skipped with an info-level message.
    """
    rule: ValidationRule = check_actionable_error_messages._rule  # type: ignore[attr-defined]
    results: list[ValidationResult] = []

    if error_results is None:
        results.append(ValidationResult(
            rule=rule,
            passed=True,
            message="Skipped: no live server connection to test error responses.",
            tool_name=None,
        ))
        return results

    for tool_name, call_result in error_results.items():
        if not call_result.is_error:
            # The call didn't error — can't validate error quality.
            results.append(ValidationResult(
                rule=rule,
                passed=True,
                message=f"Tool '{tool_name}' did not return an error for bad input (may be lenient).",
                tool_name=tool_name,
            ))
            continue

        text = _extract_text(call_result.content)

        if not text.strip():
            results.append(ValidationResult(
                rule=rule,
                passed=False,
                message=f"Tool '{tool_name}' returned an empty error message.",
                tool_name=tool_name,
            ))
        elif len(text.strip()) < 10:
            results.append(ValidationResult(
                rule=rule,
                passed=False,
                message=(
                    f"Tool '{tool_name}' returned a very short error message: "
                    f"'{text.strip()}'. Errors should be actionable."
                ),
                tool_name=tool_name,
            ))
        else:
            results.append(ValidationResult(
                rule=rule,
                passed=True,
                message=f"Tool '{tool_name}' returned an actionable error message.",
                tool_name=tool_name,
            ))

    return results


@register_rule(
    name="no_internal_error_exposure",
    category="errors",
    severity="error",
    description="Tools must not expose internal errors (stack traces, file paths) to clients.",
)
def check_no_internal_error_exposure(
    tools: list[Any],
    *,
    error_results: dict[str, Any] | None = None,
    **kwargs: Any,
) -> list[ValidationResult]:
    """Check that error responses don't leak internal implementation details.

    ``error_results`` is a dict mapping tool names to ``ToolCallResult`` objects.
    If not provided, the check is skipped.
    """
    rule: ValidationRule = check_no_internal_error_exposure._rule  # type: ignore[attr-defined]
    results: list[ValidationResult] = []

    if error_results is None:
        results.append(ValidationResult(
            rule=rule,
            passed=True,
            message="Skipped: no live server connection to test error responses.",
            tool_name=None,
        ))
        return results

    for tool_name, call_result in error_results.items():
        if not call_result.is_error:
            results.append(ValidationResult(
                rule=rule,
                passed=True,
                message=f"Tool '{tool_name}' did not return an error.",
                tool_name=tool_name,
            ))
            continue

        text = _extract_text(call_result.content)
        matched_pattern = _has_internal_patterns(text)

        if matched_pattern is not None:
            results.append(ValidationResult(
                rule=rule,
                passed=False,
                message=(
                    f"Tool '{tool_name}' exposes internal details in error response "
                    f"(matched pattern: {matched_pattern}). "
                    f"Wrap errors to hide implementation details from clients."
                ),
                tool_name=tool_name,
            ))
        else:
            results.append(ValidationResult(
                rule=rule,
                passed=True,
                message=f"Tool '{tool_name}' error response does not expose internals.",
                tool_name=tool_name,
            ))

    return results
