from __future__ import annotations

import contextlib
import fnmatch
import ipaddress
import re
from typing import TYPE_CHECKING, Any, Protocol
from urllib.parse import unquote

from lewaf.core import compile_regex

if TYPE_CHECKING:
    from collections.abc import Callable

    from lewaf.primitives.collections import TransactionVariables


class TransactionProtocol(Protocol):
    """Protocol defining the transaction interface needed by operators."""

    variables: TransactionVariables

    def capturing(self) -> bool:
        """Return whether the transaction is capturing matches."""
        ...

    def capture_field(self, index: int, value: str) -> None:
        """Capture a field value at the given index."""
        ...


OPERATORS = {}


class OperatorOptions:
    """Options for creating operators, matching Go's OperatorOptions."""

    def __init__(
        self,
        arguments: str,
        path: list[str] | None = None,
        datasets: dict[str, list[str]] | None = None,
    ):
        self.arguments = arguments
        self.path = path or []
        self.datasets = datasets or {}


class Operator:
    """Base class for rule operators."""

    def __init__(self, argument: str):
        self._argument = argument

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Evaluate the operator against a value in the context of a transaction."""
        raise NotImplementedError


class OperatorFactory:
    """Factory function type for creating operators."""

    @staticmethod
    def create(options: OperatorOptions) -> Any:
        raise NotImplementedError


def register_operator(name: str) -> Callable:
    """Register an operator factory by name."""

    def decorator(factory_cls):
        OPERATORS[name.lower()] = factory_cls
        return factory_cls

    return decorator


def get_operator(name: str, options: OperatorOptions) -> Operator:
    """Get an operator instance by name."""
    if name.lower() not in OPERATORS:
        msg = f"Unknown operator: {name}"
        raise ValueError(msg)
    factory = OPERATORS[name.lower()]
    return factory.create(options)


@register_operator("rx")
class RxOperatorFactory(OperatorFactory):
    """Factory for regex operators."""

    @staticmethod
    def create(options: OperatorOptions) -> RxOperator:
        return RxOperator(options.arguments)


class RxOperator(Operator):
    """Regular expression operator."""

    def __init__(self, argument: str):
        super().__init__(argument)
        self._regex = compile_regex(argument)

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Evaluate regex against the value."""
        if tx.capturing():
            # Handle capture groups if transaction supports it
            match = self._regex.search(value)
            if match:
                for i, group in enumerate(
                    match.groups()[:9]
                ):  # Max 9 capture groups like Go
                    tx.capture_field(i + 1, group if group is not None else "")
                return True
            return False
        return self._regex.search(value) is not None


@register_operator("eq")
class EqOperatorFactory(OperatorFactory):
    """Factory for equality operators."""

    @staticmethod
    def create(options: OperatorOptions) -> EqOperator:
        return EqOperator(options.arguments)


class EqOperator(Operator):
    """Equality operator."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value equals the argument."""
        return value == self._argument


@register_operator("contains")
class ContainsOperatorFactory(OperatorFactory):
    """Factory for contains operators."""

    @staticmethod
    def create(options: OperatorOptions) -> ContainsOperator:
        return ContainsOperator(options.arguments)


class ContainsOperator(Operator):
    """Contains substring operator."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value contains the argument substring."""
        return self._argument in value


@register_operator("beginswith")
class BeginsWithOperatorFactory(OperatorFactory):
    """Factory for begins with operators."""

    @staticmethod
    def create(options: OperatorOptions) -> BeginsWithOperator:
        return BeginsWithOperator(options.arguments)


class BeginsWithOperator(Operator):
    """Begins with operator."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value begins with the argument."""
        return value.startswith(self._argument)


@register_operator("endswith")
class EndsWithOperatorFactory(OperatorFactory):
    """Factory for ends with operators."""

    @staticmethod
    def create(options: OperatorOptions) -> EndsWithOperator:
        return EndsWithOperator(options.arguments)


class EndsWithOperator(Operator):
    """Ends with operator."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value ends with the argument."""
        return value.endswith(self._argument)


@register_operator("gt")
class GtOperatorFactory(OperatorFactory):
    """Factory for greater than operators."""

    @staticmethod
    def create(options: OperatorOptions) -> GtOperator:
        return GtOperator(options.arguments)


class GtOperator(Operator):
    """Greater than operator."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value is greater than the argument."""
        try:
            return float(value) > float(self._argument)
        except ValueError:
            return False


@register_operator("ge")
class GeOperatorFactory(OperatorFactory):
    """Factory for greater than or equal operators."""

    @staticmethod
    def create(options: OperatorOptions) -> GeOperator:
        return GeOperator(options.arguments)


