"""BlockList scanner for Tru-Guard."""

from typing import Any

try:
    from fuzzysearch import find_near_matches
except ImportError:
    find_near_matches = None  # type: ignore

from trusys.scanners import BaseScanner, register_scanner
from trusys.types import (
    ScanContext,
    ScanResult,
    ScanType,
    Severity,
    Violation,
)


@register_scanner(
    name="blocklist",
    scan_types=["input", "output"],
    description="Scans content against a blocklist of words/phrases with optional fuzzy matching",
    validator_type="blocklist",
)
class BlockListScanner(BaseScanner):
    """Scanner that checks content against a blocklist of words/phrases.

    Supports both exact matching and fuzzy matching (using Levenshtein distance).
    Can be case-sensitive or case-insensitive.
    Can check multiple blocklists with different settings in a single scan.

    Args:
        config: Array of blocklist objects, each with:
               - blocklist (str, required): Word/phrase to block
               - case_sensitive (bool, optional): Whether matching is case-sensitive (default: False)
               - fuzzy_match (bool, optional): Whether to use fuzzy matching (default: False)
               - max_l_dist (int, optional): Maximum Levenshtein distance for fuzzy matching (default: 1)
        on_fail: Action to take when scan fails
        enabled: Whether this scanner is enabled
    """

    def __init__(
        self,
        config: list[dict[str, Any]] | None = None,
        on_fail: str | None = None,
        enabled: bool | None = None,
    ) -> None:
        """Initialize the blocklist scanner.

        Args:
            config: Array of blocklist objects, each containing:
                   - blocklist (str, required): Word/phrase to block
                   - case_sensitive (bool, optional): Whether matching is case-sensitive (default: False)
                   - fuzzy_match (bool, optional): Whether to use fuzzy matching (default: False)
                   - max_l_dist (int, optional): Maximum Levenshtein distance for fuzzy matching (default: 1)
            on_fail: Action to take when scan fails
            enabled: Whether this scanner is enabled
        """
        # Config should be a list of blocklist objects
        if config is None:
            raise ValueError("'config' is required for BlockListScanner and must be a list of blocklist objects")
        
        if not isinstance(config, list):
            raise ValueError("'config' must be a list of blocklist objects for BlockListScanner")
        
        if len(config) == 0:
            raise ValueError("'config' must contain at least one blocklist object")
        
        blocklists = config
        
        # Initialize base class with empty dict (we handle config separately)
        super().__init__(config={}, on_fail=on_fail, enabled=enabled)

        # Process and validate each blocklist
        self.blocklists: list[dict[str, Any]] = []
        for i, blocklist_config in enumerate(blocklists):
            if not isinstance(blocklist_config, dict):
                raise ValueError(f"Blocklist at index {i} must be a dictionary")
            
            blocklist = blocklist_config.get("blocklist")
            if not blocklist:
                raise ValueError(f"'blocklist' is required in blocklist object at index {i}")
            if not isinstance(blocklist, str):
                raise ValueError(f"'blocklist' must be a string in blocklist object at index {i}")
            
            # Extract other parameters with defaults
            case_sensitive = blocklist_config.get("case_sensitive", False)
            fuzzy_match = blocklist_config.get("fuzzy_match", False)
            max_l_dist = blocklist_config.get("max_l_dist", 1)
            
            # Validate fuzzy_match dependencies
            if fuzzy_match and find_near_matches is None:
                raise ImportError(
                    "fuzzysearch library is required for fuzzy matching. "
                    "Install it with: pip install fuzzysearch"
                )
            
            # Store blocklist configuration
            self.blocklists.append({
                "blocklist": blocklist,
                "case_sensitive": case_sensitive,
                "fuzzy_match": fuzzy_match,
                "max_l_dist": max_l_dist,
            })

    def scan(self, content: dict[str, Any], context: ScanContext) -> ScanResult:
        """Perform blocklist scan on content.

        Args:
            content: JSON dictionary containing content to scan.
                   Should contain 'prompt' for input scans or 'response' for output scans.
                   The scanner extracts the appropriate field based on scan_type.
            context: The scan context

        Returns:
            ScanResult with the scan outcome
        """
        # Extract the actual text to scan based on scan type
        if context.scan_type == ScanType.INPUT:
            text_to_scan = content.get("prompt", "")
        elif context.scan_type == ScanType.OUTPUT:
            text_to_scan = content.get("response", "")
        else:
            # Fallback: try prompt first, then response
            text_to_scan = content.get("prompt") or content.get("response", "")

        all_violations = []
        all_metadata = []

        # Check each blocklist
        for blocklist_config in self.blocklists:
            blocklist = blocklist_config["blocklist"]
            case_sensitive = blocklist_config["case_sensitive"]
            fuzzy_match = blocklist_config["fuzzy_match"]
            max_l_dist = blocklist_config["max_l_dist"]
            
            if fuzzy_match:
                violations = self._fuzzy_scan(text_to_scan, blocklist, case_sensitive, max_l_dist)
            else:
                violations = self._exact_scan(text_to_scan, blocklist, case_sensitive)
            
            all_violations.extend(violations)
            
            # Store metadata for this blocklist
            all_metadata.append({
                "blocklist": blocklist,
                "case_sensitive": case_sensitive,
                "fuzzy_match": fuzzy_match,
                "max_l_dist": max_l_dist if fuzzy_match else None,
                "matches_found": len(violations),
            })

        passed = len(all_violations) == 0

        # Build reason from violations - list all matches
        reason = ""
        if not passed:
            if all_violations:
                parts = []
                for v in all_violations:
                    blocked_item = v.metadata.get("blocked_item", "")
                    matched_text = v.metadata.get("matched_text", "")
                    if blocked_item:
                        part = f"'{blocked_item}'"
                        if matched_text:
                            part += f" (matched: '{matched_text}')"
                        parts.append(part)
                    else:
                        parts.append(v.message)
                reason = "Found blocked word/phrase: " + ", ".join(parts) if parts else all_violations[0].message
            else:
                reason = "Blocklist check failed"
        
        # Build assertion output - list all matches
        assertion_output = ""
        if all_violations:
            matched_parts = []
            for v in all_violations:
                m = v.metadata.get("matched_text")
                b = v.metadata.get("blocked_item")
                if m:
                    matched_parts.append(f"matched_text': '{m}'")
                elif b:
                    matched_parts.append(f"blocked_item': '{b}'")
            assertion_output = "; ".join(matched_parts) if matched_parts else ""
        
        # Calculate score (0.0 if failed, 1.0 if passed)
        score = 1.0 if passed else 0.9  # Default score for failures
        
        # Build metadata in new format
        metadata = {
            "blocklists": all_metadata,
            "total_matches_found": len(all_violations),
            "assertion": {
                "type": "blocklist",
                "output": assertion_output
            },
            "score": score,
            "reason": reason,
            "latency": 0.0,  # Will be set by execution runner
            "action": {
                "type": self.on_fail.value if self.on_fail else "ignore"
            }
        }

        return ScanResult(
            passed=passed,
            scanner_name=self.name,
            scan_type=context.scan_type,
            violations=all_violations,
            metadata=metadata,
        )

    def _exact_scan(self, text: str, blocklist: str, case_sensitive: bool) -> list[Violation]:
        """Perform exact matching scan.

        Args:
            text: The text to scan
            blocklist: Word/phrase to block
            case_sensitive: Whether matching is case-sensitive

        Returns:
            List of violations found
        """
        violations = []
        search_text = text if case_sensitive else text.lower()
        search_item = blocklist if case_sensitive else blocklist.lower()

        # Find all occurrences
        start = 0
        while True:
            pos = search_text.find(search_item, start)
            if pos == -1:
                break

            end = pos + len(search_item)
            matched_text = text[pos:end]

            violations.append(
                Violation(
                    rule="blocklist_exact_match",
                    severity=Severity.HIGH,
                    message=f"Found blocked word/phrase: '{blocklist}'",
                    span=(pos, end),
                    metadata={"matched_text": matched_text, "blocked_item": blocklist},
                )
            )

            start = pos + 1

        return violations

    def _fuzzy_scan(
        self, text: str, blocklist: str, case_sensitive: bool, max_l_dist: int
    ) -> list[Violation]:
        """Perform fuzzy matching scan.

        Args:
            text: The text to scan
            blocklist: Word/phrase to block
            case_sensitive: Whether matching is case-sensitive
            max_l_dist: Maximum Levenshtein distance for fuzzy matching

        Returns:
            List of violations found
        """
        violations = []

        # Create spaceless version for fuzzy matching
        spaceless_text = text.replace(" ", "").lower() if not case_sensitive else text.replace(" ", "")
        # Map from spaceless index to original index
        # List of tuples: (character, index in original string)
        # Using 0-based indexing (Python standard)
        spaceless_index_map: list[tuple[str, int]] = []

        for i in range(len(text)):
            if text[i] != " ":
                spaceless_index_map.append((text[i], i))

        spaceless_blocked = (
            blocklist.replace(" ", "").lower()
            if not case_sensitive
            else blocklist.replace(" ", "")
        )

        matches = find_near_matches(
            spaceless_blocked,
            spaceless_text,
            max_l_dist=max_l_dist,
        )

        # Convert matches to violations
        for match in matches:
            if match.start < len(spaceless_index_map) and match.end > 0:
                # Map spaceless start index to original start index (0-based)
                actual_start = spaceless_index_map[match.start][1]
                # Map spaceless end index to original end index
                # match.end is exclusive, so we use match.end - 1
                end_idx = min(match.end - 1, len(spaceless_index_map) - 1)
                actual_end = spaceless_index_map[end_idx][1] + 1  # +1 because end is exclusive in slicing
                triggering_text = text[actual_start:actual_end]

                violations.append(
                    Violation(
                        rule="blocklist_fuzzy_match",
                        severity=Severity.HIGH,
                        message=f"Found fuzzy match with blocked word/phrase '{blocklist}' (distance={match.dist})",
                        span=(actual_start, actual_end),
                        metadata={
                            "matched_text": triggering_text,
                            "fuzzy_match": match.matched,
                            "levenshtein_distance": match.dist,
                            "blocked_item": blocklist,
                        },
                    )
                )

        return violations

    def get_config(self) -> dict[str, Any]:
        """Get the scanner configuration.

        Returns:
            Dictionary of configuration values with 'blocklists' key containing the array
        """
        config = super().get_config()
        blocklists_config = []
        for blocklist_config in self.blocklists:
            blocklists_config.append({
                "blocklist": blocklist_config["blocklist"],
                "case_sensitive": blocklist_config["case_sensitive"],
                "fuzzy_match": blocklist_config["fuzzy_match"],
                "max_l_dist": blocklist_config["max_l_dist"] if blocklist_config["fuzzy_match"] else None,
            })
        # Return the blocklists array directly in the config dict
        config["blocklists"] = blocklists_config
        return config

    def __repr__(self) -> str:
        """String representation of the scanner."""
        blocklists_repr = ", ".join(
            f"{{blocklist={b['blocklist']!r}, "
            f"case_sensitive={b['case_sensitive']}, "
            f"fuzzy_match={b['fuzzy_match']}}}"
            for b in self.blocklists
        )
        return (
            f"{self.__class__.__name__}("
            f"blocklists=[{blocklists_repr}], "
            f"on_fail={self.on_fail.value!r})"
        )
