#!/usr/bin/env python3
"""
Cypher Parser Module

A complete openCypher parser with configurable compatibility modes.

Location: grammars/cypher_parser.py
Grammar:  grammars/lark_grammars/cypher_from_grammars_v4.lark

Usage:
    from cypher_parser import CypherParser, CompatibilityMode

    # Strict mode (openCypher TCK compliant)
    parser = CypherParser(mode=CompatibilityMode.STRICT)
    tree = parser.parse("MATCH (n) RETURN n")

    # Lenient mode (libcypher-parser compatible, error recovery)
    parser = CypherParser(mode=CompatibilityMode.LENIENT)
    tree = parser.parse("foo")  # Accepts invalid input gracefully

Semantic Validation:
    The parser only handles SYNTAX. Semantic validation (type checking,
    undefined variables, numeric overflow, etc.) should be done by a
    separate semantic analyzer after parsing.

    Example semantic errors (not caught by parser):
    - RETURN 9223372036854775808  (integer overflow)
    - MATCH () RETURN foo         (undefined variable)
    - RETURN 1 + "string"         (type mismatch)
"""

from enum import Enum, auto
from pathlib import Path
from typing import Optional, Union, List, Dict, Any
from dataclasses import dataclass
import lark
from lark import Lark, Tree, Token


class CompatibilityMode(Enum):
    """Parser compatibility modes."""
    STRICT = auto()      # openCypher TCK compliant - rejects invalid syntax
    LENIENT = auto()     # libcypher-parser compatible - error recovery
    NEO4J = auto()       # Neo4j extensions (same as STRICT for now)


@dataclass
class ParseResult:
    """Result of parsing a Cypher query."""
    success: bool
    tree: Optional[Tree] = None
    error: Optional[str] = None
    error_line: Optional[int] = None
    error_column: Optional[int] = None
    recovered: bool = False  # True if error recovery was used


class CypherParser:
    """
    Configurable Cypher parser with multiple compatibility modes.

    Attributes:
        mode: The compatibility mode (STRICT, LENIENT, NEO4J)
        grammar_path: Path to the Lark grammar file
    """

    # Grammar file location relative to this module
    GRAMMAR_DIR = Path(__file__).parent / 'lark_grammars'
    STRICT_GRAMMAR = GRAMMAR_DIR / 'cypher_from_grammars_v4.lark'

    def __init__(self, mode: CompatibilityMode = CompatibilityMode.STRICT):
        """
        Initialize the parser with specified compatibility mode.

        Args:
            mode: CompatibilityMode.STRICT for TCK compliance,
                  CompatibilityMode.LENIENT for error recovery
        """
        self.mode = mode
        self._strict_parser = None
        self._lenient_parser = None
        self._load_parsers()

    def _load_parsers(self):
        """Load the Lark parsers."""
        grammar_text = self.STRICT_GRAMMAR.read_text()

        # Strict parser - standard grammar
        self._strict_parser = Lark(
            grammar_text,
            parser='earley',
            start='start'
        )

        # Lenient parser - modified grammar for error recovery
        lenient_grammar = self._create_lenient_grammar(grammar_text)
        self._lenient_parser = Lark(
            lenient_grammar,
            parser='earley',
            start='start'
        )

    def _create_lenient_grammar(self, grammar: str) -> str:
        """
        Modify grammar for lenient/error-recovery mode (libcypher-parser compatible).

        Adds support for:
        - Bare identifiers as statements (error recovery)
        - Unclosed strings
        - Multiple empty statements (;;)
        - Backslash line continuations in client commands
        - YIELD - (void marker)
        - Client command arguments with special chars (=, quotes)
        """
        # Add error recovery rules before lexer rules
        error_recovery_rules = '''
// === ERROR RECOVERY RULES (Lenient Mode - libcypher-parser compatible) ===

// Allow bare identifiers and expressions as error recovery
error_statement: error_expression
error_expression: /[^;]+/

// Allow unclosed strings (libcypher recovers gracefully)
unclosed_string: /"[^"]*$/ | /'[^']*$/

// YIELD void marker (YIELD - means void/no return)
yield_void: SUB

'''
        # Modify the script rule to allow error recovery and empty statements
        modified_grammar = grammar.replace(
            'script: statement_list | empty_input',
            '''script: lenient_statement_list | empty_input
lenient_statement_list: lenient_statement (SEMI lenient_statement)* SEMI*
lenient_statement: statement | error_statement | '''
        )

        # Add unclosed string support to literals
        modified_grammar = modified_grammar.replace(
            'string_lit: STRING_LITERAL',
            'string_lit: STRING_LITERAL | unclosed_string'
        )

        # Allow YIELD - (void marker) in standalone_call
        modified_grammar = modified_grammar.replace(
            'standalone_call: CALL invocation_name paren_expression_chain? (YIELD (MULT | yield_items))? where? return_st?',
            'standalone_call: CALL invocation_name paren_expression_chain? (YIELD (MULT | yield_items | yield_void))? where? return_st?'
        )

        # Enhance client_command to allow special characters in arguments
        modified_grammar = modified_grammar.replace(
            'client_command: COLON ID (ID | STRING_LITERAL)*',
            'client_command: COLON ID client_arg*\nclient_arg: /[^;\\n]+/'
        )

        # Add the error recovery rules
        modified_grammar = modified_grammar.replace(
            '// === Lexer Rules (Terminals) ===',
            error_recovery_rules + '\n// === Lexer Rules (Terminals) ==='
        )

        return modified_grammar

    def parse(self, query: str) -> ParseResult:
        """
        Parse a Cypher query.

        Args:
            query: The Cypher query string

        Returns:
            ParseResult with success status, tree, and any errors
        """
        if self.mode == CompatibilityMode.LENIENT:
            return self._parse_lenient(query)
        else:
            return self._parse_strict(query)

    def _parse_strict(self, query: str) -> ParseResult:
        """Parse with strict mode - no error recovery."""
        try:
            tree = self._strict_parser.parse(query)
            return ParseResult(success=True, tree=tree)
        except lark.exceptions.UnexpectedInput as e:
            return ParseResult(
                success=False,
                error=str(e),
                error_line=getattr(e, 'line', None),
                error_column=getattr(e, 'column', None)
            )
        except Exception as e:
            return ParseResult(success=False, error=str(e))

    def _parse_lenient(self, query: str) -> ParseResult:
        """Parse with lenient mode - attempt error recovery."""
        # First try strict parsing
        strict_result = self._parse_strict(query)
        if strict_result.success:
            return strict_result

        # Try lenient parsing with error recovery
        try:
            tree = self._lenient_parser.parse(query)
            return ParseResult(success=True, tree=tree, recovered=True)
        except lark.exceptions.UnexpectedInput as e:
            # Even lenient mode failed - return the original strict error
            return ParseResult(
                success=False,
                error=strict_result.error,
                error_line=strict_result.error_line,
                error_column=strict_result.error_column
            )
        except Exception as e:
            return ParseResult(success=False, error=str(e))

    def validate_syntax(self, query: str) -> bool:
        """
        Quick syntax validation without returning the tree.

        Args:
            query: The Cypher query string

        Returns:
            True if syntax is valid, False otherwise
        """
        return self.parse(query).success

    @classmethod
    def get_grammar_path(cls) -> Path:
        """Get the path to the grammar file for integration."""
        return cls.STRICT_GRAMMAR