class GeOperator(Operator):
    """Greater than or equal operator."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value is greater than or equal to the argument."""
        try:
            return float(value) >= float(self._argument)
        except ValueError:
            return False


@register_operator("lt")
class LtOperatorFactory(OperatorFactory):
    """Factory for less than operators."""

    @staticmethod
    def create(options: OperatorOptions) -> LtOperator:
        return LtOperator(options.arguments)


class LtOperator(Operator):
    """Less than operator."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value is less than the argument."""
        try:
            return float(value) < float(self._argument)
        except ValueError:
            return False


@register_operator("le")
class LeOperatorFactory(OperatorFactory):
    """Factory for less than or equal operators."""

    @staticmethod
    def create(options: OperatorOptions) -> LeOperator:
        return LeOperator(options.arguments)


class LeOperator(Operator):
    """Less than or equal operator."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value is less than or equal to the argument."""
        try:
            return float(value) <= float(self._argument)
        except ValueError:
            return False


@register_operator("within")
class WithinOperatorFactory(OperatorFactory):
    """Factory for within operators."""

    @staticmethod
    def create(options: OperatorOptions) -> WithinOperator:
        return WithinOperator(options.arguments)


class WithinOperator(Operator):
    """Within range operator."""

    def __init__(self, argument: str):
        super().__init__(argument)
        # Parse space-separated values
        self._values = set(argument.split())

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value is within the set of allowed values."""
        return value in self._values


@register_operator("ipmatch")
class IpMatchOperatorFactory(OperatorFactory):
    """Factory for IP match operators."""

    @staticmethod
    def create(options: OperatorOptions) -> IpMatchOperator:
        return IpMatchOperator(options.arguments)


class IpMatchOperator(Operator):
    """IP address/network matching operator."""

    def __init__(self, argument: str):
        super().__init__(argument)
        self._network: ipaddress.IPv4Network | ipaddress.IPv6Network | None = None
        # Parse IP address or CIDR network
        try:
            self._network = ipaddress.ip_network(argument, strict=False)
        except ValueError:
            # Fallback to exact IP match
            try:
                self._network = ipaddress.ip_network(f"{argument}/32", strict=False)
            except ValueError:
                self._network = None

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if IP address matches the network/address."""
        if not self._network:
            return False

        try:
            ip = ipaddress.ip_address(value.strip())
            return ip in self._network
        except ValueError:
            return False


@register_operator("detectsqli")
class DetectSQLiOperatorFactory(OperatorFactory):
    """Factory for SQL injection detection operators."""

    @staticmethod
    def create(options: OperatorOptions) -> DetectSQLiOperator:
        return DetectSQLiOperator(options.arguments)


class DetectSQLiOperator(Operator):
    """SQL injection detection operator."""

    def __init__(self, argument: str):
        super().__init__(argument)
        # Common SQL injection patterns
        self._patterns = [
            compile_regex(r"(?i)(union\s+select|select\s+.*\s+from)"),
            compile_regex(r"(?i)(or\s+1\s*=\s*1|and\s+1\s*=\s*1)"),
            compile_regex(r"(?i)(drop\s+table|delete\s+from|insert\s+into)"),
            compile_regex(r"(?i)(exec\s*\(|execute\s*\(|sp_executesql)"),
            compile_regex(r"(?i)['\"][\s]*(\s*or\s+|--|\s*union\s+)"),
            compile_regex(r"(?i)(having\s+|group\s+by\s+|order\s+by\s+)"),
        ]

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Detect SQL injection patterns."""
        decoded_value = unquote(value)  # URL decode first
        for pattern in self._patterns:
            if pattern.search(decoded_value):
                return True
        return False


@register_operator("detectxss")
class DetectXSSOperatorFactory(OperatorFactory):
    """Factory for XSS detection operators."""

    @staticmethod
    def create(options: OperatorOptions) -> DetectXSSOperator:
        return DetectXSSOperator(options.arguments)


class DetectXSSOperator(Operator):
    """XSS detection operator."""

    def __init__(self, argument: str):
        super().__init__(argument)
        # Common XSS patterns
        self._patterns = [
            compile_regex(r"(?i)<script[^>]*>"),
            compile_regex(r"(?i)javascript:"),
            compile_regex(r"(?i)on\w+\s*="),  # event handlers
            compile_regex(r"(?i)<iframe[^>]*>"),
            compile_regex(r"(?i)document\.cookie"),
            compile_regex(r"(?i)alert\s*\("),
            compile_regex(r"(?i)eval\s*\("),
            compile_regex(r"(?i)<object[^>]*>"),
            compile_regex(r"(?i)<embed[^>]*>"),
        ]

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Detect XSS patterns."""
        decoded_value = unquote(value)  # URL decode first
        for pattern in self._patterns:
            if pattern.search(decoded_value):
                return True
        return False


