"""
SPARQL security utilities for preventing injection attacks.

This module provides functions for escaping SPARQL literals, validating operators,
and sanitizing IRIs to prevent SPARQL injection vulnerabilities.
"""

import re
import urllib.parse
from enum import Enum
from typing import Any

from ..exc.exceptions import IRIError

# Categorized SPARQL keywords for security checking

# Mutation keywords - dangerous when combined with injection indicators (; { })
MUTATION_KEYWORDS: frozenset[str] = frozenset(
    ["DROP", "DELETE", "INSERT", "CREATE", "CLEAR", "LOAD"]
)

# Query keywords
QUERY_KEYWORDS: frozenset[str] = frozenset(
    [
        "SELECT",
        "CONSTRUCT",
        "ASK",
        "DESCRIBE",
        "WHERE",
        "FILTER",
        "OPTIONAL",
        "UNION",
        "GRAPH",
        "SERVICE",
        "BIND",
        "MINUS",
        "GROUP",
        "HAVING",
        "ORDER",
        "LIMIT",
        "OFFSET",
        "DISTINCT",
        "REDUCED",
        "AS",
        "FROM",
        "NAMED",
        "PREFIX",
        "BASE",
        "COPY",
        "MOVE",
        "ADD",
        "WITH",
        "USING",
        "VALUES",
        "UNDEF",
    ]
)

# Function keywords
FUNCTION_KEYWORDS: frozenset[str] = frozenset(
    [
        "BOUND",
        "IF",
        "COALESCE",
        "STR",
        "LANG",
        "LANGMATCHES",
        "DATATYPE",
        "IRI",
        "URI",
        "BNODE",
        "RAND",
        "ABS",
        "CEIL",
        "FLOOR",
        "ROUND",
        "CONCAT",
        "SUBSTR",
        "STRLEN",
        "REPLACE",
        "UCASE",
        "LCASE",
        "ENCODE_FOR_URI",
        "CONTAINS",
        "STRSTARTS",
        "STRENDS",
        "STRBEFORE",
        "STRAFTER",
        "YEAR",
        "MONTH",
        "DAY",
        "HOURS",
        "MINUTES",
        "SECONDS",
        "TIMEZONE",
        "TZ",
        "NOW",
        "UUID",
        "STRUUID",
        "MD5",
        "SHA1",
        "SHA256",
        "SHA384",
        "SHA512",
        "REGEX",
        "EXISTS",
        "NOT",
        "IN",
        "AND",
        "OR",
        "TRUE",
        "FALSE",
        "NULL",
    ]
)

# All SPARQL keywords combined
SPARQL_KEYWORDS: frozenset[str] = MUTATION_KEYWORDS | QUERY_KEYWORDS | FUNCTION_KEYWORDS

# Dangerous keywords for IRIs (mutation + critical query keywords)
DANGEROUS_IRI_KEYWORDS: frozenset[str] = MUTATION_KEYWORDS | frozenset(
    ["SELECT", "WHERE", "FILTER"]
)

# Dangerous keywords for property paths (mutation + all query keywords)
DANGEROUS_PATH_KEYWORDS: frozenset[str] = MUTATION_KEYWORDS | QUERY_KEYWORDS

# Characters that indicate SPARQL syntax/injection attempts
# Note: Quotes and parens are removed as they're valid in escaped literal values
INJECTION_INDICATORS: frozenset[str] = frozenset([";", "{", "}"])


class SPARQLOperator(Enum):
    """Enumeration of safe SPARQL operators and functions."""

    # Comparison operators
    EQUAL = "="
    NOT_EQUAL = "!="
    LESS_THAN = "<"
    GREATER_THAN = ">"
    LESS_THAN_OR_EQUAL = "<="
    GREATER_THAN_OR_EQUAL = ">="

    # String functions
    CONTAINS = "CONTAINS"
    REGEX = "REGEX"
    STRSTARTS = "STRSTARTS"
    STRENDS = "STRENDS"
    STRBEFORE = "STRBEFORE"
    STRAFTER = "STRAFTER"

    # Language and datatype functions
    LANG = "LANG"
    LANGMATCHES = "LANGMATCHES"
    DATATYPE = "DATATYPE"

    # Type checking functions
    BOUND = "BOUND"
    SAMETERM = "SAMETERM"
    ISIRI = "ISIRI"
    ISURI = "ISURI"
    ISBLANK = "ISBLANK"
    ISLITERAL = "ISLITERAL"
    ISNUMERIC = "ISNUMERIC"

    # Set operators
    IN = "IN"
    NOT_IN = "NOT IN"

    @classmethod
    def is_valid(cls, operator: str) -> bool:
        """Check if an operator string is valid."""
        # Check if operator matches any enum value
        for op in cls:
            if op.value == operator:
                return True
        # Also check simple operators with optional whitespace
        simple_op_pattern: str = r"^\s*[!=<>]=?\s*$"
        return bool(re.match(simple_op_pattern, operator))