class SemanticValidator:
    """
    Semantic validator for parsed Cypher queries.

    This class validates semantic constraints that the parser doesn't check:
    - Integer overflow (numbers too large)
    - Undefined variable references
    - Type mismatches
    - Invalid function calls

    Usage:
        parser = CypherParser()
        result = parser.parse("RETURN 9223372036854775808")
        if result.success:
            validator = SemanticValidator()
            errors = validator.validate(result.tree)
    """

    # Integer bounds for Neo4j/Cypher
    MIN_INT = -9223372036854775808  # -2^63
    MAX_INT = 9223372036854775807   # 2^63 - 1

    def validate(self, tree: Tree) -> List[str]:
        """
        Validate semantic constraints on a parsed tree.

        Args:
            tree: The Lark parse tree

        Returns:
            List of semantic error messages (empty if valid)
        """
        errors = []
        errors.extend(self._check_integer_bounds(tree))
        # Add more semantic checks here as needed
        return errors

    def _check_integer_bounds(self, tree: Tree) -> List[str]:
        """Check for integer overflow/underflow."""
        errors = []
        for token in tree.scan_values(lambda v: isinstance(v, Token)):
            if token.type in ('DIGIT', 'DIGITS'):
                try:
                    value = int(token.value)
                    if value > self.MAX_INT:
                        errors.append(f"Integer overflow: {token.value} exceeds maximum value")
                    elif value < self.MIN_INT:
                        errors.append(f"Integer underflow: {token.value} below minimum value")
                except ValueError:
                    pass  # Not a simple integer
        return errors


# Convenience functions
def parse_cypher(query: str, mode: CompatibilityMode = CompatibilityMode.STRICT) -> ParseResult:
    """
    Parse a Cypher query with the specified mode.

    Args:
        query: The Cypher query string
        mode: Compatibility mode (default: STRICT)

    Returns:
        ParseResult
    """
    parser = CypherParser(mode=mode)
    return parser.parse(query)


def validate_cypher(query: str) -> bool:
    """
    Quick syntax validation for a Cypher query.

    Args:
        query: The Cypher query string

    Returns:
        True if valid syntax, False otherwise
    """
    return CypherParser().validate_syntax(query)


# For nxcypher integration
def get_grammar_for_nxcypher() -> str:
    """
    Get the grammar text for embedding in nxcypher.

    Returns:
        The complete Lark grammar as a string
    """
    return CypherParser.STRICT_GRAMMAR.read_text()


if __name__ == '__main__':
    # Demo / test
    import sys

    print("Cypher Parser Demo")
    print("=" * 60)
    print(f"Grammar location: {CypherParser.get_grammar_path()}")
    print()

    # Test queries
    tests = [
        ("MATCH (n) RETURN n", "Valid query"),
        ("RETURN 1 + 2", "Simple expression"),
        ("foo", "Bare identifier (invalid in strict, recovered in lenient)"),
        ("RETURN 'unclosed", "Unclosed string"),
        ("return 1; return 2", "Multiple statements"),
    ]

    strict_parser = CypherParser(CompatibilityMode.STRICT)
    lenient_parser = CypherParser(CompatibilityMode.LENIENT)

    for query, desc in tests:
        print(f"Query: {query!r}")
        print(f"  Description: {desc}")

        strict = strict_parser.parse(query)
        lenient = lenient_parser.parse(query)

        print(f"  STRICT:  {'✓ PASS' if strict.success else '✗ FAIL'}")
        print(f"  LENIENT: {'✓ PASS' if lenient.success else '✗ FAIL'}" +
              (' (recovered)' if lenient.recovered else ''))
        print()
