#!/usr/bin/env python3
"""Find SQL queries mixing named and positional parameters in PHP files."""
import re
import os
import glob

dirs = ['api/', 'classes/', 'services/']
results = []

for d in dirs:
    for filepath in glob.glob(os.path.join(d, '**', '*.php'), recursive=True):
        try:
            with open(filepath, 'r', errors='replace') as f:
                content = f.read()
        except Exception:
            continue

        lines = content.split('\n')
        total_lines = len(lines)

        # APPROACH 1: Look at the whole file content for SQL strings
        # Find all SQL string assignments and prepare/query calls
        # Use regex to find multi-line strings between quotes
        
        # Find all quoted strings that look like SQL (may span lines with concatenation)
        # Pattern: capture everything between matching quotes in SQL context
        
        # First find all $var = "..." or prepare("...") blocks
        # Including multi-line with string concatenation
        
        # Simpler approach: for each file, find all occurrences of execute([...])
        # and check if the SQL and params are mixed
        
        # APPROACH: Read entire file, find SQL strings, check for mixed params
        # Look for patterns like: "SELECT ... ? ... :param ..."
        
        # Find all string literals that contain SQL keywords
        # Handle both single-line and multi-line concatenated strings
        
        for i, line in enumerate(lines):
            # Skip comment lines
            stripped = line.strip()
            if stripped.startswith('//') or stripped.startswith('*') or stripped.startswith('#'):
                continue
            
            # Look for SQL query assignments or prepare/query calls on this line
            is_sql_line = bool(
                re.search(r'(prepare|query|exec)\s*\(', line, re.IGNORECASE) or
                re.search(r'\$\w*(sql|query)\w*\s*=', line, re.IGNORECASE)
            )
            
            if not is_sql_line:
                continue
            
            # Gather block: from this line until statement seems complete
            block_lines_list = []
            j = i
            brace_depth = 0
            while j < total_lines and j < i + 50:
                bl = lines[j]
                block_lines_list.append(bl)
                brace_depth += bl.count('(') - bl.count(')')
                # End conditions
                if j > i and brace_depth <= 0 and ';' in bl:
                    break
                j += 1
            
            block = '\n'.join(block_lines_list)
            
            # Now extract ALL string content from this block
            # Handle concatenated strings like "part1" . "part2"
            # and "part1 
            #   part2" (multi-line strings)
            all_strings = []
            # Double-quoted strings (handle escaped quotes)
            all_strings.extend(re.findall(r'"((?:[^"\\]|\\.)*)"', block))
            # Single-quoted strings
            all_strings.extend(re.findall(r"'((?:[^'\\]|\\.)*)'", block))
            
            full_sql = ' '.join(all_strings)
            
            # Must contain SQL keywords
            if not re.search(r'\b(SELECT|INSERT|UPDATE|DELETE|REPLACE\s+INTO)\b', full_sql, re.IGNORECASE):
                continue
            
            # Check for positional (?) - but not in ternary context
            # In SQL strings, ? should appear as a standalone placeholder
            has_positional = bool(re.search(r'\?', full_sql))
            
            # Check for named params - :word but not :: or :// or :\ 
            named_in_sql = re.findall(r'(?<![:\w/]):[a-zA-Z_][a-zA-Z0-9_]*', full_sql)
            # Filter out things that are clearly not SQL params (like :hover, :root from CSS)
            named_in_sql = [n for n in named_in_sql if n.lower() not in (':hover', ':root', ':focus', ':active', ':visited')]
            has_named = len(named_in_sql) > 0
            
            if has_positional and has_named:
                # Extra validation: make sure ? is in the SQL part, not just in error messages etc.
                # Check each individual string for SQL content
                sql_strings = [s for s in all_strings if re.search(r'\b(SELECT|INSERT|UPDATE|DELETE|WHERE|SET|FROM|INTO|VALUES)\b', s, re.IGNORECASE)]
                combined_sql = ' '.join(sql_strings)
                
                has_pos_in_sql = bool(re.search(r'\?', combined_sql))
                named_in_real_sql = re.findall(r'(?<![:\w/]):[a-zA-Z_][a-zA-Z0-9_]*', combined_sql)
                named_in_real_sql = [n for n in named_in_real_sql if n.lower() not in (':hover', ':root', ':focus', ':active', ':visited')]
                
                if has_pos_in_sql and named_in_real_sql:
                    results.append({
                        'file': filepath,
                        'start_line': i + 1,
                        'end_line': i + len(block_lines_list),
                        'named_params': named_in_real_sql,
                        'block': block.strip(),
                        'sql_text': combined_sql[:500]
                    })

# Deduplicate (same file + same start line)
seen = set()
unique_results = []
for r in results:
    key = (r['file'], r['start_line'])
    if key not in seen:
        seen.add(key)
        unique_results.append(r)

# Output results
if not unique_results:
    print("No mixed parameter queries found.")
else:
    for r in unique_results:
        print("=" * 80)
        print(f"FILE: {r['file']}")
        print(f"LINES: {r['start_line']}-{r['end_line']}")
        print(f"NAMED PARAMS: {r['named_params']}")
        print(f"SQL TEXT: {r['sql_text'][:400]}")
        print(f"CODE BLOCK:")
        for bl in r['block'].split('\n'):
            print(f"  {bl}")
        print()

    print(f"TOTAL: {len(unique_results)} potential mixed parameter queries found.")