@register_operator("validatebyterange")
class ValidateByteRangeOperatorFactory(OperatorFactory):
    """Factory for byte range validation operators."""

    @staticmethod
    def create(options: OperatorOptions) -> ValidateByteRangeOperator:
        return ValidateByteRangeOperator(options.arguments)


class ValidateByteRangeOperator(Operator):
    """Validate byte range operator."""

    def __init__(self, argument: str):
        super().__init__(argument)
        # Parse byte ranges like "32-126,9,10,13" or "1-255"
        self._valid_bytes: set[int] = set()
        for part in argument.split(","):
            part = part.strip()
            if "-" in part:
                start, end = map(int, part.split("-"))
                self._valid_bytes.update(range(start, end + 1))
            else:
                self._valid_bytes.add(int(part))

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if all bytes in value are within valid ranges."""
        try:
            value_bytes = value.encode("utf-8")
            return all(byte in self._valid_bytes for byte in value_bytes)
        except Exception:
            return False


@register_operator("validateutf8encoding")
class ValidateUtf8EncodingOperatorFactory(OperatorFactory):
    """Factory for UTF-8 validation operators."""

    @staticmethod
    def create(options: OperatorOptions) -> ValidateUtf8EncodingOperator:
        return ValidateUtf8EncodingOperator(options.arguments)


class ValidateUtf8EncodingOperator(Operator):
    """UTF-8 encoding validation operator."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value is valid UTF-8."""
        try:
            # If we can encode and decode it, it's valid UTF-8
            value.encode("utf-8").decode("utf-8")
            return True
        except UnicodeError:
            return False


@register_operator("pm")
class PmOperatorFactory(OperatorFactory):
    """Factory for phrase match operators."""

    @staticmethod
    def create(options: OperatorOptions) -> PmOperator:
        return PmOperator(options.arguments)


class PmOperator(Operator):
    """Phrase match operator for exact string matching."""

    def __init__(self, argument: str):
        super().__init__(argument)
        # Parse space-separated phrases
        self._phrases = [
            phrase.strip() for phrase in argument.split() if phrase.strip()
        ]

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if any phrase matches the value."""
        value_lower = value.lower()
        return any(phrase.lower() in value_lower for phrase in self._phrases)


@register_operator("pmfromfile")
class PmFromFileOperatorFactory(OperatorFactory):
    """Factory for phrase match from file operators."""

    @staticmethod
    def create(options: OperatorOptions) -> PmFromFileOperator:
        return PmFromFileOperator(options.arguments)


class PmFromFileOperator(Operator):
    """Phrase match from file operator."""

    def __init__(self, argument: str):
        super().__init__(argument)
        self._phrases: list[str] = []
        # In a real implementation, we'd read from the file
        # For now, we'll simulate by treating the argument as a filename
        self._filename = argument

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if any phrase from file matches the value."""
        # For testing purposes, we'll simulate some common patterns
        # In production, this would read from the actual data file
        if "php-errors" in self._filename:
            php_errors = ["parse error", "fatal error", "warning:", "notice:"]
            return any(error in value.lower() for error in php_errors)
        if "sql-errors" in self._filename:
            sql_errors = ["syntax error", "mysql error", "ora-", "sqlstate"]
            return any(error in value.lower() for error in sql_errors)
        if "unix-shell" in self._filename:
            shell_commands = ["bin/sh", "/bin/bash", "wget", "curl"]
            return any(cmd in value.lower() for cmd in shell_commands)

        # Default behavior - no match
        return False


@register_operator("strmatch")
class StrMatchOperatorFactory(OperatorFactory):
    """Factory for string match operators."""

    @staticmethod
    def create(options: OperatorOptions) -> StrMatchOperator:
        return StrMatchOperator(options.arguments)


class StrMatchOperator(Operator):
    """String match operator with wildcards."""

    def __init__(self, argument: str):
        super().__init__(argument)
        # Convert glob-style pattern to regex
        self._pattern = compile_regex(fnmatch.translate(argument))

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value matches the string pattern."""
        return self._pattern.match(value) is not None


@register_operator("streq")
class StrEqOperatorFactory(OperatorFactory):
    """Factory for string equality operators."""

    @staticmethod
    def create(options: OperatorOptions) -> StrEqOperator:
        return StrEqOperator(options.arguments)


class StrEqOperator(Operator):
    """String equality operator (case sensitive)."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value exactly equals the argument."""
        return value == self._argument


