"""Guardrails — input/output validation and content filtering for agents."""
from __future__ import annotations

import inspect
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Callable


@dataclass
class GuardrailResult:
    """Result of a guardrail check.

    Attributes:
        passed: Whether the content passed the check.
        message: Human-readable explanation (used in errors/logs).
        action: What to do when check fails — "block", "warn", or "override".
        override_content: Replacement content when action is "override".
    """

    passed: bool
    message: str = ""
    action: str = "block"
    override_content: str | None = None


class Guardrail(ABC):
    """Abstract base class for guardrails."""

    @abstractmethod
    async def check(self, content: str, *, role: str = "user") -> GuardrailResult:
        """Check content against this guardrail.

        Args:
            content: The text to validate.
            role: "user" for input guardrails, "assistant" for output guardrails.

        Returns:
            GuardrailResult with pass/fail status and action.
        """
        ...


class ContentFilter(Guardrail):
    """Block content containing any of the specified words or phrases.

    Args:
        blocked: List of blocked words/phrases.
        case_sensitive: Whether matching is case-sensitive.
        action: Action on failure — "block", "warn", or "override".
        override_with: Replacement content when action is "override".
    """

    def __init__(
        self,
        blocked: list[str],
        *,
        case_sensitive: bool = False,
        action: str = "block",
        override_with: str = "I can't help with that request.",
    ) -> None:
        self.blocked = blocked
        self.case_sensitive = case_sensitive
        self.action = action
        self.override_with = override_with

    async def check(self, content: str, *, role: str = "user") -> GuardrailResult:
        check_content = content if self.case_sensitive else content.lower()
        for word in self.blocked:
            check_word = word if self.case_sensitive else word.lower()
            if check_word in check_content:
                return GuardrailResult(
                    passed=False,
                    message=f"Blocked content detected: '{word}'",
                    action=self.action,
                    override_content=self.override_with if self.action == "override" else None,
                )
        return GuardrailResult(passed=True)


class RegexGuard(Guardrail):
    """Guard based on regex pattern matching.

    Args:
        deny: Patterns that cause failure if matched.
        allow: If provided, content must match at least one pattern to pass.
        action: Action on failure.
        message: Custom failure message.
    """

    def __init__(
        self,
        *,
        deny: list[str] | None = None,
        allow: list[str] | None = None,
        action: str = "block",
        message: str = "Content matched a denied pattern",
    ) -> None:
        self.deny = [re.compile(p) for p in (deny or [])]
        self.allow = [re.compile(p) for p in (allow or [])] if allow else None
        self.action = action
        self.message = message

    async def check(self, content: str, *, role: str = "user") -> GuardrailResult:
        for pattern in self.deny:
            match = pattern.search(content)
            if match:
                return GuardrailResult(
                    passed=False,
                    message=f"{self.message}: '{match.group()}'",
                    action=self.action,
                )

        if self.allow is not None:
            if not any(p.search(content) for p in self.allow):
                return GuardrailResult(
                    passed=False,
                    message="Content did not match any allowed pattern",
                    action=self.action,
                )

        return GuardrailResult(passed=True)


class MaxLengthGuard(Guardrail):
    """Reject content exceeding a character limit.

    Args:
        max_chars: Maximum allowed content length.
        action: Action on failure.
    """

    def __init__(self, max_chars: int, *, action: str = "block") -> None:
        self.max_chars = max_chars
        self.action = action

    async def check(self, content: str, *, role: str = "user") -> GuardrailResult:
        if len(content) > self.max_chars:
            return GuardrailResult(
                passed=False,
                message=f"Content length {len(content)} exceeds limit of {self.max_chars}",
                action=self.action,
            )
        return GuardrailResult(passed=True)


class KeywordGuard(Guardrail):
    """Flexible keyword matcher with configurable action.

    Args:
        keywords: List of keywords to detect.
        action: Action on detection — "block", "warn", or "override".
        message: Custom message on detection.
        case_sensitive: Whether matching is case-sensitive.
        override_with: Replacement content when action is "override".
    """

    def __init__(
        self,
        keywords: list[str],
        *,
        action: str = "warn",
        message: str = "Keyword detected",
        case_sensitive: bool = False,
        override_with: str | None = None,
    ) -> None:
        self.keywords = keywords
        self.action = action
        self.message = message
        self.case_sensitive = case_sensitive
        self.override_with = override_with

    async def check(self, content: str, *, role: str = "user") -> GuardrailResult:
        check_content = content if self.case_sensitive else content.lower()
        for keyword in self.keywords:
            check_kw = keyword if self.case_sensitive else keyword.lower()
            if check_kw in check_content:
                return GuardrailResult(
                    passed=False,
                    message=f"{self.message}: '{keyword}'",
                    action=self.action,
                    override_content=self.override_with if self.action == "override" else None,
                )
        return GuardrailResult(passed=True)