class SPARQLSecurityError(Exception):
    """Base exception for SPARQL security violations."""

    def __init__(self, message: str, context: dict[str, Any] | None = None):
        """
        Initialize security error with message and optional context.

        Args:
            message: Error message
            context: Optional dictionary with additional context for debugging
        """
        self.context: dict[str, Any] = context or {}
        super().__init__(message)


class SPARQLInjectionError(SPARQLSecurityError):
    """Exception raised when potential SPARQL injection is detected."""

    def __init__(
        self, message: str, value: str | None = None, keyword: str | None = None
    ):
        """
        Initialize injection error with details about the attempted injection.

        Args:
            message: Error message
            value: The value that triggered the injection detection
            keyword: The dangerous keyword that was detected
        """
        context: dict[str, Any] = {}
        if value is not None:
            context["value"] = value
        if keyword is not None:
            context["keyword"] = keyword
        super().__init__(message, context)
        self.value: str | None = value
        self.keyword: str | None = keyword


# Alias for backward compatibility - use IRIError from exc.exceptions
InvalidIRIError = IRIError


class InvalidOperatorError(SPARQLSecurityError):
    """Exception raised for invalid SPARQL operators."""

    def __init__(self, message: str, operator: str | None = None):
        """
        Initialize operator error with the invalid operator.

        Args:
            message: Error message
            operator: The invalid operator that was rejected
        """
        context: dict[str, Any] = {}
        if operator is not None:
            context["operator"] = operator
        super().__init__(message, context)
        self.operator: str | None = operator


def _is_keyword_match(keyword: str, upper_value: str) -> bool:
    """Check if keyword matches as whole word in value."""
    pattern: str = rf"\b{keyword}\b"
    return bool(re.search(pattern, upper_value))


def _looks_like_injection(value: str) -> bool:
    """Check if value contains injection indicators."""
    return any(char in value for char in INJECTION_INDICATORS)


def _validate_no_injection_patterns(value: str) -> None:
    """Validate that value doesn't contain injection patterns."""
    upper_value: str = value.upper()

    for keyword in SPARQL_KEYWORDS:
        if not _is_keyword_match(keyword, upper_value):
            continue

        if _looks_like_injection(value):
            raise SPARQLInjectionError(
                f"Potential SPARQL injection detected: '{value}' contains "
                f"SPARQL keyword '{keyword}'. Use parameterized queries instead.",
                value=value,
                keyword=keyword,
            )


def validate_no_sparql_keywords(
    value: str,
    keywords: frozenset[str],
    context: str = "value",
) -> None:
    """
    Validate that value doesn't contain dangerous SPARQL keywords.

    Uses word boundary matching to avoid false positives with legitimate IRIs
    that contain keywords as substrings (e.g., 'schema:selectProperty' contains
    'SELECT' but is not an injection attempt).

    Args:
        value: String value to validate
        keywords: Set of keywords to check for
        context: Context description for error messages (default: "value")

    Raises:
        SPARQLInjectionError: If dangerous SPARQL keywords are detected as whole words
    """
    upper_value: str = value.upper()

    for keyword in keywords:
        if _is_keyword_match(keyword, upper_value):
            raise SPARQLInjectionError(
                f"{context} contains dangerous SPARQL keyword '{keyword}': {value}",
                value=value,
                keyword=keyword,
            )


def _apply_literal_escaping(value: str) -> str:
    """Apply character-level escaping for SPARQL literals."""
    # Escape backslashes first to avoid double-escaping
    escaped: str = value.replace("\\", "\\\\")

    # Escape quotes (required for SPARQL string literals)
    escaped = escaped.replace('"', '\\"')
    escaped = escaped.replace("'", "\\'")

    # Escape newlines and other control characters
    escaped = escaped.replace("\n", "\\n")
    escaped = escaped.replace("\r", "\\r")
    escaped = escaped.replace("\t", "\\t")

    return escaped