@register_operator("unconditional")
@register_operator("unconditionalmatch")  # Alias for Go compatibility
class UnconditionalOperatorFactory(OperatorFactory):
    """Factory for unconditional operators."""

    @staticmethod
    def create(options: OperatorOptions) -> UnconditionalOperator:
        return UnconditionalOperator(options.arguments)


class UnconditionalOperator(Operator):
    """Unconditional operator that always matches."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Always returns True."""
        return True


@register_operator("nomatch")
class NoMatchOperatorFactory(OperatorFactory):
    """Factory for NoMatch operators."""

    @staticmethod
    def create(options: OperatorOptions) -> NoMatchOperator:
        return NoMatchOperator(options.arguments)


class NoMatchOperator(Operator):
    """NoMatch operator that always returns false."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Always returns False."""
        return False


@register_operator("validateurlencoding")
class ValidateUrlEncodingOperatorFactory(OperatorFactory):
    """Factory for ValidateUrlEncoding operators."""

    @staticmethod
    def create(options: OperatorOptions) -> ValidateUrlEncodingOperator:
        return ValidateUrlEncodingOperator(options.arguments)


class ValidateUrlEncodingOperator(Operator):
    """Validates URL-encoded characters in input."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if the input contains valid URL encoding."""
        import re  # noqa: PLC0415 - Avoids circular import

        # Find all percent-encoded sequences
        encoded_chars = re.findall(r"%[0-9A-Fa-f]{2}", value)

        for encoded_char in encoded_chars:
            try:
                # Try to decode the percent-encoded character
                hex_value = encoded_char[1:]  # Remove the %
                int(hex_value, 16)  # Validate it's a valid hex number
            except ValueError:
                # Invalid hex encoding found
                return True

        # Check for incomplete percent encodings (% followed by less than 2 hex chars)
        incomplete_pattern = r"%(?:[0-9A-Fa-f]?(?![0-9A-Fa-f])|(?![0-9A-Fa-f]))"
        return bool(re.search(incomplete_pattern, value))


@register_operator("validateschema")
class ValidateSchemaOperatorFactory(OperatorFactory):
    """Factory for ValidateSchema operators."""

    @staticmethod
    def create(options: OperatorOptions) -> ValidateSchemaOperator:
        return ValidateSchemaOperator(options.arguments)


