"""
SPARQL property path representation and validation.

Property paths allow traversing multiple relationship levels and using
path operators for complex SPARQL queries.
"""

import re

from ..security import (
    DANGEROUS_PATH_KEYWORDS,
    escape_sparql_iri,
    validate_no_sparql_keywords,
)
from .constants import _PROPERTY_PATH_ALLOWED_CHARS_PATTERN


class PropertyPath:
    """
    Represents a SPARQL property path for relationship traversal.

    Property paths allow traversing multiple relationship levels and using
    path operators like zero-or-more (*), one-or-more (+), zero-or-one (?),
    alternative paths (|), and inverse paths (^).

    **IRI Expansion**: Property paths are NOT automatically expanded by the
    session's prefix registry. You must use the exact form you want in the
    generated SPARQL query:
    - Short-form: `PropertyPath('schema:knows+')` uses prefix as-is
    - Full IRI: `PropertyPath('<http://schema.org/knows>+')` uses full IRI

    **Supported Operators**:
        - ``/`` (sequence): Path concatenation, e.g., ``schema:worksFor/schema:name``
        - ``+`` (one-or-more): Transitive closure, e.g., ``schema:knows+``
        - ``*`` (zero-or-more): Reflexive transitive, e.g., ``org:reportsTo*``
        - ``?`` (zero-or-one): Optional single step, e.g., ``schema:nickname?``
        - ``|`` (alternative): Either path, e.g., ``rdfs:label|schema:name``
        - ``^`` (inverse): Reverse direction, e.g., ``^schema:child``
        - ``()`` (grouping): Group path expressions, e.g.,
          ``(schema:knows|schema:friend)+``

    **Inverse Paths (^)**:
        The inverse operator ``^`` traverses a predicate in the reverse direction.
        While a normal pattern ``?s <predicate> ?o`` finds objects of subjects,
        an inverse pattern ``?s ^<predicate> ?o`` finds subjects where the object
        points to them via the predicate.

        This is useful for Wikidata-style patterns where you need to find a
        resource through an inverse relationship:

        .. code-block:: python

            from typing import Annotated
            from sparqlmojo import Model, IRIField, PropertyPath, SubjectField

            class Child(Model):
                iri: Annotated[str, SubjectField()]
                # Find parent by traversing parent->child in reverse
                parent: Annotated[str | None, IRIField(
                    PropertyPath("^<http://schema.org/children>")
                )] = None

            # Query generates: ?s ^<http://schema.org/children> ?parent .
            # Which is equivalent to: ?parent <http://schema.org/children> ?s .

    Examples:
        - ``'schema:worksFor/schema:name'`` - sequence path (organization then name)
        - ``'schema:knows+'`` - one or more knows relationships (transitive)
        - ``'org:reportsTo*'`` - zero or more reportsTo relationships
        - ``'schema:parent|schema:guardian'`` - alternative paths
        - ``'^schema:child'`` - inverse path (find parents via child relationship)
        - ``'(schema:knows|schema:friend)+'`` - grouped alternative with transitive
        - ``'<http://schema.org/knows>+'`` - full IRI with transitive operator
        - ``'^<http://wikiba.se/ontology#claim>'`` - inverse path with full IRI
    """

    OPERATORS: frozenset[str] = frozenset({"^", "*", "+", "?", "|"})

    def __init__(self, path: str):
        """
        Initialize property path.

        Args:
            path: SPARQL property path string (e.g., 'schema:knows+', 'org:reportsTo*')

        Raises:
            ValueError: If path is empty or contains invalid characters
            SPARQLInjectionError: If path contains dangerous SPARQL keywords
        """
        if not path or not path.strip():
            raise ValueError("Property path cannot be empty")

        # Validate for SPARQL injection
        self._validate_property_path_security(path)

        self._path = path

    @property
    def path(self) -> str:
        """Get the property path string (read-only)."""
        return self._path

    def _validate_property_path_security(self, path: str) -> None:
        """
        Validate property path for security.

        Allows valid property path operators but rejects dangerous SPARQL keywords.

        Args:
            path: Property path string to validate

        Raises:
            ValueError: If path contains invalid characters
            SPARQLInjectionError: If path contains dangerous SPARQL keywords
        """
        self._validate_no_dangerous_keywords(path)
        self._validate_allowed_characters(path)

    def _validate_no_dangerous_keywords(self, path: str) -> None:
        """
        Check for dangerous SPARQL keywords in property path.

        Uses word boundary matching to avoid false positives with legitimate IRIs
        that contain keywords as substrings.

        Args:
            path: Property path string to validate

        Raises:
            SPARQLInjectionError: If dangerous SPARQL keywords are detected
        """
        validate_no_sparql_keywords(path, DANGEROUS_PATH_KEYWORDS, "Property path")

    def _validate_allowed_characters(self, path: str) -> None:
        """
        Validate property path character set.

        Property paths should contain:
        - IRI characters (letters, numbers, :, /, #, -, _, .)
        - Angle brackets for full IRIs (<, >)
        - Property path operators (*, +, ?, |, ^, /, (, ))
        - Limited whitespace

        Args:
            path: Property path string to validate

        Raises:
            ValueError: If path contains invalid characters
        """
        if not re.match(_PROPERTY_PATH_ALLOWED_CHARS_PATTERN, path):
            raise ValueError(
                f"Property path contains invalid characters: {path}. "
                f"Allowed: IRI characters, *, +, ?, |, ^, /, (), "
                f"and limited whitespace."
            )

    def to_sparql(self) -> str:
        """
        Return SPARQL representation of property path.

        Returns:
            SPARQL property path string ready for use in queries
        """
        return self.path

    @staticmethod
    def format_predicate(predicate: str) -> str:
        """
        Format a predicate IRI for use in a property path expression.

        This function wraps full IRIs in angle brackets while leaving
        prefixed forms (e.g., schema:knows) unchanged.

        Rules:
        - Full IRI (contains "://"): wrap in <...>
        - Already wrapped (starts with "<"): return as-is
        - Prefixed form (contains ":"): return as-is

        Args:
            predicate: The predicate IRI to format

        Returns:
            Formatted predicate string suitable for property path expressions

        Examples:
            >>> PropertyPath.format_predicate("http://schema.org/knows")
            '<http://schema.org/knows>'
            >>> PropertyPath.format_predicate("schema:knows")
            'schema:knows'
            >>> PropertyPath.format_predicate("<http://schema.org/knows>")
            '<http://schema.org/knows>'
        """
        # Already wrapped in angle brackets
        if predicate.startswith("<") and predicate.endswith(">"):
            return predicate

        # Full IRI (contains "://") - needs wrapping
        if "://" in predicate:
            escaped: str = escape_sparql_iri(predicate)
            return f"<{escaped}>"

        # Prefixed form or other - return as-is
        return predicate

    @staticmethod
    def has_operators(predicate: str) -> bool:
        """
        Check if a predicate string contains property path operators.

        Property path operators (^, *, +, ?, |) should not be wrapped in angle
        brackets. This function detects their presence to avoid double-wrapping.

        Args:
            predicate: The predicate string to check

        Returns:
            True if the predicate contains any property path operators
        """
        return any(op in predicate for op in PropertyPath.OPERATORS)


__all__ = ["PropertyPath"]
