#!/usr/bin/env python3
"""
HTMX Template Validator

Validates Thymeleaf templates for common HTMX issues:
- Missing CSRF tokens in forms with hx-post/hx-put/hx-delete/hx-patch
- Invalid hx-target selectors
- Invalid hx-swap values
- Missing hx-trigger on elements with hx-get
- Incorrect hx-vals syntax
- Missing required attributes

Usage:
    python validate_htmx.py <template_directory>
    python validate_htmx.py templates/
    python validate_htmx.py src/main/resources/templates/
"""

import re
import sys
from pathlib import Path
from typing import List, Tuple

# Valid hx-swap values
VALID_SWAP_VALUES = {
    'innerHTML', 'outerHTML', 'beforebegin', 'afterbegin',
    'beforeend', 'afterend', 'delete', 'none'
}

# Valid hx-swap modifiers
VALID_SWAP_MODIFIERS = {
    'transition', 'swap', 'settle', 'scroll', 'show',
    'focus-scroll', 'ignoreTitle'
}

# HTTP methods that modify data (require CSRF)
CSRF_REQUIRED_METHODS = {'hx-post', 'hx-put', 'hx-delete', 'hx-patch'}

# HTMX attributes that require targets
TARGET_REQUIRED_ATTRS = {'hx-get', 'hx-post', 'hx-put', 'hx-delete', 'hx-patch'}


class ValidationError:
    def __init__(self, file_path: str, line_num: int, message: str, line_content: str):
        self.file_path = file_path
        self.line_num = line_num
        self.message = message
        self.line_content = line_content.strip()

    def __str__(self):
        return f"{self.file_path}:{self.line_num} - {self.message}\n  {self.line_content}"


def validate_csrf_token(line: str, line_num: int, file_path: str, 
                       has_csrf_meta: bool, has_csrf_input: bool) -> List[ValidationError]:
    """Check if forms with HTMX POST/PUT/DELETE/PATCH have CSRF protection"""
    errors = []
    
    # Check for HTMX methods that modify data
    for method in CSRF_REQUIRED_METHODS:
        if method in line.lower():
            # Check if form element
            if '<form' in line.lower():
                # CSRF should be in meta tags OR hidden input
                if not has_csrf_meta and not has_csrf_input:
                    errors.append(ValidationError(
                        file_path, line_num,
                        f"Form with {method} missing CSRF protection (no meta tags or hidden input found)",
                        line
                    ))
            else:
                # Non-form HTMX request (button, div, etc.) - should have meta tags
                if not has_csrf_meta:
                    errors.append(ValidationError(
                        file_path, line_num,
                        f"Element with {method} requires CSRF meta tags in layout",
                        line
                    ))
    
    return errors


def validate_target_selector(line: str, line_num: int, file_path: str) -> List[ValidationError]:
    """Validate hx-target selector syntax"""
    errors = []
    
    # Find hx-target attributes
    target_match = re.search(r'hx-target=["\']([^"\']+)["\']', line)
    if target_match:
        selector = target_match.group(1)
        
        # Check for common CSS selector patterns
        valid_patterns = [
            r'^#[\w-]+$',              # ID selector: #my-id
            r'^\.[\w-]+$',             # Class selector: .my-class
            r'^\w+$',                  # Element selector: div
            r'^this$',                 # this keyword
            r'^closest\s+',            # closest parent: closest tr
            r'^find\s+',               # find descendant: find .item
            r'^next\s+',               # next sibling: next .row
            r'^previous\s+',           # previous sibling: previous div
            r'^\[[\w-]+',              # Attribute selector: [data-id]
        ]
        
        is_valid = any(re.match(pattern, selector) for pattern in valid_patterns)
        
        if not is_valid:
            errors.append(ValidationError(
                file_path, line_num,
                f"Suspicious hx-target selector: '{selector}' - verify CSS selector syntax",
                line
            ))
    
    return errors


def validate_swap_value(line: str, line_num: int, file_path: str) -> List[ValidationError]:
    """Validate hx-swap values"""
    errors = []
    
    # Find hx-swap attributes
    swap_match = re.search(r'hx-swap=["\']([^"\']+)["\']', line)
    if swap_match:
        swap_value = swap_match.group(1)
        
        # Split by space (modifiers)
        parts = swap_value.split()
        main_value = parts[0]
        
        # Check main swap value
        if main_value not in VALID_SWAP_VALUES:
            errors.append(ValidationError(
                file_path, line_num,
                f"Invalid hx-swap value: '{main_value}' (valid: {', '.join(sorted(VALID_SWAP_VALUES))})",
                line
            ))
        
        # Check modifiers
        for i, part in enumerate(parts[1:], 1):
            modifier_name = part.split(':')[0]
            if modifier_name not in VALID_SWAP_MODIFIERS:
                errors.append(ValidationError(
                    file_path, line_num,
                    f"Invalid hx-swap modifier: '{modifier_name}' (valid: {', '.join(sorted(VALID_SWAP_MODIFIERS))})",
                    line
                ))
    
    return errors