class ValidateSchemaOperator(Operator):
    """Validates JSON/XML schema."""

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if the input is valid JSON or XML."""
        import json  # noqa: PLC0415 - Avoids circular import
        import xml.etree.ElementTree as ET  # noqa: PLC0415 - Avoids circular import

        # Try JSON validation first
        try:
            json.loads(value)
            return False  # Valid JSON, no error
        except json.JSONDecodeError:
            pass

        # Try XML validation
        try:
            ET.fromstring(value)
            return False  # Valid XML, no error
        except ET.ParseError:
            pass

        # If neither JSON nor XML is valid, return True (validation failed)
        return True


@register_operator("validatenid")
class ValidateNidOperatorFactory(OperatorFactory):
    """Factory for ValidateNid operators."""

    @staticmethod
    def create(options: OperatorOptions) -> ValidateNidOperator:
        return ValidateNidOperator(options.arguments)


class ValidateNidOperator(Operator):
    """Validates National ID numbers for different countries.

    Syntax: @validateNid <country_code> <regex>
    Supported countries:
    - cl: Chilean RUT (Rol Único Tributario)
    - us: US Social Security Number
    """

    def __init__(self, argument: str):
        super().__init__(argument)
        # Parse argument: "country_code regex_pattern"
        parts = argument.split(None, 1)
        if len(parts) < 2:
            msg = "validateNid requires format: <country_code> <regex>"
            raise ValueError(msg)

        self._country_code = parts[0].lower()
        self._regex_pattern = parts[1]
        self._regex = compile_regex(self._regex_pattern)

        # Select validation function based on country code
        if self._country_code == "cl":
            self._validator = self._validate_cl
        elif self._country_code == "us":
            self._validator = self._validate_us
        else:
            msg = (
                f"Unsupported country code '{self._country_code}'. "
                "Supported: cl (Chile), us (USA)"
            )
            raise ValueError(msg)

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Find and validate National IDs in the input value."""
        matches = self._regex.findall(value)

        result = False
        for i, match in enumerate(matches[:10]):  # Max 10 matches
            if self._validator(match):
                result = True
                # Capture the valid NID
                tx.capture_field(i, match)

        return result

    def _validate_cl(self, nid: str) -> bool:
        """Validate Chilean RUT (Rol Único Tributario).

        Format: 12.345.678-9 or 12345678-9 or 123456789
        Uses modulo 11 checksum algorithm.
        """
        if len(nid) < 8:
            return False

        # Normalize: remove non-digits except 'k' or 'K'
        nid = nid.lower()
        nid = re.sub(r"[^\dk]", "", nid)

        if len(nid) < 2:
            return False

        # Split into number and verification digit
        rut_number = nid[:-1]
        dv = nid[-1]

        try:
            rut = int(rut_number)
        except ValueError:
            return False

        # Calculate verification digit using modulo 11
        total = 0
        factor = 2
        while rut > 0:
            total += (rut % 10) * factor
            rut //= 10
            if factor == 7:
                factor = 2
            else:
                factor += 1

        remainder = total % 11
        if remainder == 0:
            expected_dv = "0"
        elif remainder == 1:
            expected_dv = "k"
        else:
            expected_dv = str(11 - remainder)

        return dv == expected_dv

    def _validate_us(self, nid: str) -> bool:
        """Validate US Social Security Number.

        Format: 123-45-6789
        Rules:
        - Area (first 3 digits): 001-665, 667-899 (not 666)
        - Group (middle 2 digits): 01-99
        - Serial (last 4 digits): 0001-9999
        - No repeating digits (e.g., 111-11-1111)
        - No sequential digits (e.g., 123-45-6789 if truly sequential)
        """
        # Remove non-digits
        nid = re.sub(r"[^\d]", "", nid)

        if len(nid) < 9:
            return False

        try:
            area = int(nid[0:3])
            group = int(nid[3:5])
            serial = int(nid[5:9])
        except ValueError:
            return False

        # Validate area, group, serial ranges
        if area == 0 or group == 0 or serial == 0:
            return False
        if area >= 740 or area == 666:
            return False

        # Check for all same digits
        if len(set(nid[:9])) == 1:
            return False

        # Check for sequential digits
        is_sequential = True
        prev_digit = int(nid[0])
        for i in range(1, 9):
            curr_digit = int(nid[i])
            if curr_digit != prev_digit + 1:
                is_sequential = False
                break
            prev_digit = curr_digit

        return not is_sequential


@register_operator("restpath")
class RestPathOperatorFactory(OperatorFactory):
    """Factory for RestPath operators."""

    @staticmethod
    def create(options: OperatorOptions) -> RestPathOperator:
        return RestPathOperator(options.arguments)


class RestPathOperator(Operator):
    """REST path pattern matching operator."""

    def __init__(self, argument: str):
        super().__init__(argument)
        self._pattern = self._compile_path_pattern(argument)

    def _compile_path_pattern(self, path_pattern: str) -> str:
        """Convert REST path pattern to regex."""
        import re  # noqa: PLC0415 - Avoids circular import

        # Escape special regex characters except {}
        escaped = re.escape(path_pattern)

        # Replace escaped braces back and convert {param} to named capture groups
        # This handles patterns like /path/{id}/{name}
        pattern = re.sub(r"\\{([^}]+)\\}", r"(?P<\1>[^/]+)", escaped)

        # Anchor the pattern
        return f"^{pattern}$"

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if the input matches the REST path pattern."""
        import re  # noqa: PLC0415 - Avoids circular import

        match = re.match(self._pattern, value)
        if match:
            # Populate ARGS_PATH with captured path segments
            # and add to ARGS for unified access
            for name, captured_value in match.groupdict().items():
                if hasattr(tx, "variables"):
                    # Add to args_path collection (path parameters)
                    if hasattr(tx.variables, "args_path"):
                        tx.variables.args_path.add(name, captured_value)
                    # Also add to general args collection
                    if hasattr(tx.variables, "args"):
                        tx.variables.args.add(name, captured_value)
                    # Add parameter names
                    if hasattr(tx.variables, "args_names"):
                        tx.variables.args_names.add(name, name)
            return True
        return False


@register_operator("inspectfile")
class InspectFileOperatorFactory(OperatorFactory):
    """Factory for InspectFile operators."""

    @staticmethod
    def create(options: OperatorOptions) -> InspectFileOperator:
        return InspectFileOperator(options.arguments)


class InspectFileOperator(Operator):
    """File inspection operator that executes external programs."""

    def __init__(self, argument: str):
        super().__init__(argument)
        self._script_path = argument.strip()
        if not self._script_path:
            msg = "InspectFile operator requires a script path"
            raise ValueError(msg)

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Execute external script for file inspection."""
        import os  # noqa: PLC0415 - Avoids circular import
        import subprocess  # noqa: PLC0415 - Avoids circular import
        import tempfile  # noqa: PLC0415 - Avoids circular import

        # Security check: only allow certain file extensions
        allowed_extensions = [".pl", ".py", ".sh", ".lua"]
        if not any(self._script_path.endswith(ext) for ext in allowed_extensions):
            msg = f"InspectFile: Script type not allowed: {self._script_path}"
            raise ValueError(msg)

        # Security check: prevent path traversal
        if ".." in self._script_path:
            msg = f"InspectFile: Path traversal not allowed: {self._script_path}"
            raise ValueError(msg)

        try:
            # Create temporary file with the content to inspect
            with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file:
                temp_file.write(value)
                temp_file_path = temp_file.name

            try:
                # Execute the script with the temporary file path
                result = subprocess.run(
                    [self._script_path, temp_file_path],
                    capture_output=True,
                    text=True,
                    timeout=30,  # 30 second timeout
                    check=False,
                )

                # Parse output: expect "1 message" for clean, "0 message" for threat
                output = result.stdout.strip()
                if output.startswith("1 "):
                    return False  # Clean file
                if output.startswith("0 "):
                    return True  # Threat detected
                # Unexpected output format, treat as error
                return True

            finally:
                # Clean up temporary file
                with contextlib.suppress(OSError):
                    os.unlink(temp_file_path)

            # If we get here, the script ran but we didn't parse the output correctly
            return True

        except subprocess.TimeoutExpired:
            # Script timed out, treat as error
            return True
        except Exception:
            # Any other error, treat as failed inspection
            return True