def escape_sparql_literal(value: str) -> str:
    """
    Escape a string literal for safe use in SPARQL queries.

    Args:
        value: The string value to escape

    Returns:
        Escaped string safe for SPARQL literal context

    Raises:
        SPARQLInjectionError: If dangerous SPARQL keywords are detected
    """
    if not isinstance(value, str):
        return str(value)

    # Check for injection attempts
    _validate_no_injection_patterns(value)

    # Perform escaping
    return _apply_literal_escaping(value)


def _validate_iri_type(iri: Any) -> None:
    """Ensure IRI is a string."""
    if not isinstance(iri, str):
        raise IRIError(
            str(iri) if iri else "", f"IRI must be a string, got {type(iri)}"
        )


def _validate_iri_not_empty(iri: str) -> None:
    """Ensure IRI is not empty."""
    if not iri or len(iri.strip()) == 0:
        raise IRIError(iri, "IRI cannot be empty")


def _validate_no_dangerous_keywords_in_iri(iri: str) -> None:
    """Check IRI doesn't contain SPARQL mutation or critical query keywords.

    Uses word boundary matching to avoid false positives on legitimate predicates
    like 'work_created_by_agent' (contains 'created', not standalone 'CREATE').
    """
    upper_iri: str = iri.upper()

    for keyword in DANGEROUS_IRI_KEYWORDS:
        if _is_keyword_match(keyword, upper_iri):
            raise SPARQLInjectionError(
                f"Potential SPARQL injection in IRI: '{iri}' contains "
                f"dangerous keyword '{keyword}'",
                value=iri,
                keyword=keyword,
            )


def _validate_iri_format(iri: str) -> None:
    """Validate IRI structure."""
    # Simple check for scheme://authority/path pattern
    iri_pattern: str = r'^[a-zA-Z][a-zA-Z0-9+.-]*://[^\s"<>\\|{}]+$'

    if re.match(iri_pattern, iri):
        return

    # Allow relative IRIs with angle brackets
    if iri.startswith("<") and iri.endswith(">"):
        return

    # Check for invalid characters in non-standard IRIs
    invalid_chars: list[str] = [" ", "\n", "\r", "\t", '"', "'", "\\"]
    if any(char in iri for char in invalid_chars):
        raise IRIError(
            iri,
            "Invalid IRI format - contains invalid characters. "
            "IRIs should not contain spaces, quotes, or control characters.",
        )


def validate_predicate_iri(predicate: str) -> None:
    """
    Validate that a predicate IRI is complete and not a namespace-only URI.

    Namespace URIs ending with '/' or '#' are meant to be used as prefixes,
    not as predicates directly. Using them as predicates generates invalid
    SPARQL like `OPTIONAL { ?s <http://www.wikidata.org/prop/> ?x . }`.

    Args:
        predicate: The predicate IRI to validate

    Raises:
        IRIError: If predicate is a namespace-only URI (ends with / or #)

    Example:
        >>> validate_predicate_iri("http://schema.org/name")  # Valid
        >>> validate_predicate_iri("http://schema.org/")  # Raises IRIError
    """
    if predicate.endswith("/") or predicate.endswith("#"):
        raise IRIError(
            predicate,
            "Predicate appears to be a namespace URI (ends with '/' or '#'). "
            "Use a complete predicate IRI like 'http://schema.org/name' "
            f"instead of the namespace '{predicate}'",
        )


def validate_iri(iri: str, strict: bool = True) -> None:
    """
    Validate IRI format per RFC 3987, W3C specs, and security.

    Consolidated validation function that combines:
    - Security checks (SPARQL injection prevention)
    - RFC 3987 compliance
    - W3C IRI specifications

    Args:
        iri: The IRI string to validate
        strict: If True, perform strict validation including scheme requirements.
               If False, only check for basic invalid characters and security.

    Raises:
        InvalidIRIError: If IRI format is invalid
        SPARQLInjectionError: If dangerous patterns are detected

    Validates:
        - IRI is a string and not empty
        - No SPARQL injection keywords
        - No invalid characters (spaces, control chars, angle brackets)
        - Proper IRI structure (when strict=True)
        - Valid IRI scheme (http, https, urn, etc. when strict=True)
    """
    # Type and empty validation
    _validate_iri_type(iri)
    _validate_iri_not_empty(iri)

    # Security validation
    _validate_no_dangerous_keywords_in_iri(iri)

    # Character validation
    invalid_chars: list[str] = [" ", "\n", "\r", "\t", "<", ">", '"', "'"]
    if any(invalid_char in iri for invalid_char in invalid_chars):
        invalid_found: list[str] = [char for char in invalid_chars if char in iri]
        raise IRIError(iri, f"IRI contains invalid characters: {invalid_found}")

    # Check for control characters
    if any(ord(char) < 32 for char in iri):
        raise IRIError(iri, "IRI contains control characters")

    if strict:
        # Validate as proper IRI using urllib.parse
        try:
            parsed = urllib.parse.urlparse(iri)

            # Check that scheme is present and valid
            if not parsed.scheme:
                raise IRIError(iri, "IRI must have a scheme (http, https, urn, etc.)")

            # Validate scheme format (should be alphanumeric with optional +, -, .)
            if not re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*$", parsed.scheme):
                raise IRIError(iri, f"Invalid IRI scheme: '{parsed.scheme}'")

            # Check that netloc is present for http/https schemes
            if parsed.scheme in ["http", "https"] and not parsed.netloc:
                raise IRIError(iri, f"{parsed.scheme} IRI must have a network location")

        except (ValueError, AttributeError) as e:
            raise IRIError(iri, f"IRI parsing failed: {str(e)}")