class ContentModerationGuard(Guardrail):
    """Moderation guard with optional external moderation callback."""

    DEFAULT_CATEGORIES = {
        "violence": ["kill", "murder", "bomb", "shoot"],
        "self_harm": ["suicide", "self-harm", "cut myself"],
        "sexual": ["explicit sexual", "sexual content"],
        "hate": ["racial slur", "ethnic cleansing", "hate group"],
    }

    def __init__(
        self,
        *,
        blocked_categories: list[str] | None = None,
        moderation_fn: Callable[[str, str], Any] | None = None,
        action: str = "block",
    ) -> None:
        self.blocked_categories = blocked_categories or ["violence", "self_harm", "hate"]
        self.moderation_fn = moderation_fn
        self.action = action

    async def check(self, content: str, *, role: str = "user") -> GuardrailResult:
        findings: list[str] = []
        if self.moderation_fn is not None:
            result = self.moderation_fn(content, role)
            if inspect.isawaitable(result):
                result = await result
            if isinstance(result, dict):
                findings = [str(item) for item in result.get("categories", [])]
            elif isinstance(result, (list, tuple, set)):
                findings = [str(item) for item in result]
        else:
            lowered = content.lower()
            for category, phrases in self.DEFAULT_CATEGORIES.items():
                if any(phrase in lowered for phrase in phrases):
                    findings.append(category)

        blocked = [item for item in findings if item in self.blocked_categories]
        if blocked:
            return GuardrailResult(
                passed=False,
                message=f"Moderation blocked categories: {', '.join(sorted(blocked))}",
                action=self.action,
            )
        return GuardrailResult(passed=True)


class ToxicityGuard(Guardrail):
    """Heuristic toxicity detector."""

    def __init__(
        self,
        *,
        toxic_terms: list[str] | None = None,
        threshold: int = 1,
        action: str = "block",
    ) -> None:
        self.toxic_terms = toxic_terms or ["idiot", "stupid", "worthless", "hate you", "kill yourself"]
        self.threshold = max(1, threshold)
        self.action = action

    async def check(self, content: str, *, role: str = "user") -> GuardrailResult:
        lowered = content.lower()
        matches = [term for term in self.toxic_terms if term.lower() in lowered]
        if len(matches) >= self.threshold:
            return GuardrailResult(
                passed=False,
                message=f"Toxicity detected: {', '.join(matches)}",
                action=self.action,
            )
        return GuardrailResult(passed=True)


class HallucinationGuard(Guardrail):
    """Guard that compares output against a reference context or callback."""

    def __init__(
        self,
        *,
        reference_text: str | None = None,
        required_facts: list[str] | None = None,
        checker: Callable[[str, str], Any] | None = None,
        action: str = "warn",
    ) -> None:
        self.reference_text = reference_text or ""
        self.required_facts = required_facts or []
        self.checker = checker
        self.action = action

    async def check(self, content: str, *, role: str = "assistant") -> GuardrailResult:
        if self.checker is not None:
            result = self.checker(content, role)
            if inspect.isawaitable(result):
                result = await result
            if isinstance(result, GuardrailResult):
                return result
            if result is False:
                return GuardrailResult(
                    passed=False,
                    message="Hallucination checker rejected content",
                    action=self.action,
                )
            return GuardrailResult(passed=True)

        if role != "assistant":
            return GuardrailResult(passed=True)

        reference = self.reference_text.lower()
        missing_facts = [fact for fact in self.required_facts if fact.lower() not in content.lower()]
        unsupported_sentences = []
        if reference:
            for sentence in [part.strip() for part in re.split(r"[.!?]\s*", content) if part.strip()]:
                tokens = [token for token in re.findall(r"\w+", sentence.lower()) if len(token) > 3]
                if tokens and not any(token in reference for token in tokens[:3]):
                    unsupported_sentences.append(sentence)
        if missing_facts or unsupported_sentences:
            parts = []
            if missing_facts:
                parts.append(f"missing facts: {', '.join(missing_facts)}")
            if unsupported_sentences:
                parts.append(f"unsupported claims: {unsupported_sentences[0][:120]}")
            return GuardrailResult(
                passed=False,
                message="Hallucination risk detected (" + "; ".join(parts) + ")",
                action=self.action,
            )
        return GuardrailResult(passed=True)


__all__ = [
    "Guardrail",
    "GuardrailResult",
    "ContentFilter",
    "ContentModerationGuard",
    "HallucinationGuard",
    "RegexGuard",
    "MaxLengthGuard",
    "KeywordGuard",
    "ToxicityGuard",
]