@register_operator("ipmatchfromfile")
class IpMatchFromFileOperatorFactory(OperatorFactory):
    """Factory for IpMatchFromFile operators."""

    @staticmethod
    def create(options: OperatorOptions) -> IpMatchFromFileOperator:
        return IpMatchFromFileOperator(options.arguments)


class IpMatchFromFileOperator(Operator):
    """IP address matching from file operator."""

    def __init__(self, argument: str):
        super().__init__(argument)
        self._file_path = argument.strip()
        self._ip_list = self._load_ip_list()

    def _load_ip_list(self) -> list[str]:
        """Load IP addresses and networks from file."""
        import os  # noqa: PLC0415 - Avoids circular import

        if not self._file_path:
            msg = "IpMatchFromFile operator requires a file path"
            raise ValueError(msg)

        # Security check: prevent path traversal
        if ".." in self._file_path:
            msg = f"IpMatchFromFile: Path traversal not allowed: {self._file_path}"
            raise ValueError(msg)

        ip_list: list[str] = []
        try:
            # Check if file exists
            if not os.path.exists(self._file_path):
                # For now, just log and continue with empty list
                import logging  # noqa: PLC0415 - Avoids circular import

                logging.warning(f"IpMatchFromFile: File not found: {self._file_path}")
                return ip_list

            with open(self._file_path, encoding="utf-8") as f:
                for line in f:
                    line = line.strip()
                    # Skip empty lines and comments
                    if line and not line.startswith("#"):
                        ip_list.append(line)
        except Exception as e:
            import logging  # noqa: PLC0415 - Avoids circular import

            logging.error(f"IpMatchFromFile: Error loading file {self._file_path}: {e}")

        return ip_list

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if IP address matches any in the file."""
        import ipaddress  # noqa: PLC0415 - Avoids circular import

        if not self._ip_list:
            return False

        try:
            # Parse the input IP address
            input_ip = ipaddress.ip_address(value.strip())

            # Check against each IP/network in the list
            for ip_entry in self._ip_list:
                try:
                    # Try as network first (CIDR notation)
                    if "/" in ip_entry:
                        network = ipaddress.ip_network(ip_entry, strict=False)
                        if input_ip in network:
                            return True
                    else:
                        # Try as individual IP
                        list_ip = ipaddress.ip_address(ip_entry)
                        if input_ip == list_ip:
                            return True
                except ValueError:
                    # Invalid IP format in file, skip it
                    continue

        except ValueError:
            # Invalid input IP address
            return False

        return False


# Simple dataset registry for SecDataset support
DATASETS: dict[str, list[str]] = {}


def register_dataset(name: str, data: list[str]) -> None:
    """Register a dataset for use with dataset operators."""
    DATASETS[name] = data


def get_dataset(name: str) -> list[str]:
    """Get a dataset by name."""
    return DATASETS.get(name, [])


@register_operator("pmfromdataset")
class PmFromDatasetOperatorFactory(OperatorFactory):
    """Factory for PmFromDataset operators."""

    @staticmethod
    def create(options: OperatorOptions) -> PmFromDatasetOperator:
        return PmFromDatasetOperator(options.arguments)


class PmFromDatasetOperator(Operator):
    """Pattern matching from dataset operator."""

    def __init__(self, argument: str):
        super().__init__(argument)
        self._dataset_name = argument.strip()
        if not self._dataset_name:
            msg = "PmFromDataset operator requires a dataset name"
            raise ValueError(msg)

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if value contains any patterns from the dataset."""
        patterns = get_dataset(self._dataset_name)
        if not patterns:
            return False

        value_lower = value.lower()

        # Case-insensitive substring matching
        for pattern in patterns:
            if pattern.lower() in value_lower:
                if tx.capturing():
                    tx.capture_field(0, pattern)
                return True

        return False