def escape_sparql_iri(iri: str) -> str:
    """
    Validate and escape an IRI for safe use in SPARQL queries.

    Args:
        iri: The IRI string to validate and escape

    Returns:
        Validated and escaped IRI

    Raises:
        InvalidIRIError: If the IRI format is invalid
        SPARQLInjectionError: If dangerous patterns are detected
    """
    # Use basic format validation (not strict RFC compliance)
    _validate_iri_type(iri)
    _validate_iri_not_empty(iri)
    _validate_no_dangerous_keywords_in_iri(iri)
    _validate_iri_format(iri)

    return iri


def validate_sparql_operator(operator: str) -> str:
    """
    Validate that a SPARQL operator is safe and doesn't contain injection patterns.

    Args:
        operator: The operator string to validate

    Returns:
        Validated operator string

    Raises:
        InvalidOperatorError: If the operator is not in the safe whitelist
        SPARQLInjectionError: If dangerous patterns are detected
    """
    if not isinstance(operator, str):
        raise InvalidOperatorError(
            f"Operator must be a string, got {type(operator)}", operator=str(operator)
        )

    # Check for dangerous patterns that could indicate injection
    dangerous_patterns: list[str] = [
        "DROP",
        "DELETE DATA",
        "INSERT DATA",
        "CREATE",
        "CLEAR",
    ]
    upper_op: str = operator.upper()
    for pattern in dangerous_patterns:
        if pattern in upper_op:
            raise SPARQLInjectionError(
                f"Potential SPARQL injection in operator: '{operator}' contains "
                f"dangerous pattern '{pattern}'",
                value=operator,
                keyword=pattern,
            )

    # Check if operator is valid using the enum
    if not SPARQLOperator.is_valid(operator):
        raise InvalidOperatorError(
            f"Unsafe operator: '{operator}'. Only simple comparison operators "
            f"and whitelisted functions are allowed.",
            operator=operator,
        )

    return operator


def format_sparql_value_secure(value: Any) -> str:
    """
    Secure version of format_sparql_value that prevents SPARQL injection.

    Args:
        value: The Python value to format

    Returns:
        Formatted SPARQL string representation (safe)

    Raises:
        SPARQLInjectionError: If potential injection is detected
    """
    if isinstance(value, str):
        # Check if it's a SPARQL variable
        if value.startswith("?") or value.startswith("$"):
            # Validate variable name is safe
            if not is_safe_sparql_variable(value):
                raise SPARQLInjectionError(
                    f"Unsafe SPARQL variable name: '{value}'. "
                    f"Variable names must not contain keywords or invalid characters.",
                    value=value,
                )
            return value
        # Escape string literals
        escaped_value: str = escape_sparql_literal(value)
        return f'"{escaped_value}"'
    return str(value)


def is_safe_sparql_variable(name: str) -> bool:
    """
    Check if a variable name is safe for use in SPARQL queries.

    Args:
        name: The variable name to check

    Returns:
        True if the variable name is safe, False otherwise
    """
    if not isinstance(name, str):
        return False

    # Variable names should start with ? or $
    if not (name.startswith("?") or name.startswith("$")):
        return False

    # Check for SPARQL keywords as whole words only
    variable_name = name[1:]  # Remove ? or $
    upper_name = variable_name.upper()
    # Only reject if variable name IS a keyword, not if it contains one
    if upper_name in SPARQL_KEYWORDS:
        return False

    # Valid variable name pattern
    return bool(re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", variable_name))