def validate_vals_syntax(line: str, line_num: int, file_path: str) -> List[ValidationError]:
    """Validate hx-vals JSON syntax"""
    errors = []
    
    # Find hx-vals attributes
    vals_match = re.search(r'hx-vals=["\']([^"\']+)["\']', line)
    if vals_match:
        vals_value = vals_match.group(1)
        
        # Check for common mistakes
        if not vals_value.startswith('{'):
            errors.append(ValidationError(
                file_path, line_num,
                "hx-vals should contain JSON object starting with '{'",
                line
            ))
        
        # Check for unescaped quotes (common in dynamic attributes)
        if 'hx:vals' not in line and '"' in vals_value and '\\"' not in vals_value:
            # If using static hx-vals (not hx:vals), check for proper escaping
            if vals_value.count('"') > 2:  # More than just outer quotes
                errors.append(ValidationError(
                    file_path, line_num,
                    "hx-vals may have unescaped quotes - use hx:vals for dynamic JSON",
                    line
                ))
    
    return errors


def validate_trigger(line: str, line_num: int, file_path: str) -> List[ValidationError]:
    """Validate hx-trigger syntax"""
    errors = []
    
    # Find elements with hx-get but no hx-trigger (hx-get needs explicit trigger)
    if 'hx-get' in line.lower() or 'hx:get' in line.lower():
        has_trigger = 'hx-trigger' in line.lower() or 'hx:trigger' in line.lower()
        
        # Check if it's a form (forms have implicit submit trigger)
        is_form = '<form' in line.lower()
        
        # Check if it's an input (inputs may have implicit change trigger)
        is_input = '<input' in line.lower() or '<select' in line.lower() or '<textarea' in line.lower()
        
        if not has_trigger and not is_form and not is_input:
            # Warning only - hx-get on links/buttons defaults to 'click'
            errors.append(ValidationError(
                file_path, line_num,
                "hx-get without explicit hx-trigger (defaults to 'click' on buttons/links)",
                line
            ))
    
    return errors


def validate_oob_swap(line: str, line_num: int, file_path: str) -> List[ValidationError]:
    """Validate hx-swap-oob syntax"""
    errors = []
    
    oob_match = re.search(r'hx-swap-oob=["\']([^"\']+)["\']', line)
    if oob_match:
        oob_value = oob_match.group(1)
        
        # Valid patterns: true, outerHTML, innerHTML, etc.
        if oob_value == 'true':
            # Must have an ID attribute for OOB swap
            if 'id=' not in line and 'th:id=' not in line:
                errors.append(ValidationError(
                    file_path, line_num,
                    "hx-swap-oob='true' requires element to have an 'id' attribute",
                    line
                ))
        elif oob_value not in VALID_SWAP_VALUES:
            # Check if it's a swap strategy with selector
            parts = oob_value.split(':', 1)
            if len(parts) == 2 and parts[0] in VALID_SWAP_VALUES:
                # Valid: innerHTML:#target
                pass
            else:
                errors.append(ValidationError(
                    file_path, line_num,
                    f"Invalid hx-swap-oob value: '{oob_value}'",
                    line
                ))
    
    return errors


def validate_file(file_path: Path) -> List[ValidationError]:
    """Validate a single Thymeleaf template file"""
    errors = []
    
    try:
        content = file_path.read_text(encoding='utf-8')
        lines = content.split('\n')
        
        # Check for CSRF protection in file
        has_csrf_meta = 'name="_csrf"' in content or 'name=\'_csrf\'' in content
        has_csrf_input = 'th:name="${_csrf.parameterName}"' in content or 'name="${_csrf.parameterName}"' in content
        
        for line_num, line in enumerate(lines, start=1):
            # Skip comments
            if '<!--' in line or '-->' in line:
                continue
            
            # Run validators
            errors.extend(validate_csrf_token(line, line_num, str(file_path), 
                                            has_csrf_meta, has_csrf_input))
            errors.extend(validate_target_selector(line, line_num, str(file_path)))
            errors.extend(validate_swap_value(line, line_num, str(file_path)))
            errors.extend(validate_vals_syntax(line, line_num, str(file_path)))
            errors.extend(validate_trigger(line, line_num, str(file_path)))
            errors.extend(validate_oob_swap(line, line_num, str(file_path)))
    
    except Exception as e:
        errors.append(ValidationError(
            str(file_path), 0,
            f"Error reading file: {e}",
            ""
        ))
    
    return errors


def validate_directory(dir_path: Path) -> List[ValidationError]:
    """Validate all Thymeleaf templates in directory"""
    errors = []
    
    # Find all .html files
    html_files = list(dir_path.rglob('*.html'))
    
    if not html_files:
        print(f"No HTML files found in {dir_path}")
        return errors
    
    print(f"Validating {len(html_files)} template files in {dir_path}...\n")
    
    for file_path in html_files:
        file_errors = validate_file(file_path)
        errors.extend(file_errors)
    
    return errors


def main():
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(1)
    
    target_path = Path(sys.argv[1])
    
    if not target_path.exists():
        print(f"Error: Path does not exist: {target_path}")
        sys.exit(1)
    
    # Validate single file or directory
    if target_path.is_file():
        errors = validate_file(target_path)
    else:
        errors = validate_directory(target_path)
    
    # Print results
    if errors:
        print("VALIDATION ERRORS FOUND:\n")
        for error in errors:
            print(error)
            print()
        
        print(f"\nTotal errors: {len(errors)}")
        sys.exit(1)
    else:
        print("✓ All templates validated successfully!")
        sys.exit(0)


if __name__ == '__main__':
    main()