@register_operator("ipmatchfromdataset")
class IpMatchFromDatasetOperatorFactory(OperatorFactory):
    """Factory for IpMatchFromDataset operators."""

    @staticmethod
    def create(options: OperatorOptions) -> IpMatchFromDatasetOperator:
        return IpMatchFromDatasetOperator(options.arguments)


class IpMatchFromDatasetOperator(Operator):
    """IP address matching from dataset operator."""

    def __init__(self, argument: str):
        super().__init__(argument)
        self._dataset_name = argument.strip()
        if not self._dataset_name:
            msg = "IpMatchFromDataset operator requires a dataset name"
            raise ValueError(msg)

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """Check if IP address matches any in the dataset."""
        import ipaddress  # noqa: PLC0415 - Avoids circular import

        ip_list = get_dataset(self._dataset_name)
        if not ip_list:
            return False

        try:
            # Parse the input IP address
            input_ip = ipaddress.ip_address(value.strip())

            # Check against each IP/network in the dataset
            for ip_entry in ip_list:
                try:
                    # Try as network first (CIDR notation)
                    if "/" in ip_entry:
                        network = ipaddress.ip_network(ip_entry, strict=False)
                        if input_ip in network:
                            return True
                    else:
                        # Try as individual IP
                        list_ip = ipaddress.ip_address(ip_entry)
                        if input_ip == list_ip:
                            return True
                except ValueError:
                    # Invalid IP format in dataset, skip it
                    continue

        except ValueError:
            # Invalid input IP address
            return False

        return False


@register_operator("geolookup")
class GeoLookupOperatorFactory(OperatorFactory):
    """Factory for GeoLookup operators."""

    @staticmethod
    def create(options: OperatorOptions) -> GeoLookupOperator:
        return GeoLookupOperator(options.arguments)


class GeoLookupOperator(Operator):
    """
    Geographic IP lookup operator for threat assessment.

    Performs IP geolocation and populates GEO collection variables:
    - GEO:COUNTRY_CODE (ISO 3166-1 alpha-2)
    - GEO:COUNTRY_CODE3 (ISO 3166-1 alpha-3)
    - GEO:COUNTRY_NAME (full country name)
    - GEO:COUNTRY_CONTINENT (continent code)
    - GEO:REGION (region/state code)
    - GEO:CITY (city name)
    - GEO:POSTAL_CODE (postal/zip code)
    - GEO:LATITUDE (latitude coordinate)
    - GEO:LONGITUDE (longitude coordinate)
    """

    def __init__(self, argument: str):
        super().__init__(argument)
        # Argument can specify the geolocation database path
        # For now, we'll use a simple mock implementation for demonstration
        self._db_path = argument if argument else None

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """
        Perform geolocation lookup on the input IP address.

        Args:
            tx: Transaction context
            value: IP address to lookup

        Returns:
            bool: True if geolocation data was successfully populated
        """
        import ipaddress  # noqa: PLC0415 - Avoids circular import

        try:
            # Validate IP address format
            ip_addr = ipaddress.ip_address(value.strip())

            # Skip private/local IP addresses
            if ip_addr.is_private or ip_addr.is_loopback or ip_addr.is_reserved:
                return False

            # For this implementation, we'll provide mock geolocation data
            # In a real implementation, this would query MaxMind GeoIP2 or similar
            geo_data = self._get_geolocation_data(str(ip_addr))

            if geo_data:
                # Populate GEO collection variables in transaction
                tx.variables.set_geo_data(geo_data)
                return True

            return False

        except ValueError:
            # Invalid IP address format
            return False

    def _get_geolocation_data(self, ip_address: str) -> dict[str, str] | None:
        """
        Get geolocation data for an IP address.

        This is a mock implementation. In production, this would integrate
        with MaxMind GeoIP2, IP2Location, or another geolocation service.

        Args:
            ip_address: IP address to lookup

        Returns:
            dict: Geolocation data or None if not found
        """
        # Mock data for common IP ranges for demonstration
        # In production, this would query an actual geolocation database

        # Example: Classify some known IP ranges
        if ip_address.startswith(("8.8.8.", "8.8.4.")):
            # Google DNS servers - mock as US
            return {
                "COUNTRY_CODE": "US",
                "COUNTRY_CODE3": "USA",
                "COUNTRY_NAME": "United States",
                "COUNTRY_CONTINENT": "NA",
                "REGION": "CA",
                "CITY": "Mountain View",
                "POSTAL_CODE": "94043",
                "LATITUDE": "37.4056",
                "LONGITUDE": "-122.0775",
            }
        if ip_address.startswith(("1.1.1.", "1.0.0.")):
            # Cloudflare DNS - mock as US
            return {
                "COUNTRY_CODE": "US",
                "COUNTRY_CODE3": "USA",
                "COUNTRY_NAME": "United States",
                "COUNTRY_CONTINENT": "NA",
                "REGION": "CA",
                "CITY": "San Francisco",
                "POSTAL_CODE": "94102",
                "LATITUDE": "37.7749",
                "LONGITUDE": "-122.4194",
            }
        # Default/unknown - mock as generic location
        return {
            "COUNTRY_CODE": "XX",
            "COUNTRY_CODE3": "XXX",
            "COUNTRY_NAME": "Unknown",
            "COUNTRY_CONTINENT": "XX",
            "REGION": "XX",
            "CITY": "Unknown",
            "POSTAL_CODE": "",
            "LATITUDE": "0.0000",
            "LONGITUDE": "0.0000",
        }


@register_operator("rbl")
class RblOperatorFactory(OperatorFactory):
    """Factory for Real-time Blacklist (RBL) operators."""

    @staticmethod
    def create(options: OperatorOptions) -> RblOperator:
        return RblOperator(options.arguments)


class RblOperator(Operator):
    """
    Real-time Blacklist (RBL) operator for threat intelligence integration.

    Checks IP addresses against DNS-based blacklists (DNSBL) for known threats:
    - Spam sources
    - Malware command & control servers
    - Known attackers
    - Compromised hosts
    - Tor exit nodes
    """

    def __init__(self, argument: str):
        super().__init__(argument)
        # Argument specifies the RBL hostname(s) to check
        # Format: "rbl1.example.com,rbl2.example.com" or single hostname
        self._rbl_hosts = []
        if argument:
            self._rbl_hosts = [host.strip() for host in argument.split(",")]
        else:
            # Default to common RBL services
            self._rbl_hosts = [
                "zen.spamhaus.org",
                "bl.spamcop.net",
                "dnsbl.sorbs.net",
                "cbl.abuseat.org",
            ]

    def evaluate(self, tx: TransactionProtocol, value: str) -> bool:
        """
        Check if IP address is listed in configured RBL services.

        Args:
            tx: Transaction context
            value: IP address to check

        Returns:
            bool: True if IP is found in any RBL
        """
        import ipaddress  # noqa: PLC0415 - Avoids circular import
        import socket  # noqa: PLC0415 - Avoids circular import

        try:
            # Validate IP address format
            ip_addr = ipaddress.ip_address(value.strip())

            # Skip private/local IP addresses - they won't be in public RBLs
            if ip_addr.is_private or ip_addr.is_loopback or ip_addr.is_reserved:
                return False

            # Reverse the IP address for DNS lookup
            # e.g., 192.168.1.1 becomes 1.1.168.192
            ip_parts = str(ip_addr).split(".")
            reversed_ip = ".".join(reversed(ip_parts))

            # Check each configured RBL
            for rbl_host in self._rbl_hosts:
                rbl_query = f"{reversed_ip}.{rbl_host}"

                try:
                    # Perform DNS lookup - if it resolves, IP is blacklisted
                    result = socket.gethostbyname(rbl_query)

                    # Most RBLs return 127.0.0.x for positive matches
                    if result.startswith("127.0.0."):
                        # Log which RBL triggered
                        tx.variables.tx.add("RBL_MATCH", rbl_host)
                        tx.variables.tx.add("RBL_RESULT", result)
                        return True

                except socket.gaierror:
                    # DNS lookup failed - IP not in this RBL
                    continue
                except Exception:
                    # Other DNS errors - skip this RBL
                    continue

            return False

        except ValueError:
            # Invalid IP address format
            return False
