"""
SQL Injection vulnerability check.
"""

import os
import re
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple, Set

import bandit
from bandit.core import manager

from vulnerability_checker.checks.base import BaseCheck
from vulnerability_checker.models import CheckResult, CheckStatus, VulnerabilityIssue


class SQLInjectionCheck(BaseCheck):
    """
    SQL Injection vulnerability check.
    """
    
    name = "sql-injection"
    description = "SQL Injection vulnerability check"
    
    # Patterns for detecting potential SQL injections
    SQL_INJECTION_PATTERNS = [
        # Direct use of string concatenation in queries
        r"execute\s*\(\s*['\"]SELECT .* WHERE .*\s*\+\s*",
        r"execute\s*\(\s*['\"]UPDATE .* SET .*\s*\+\s*",
        r"execute\s*\(\s*['\"]INSERT INTO .* VALUES .*\s*\+\s*",
        r"execute\s*\(\s*['\"]DELETE FROM .* WHERE .*\s*\+\s*",
        
        # String formatting for queries
        r"execute\s*\(\s*f['\"]SELECT .* WHERE .*\{.*\}",
        r"execute\s*\(\s*f['\"]UPDATE .* SET .*\{.*\}",
        r"execute\s*\(\s*f['\"]INSERT INTO .* VALUES .*\{.*\}",
        r"execute\s*\(\s*f['\"]DELETE FROM .* WHERE .*\{.*\}",
        
        # Using % for formatting
        r"execute\s*\(\s*['\"]SELECT .* WHERE .*['\"].*%.*\)",
        r"execute\s*\(\s*['\"]UPDATE .* SET .*['\"].*%.*\)",
        r"execute\s*\(\s*['\"]INSERT INTO .* VALUES .*['\"].*%.*\)",
        r"execute\s*\(\s*['\"]DELETE FROM .* WHERE .*['\"].*%.*\)",
        
        # Using .format()
        r"execute\s*\(\s*['\"]SELECT .* WHERE .*['\"]\.format\(",
        r"execute\s*\(\s*['\"]UPDATE .* SET .*['\"]\.format\(",
        r"execute\s*\(\s*['\"]INSERT INTO .* VALUES .*['\"]\.format\(",
        r"execute\s*\(\s*['\"]DELETE FROM .* WHERE .*['\"]\.format\(",
    ]
    
    # Safe patterns that can be used
    SAFE_PATTERNS = [
        r"execute\s*\(\s*['\"]SELECT .* WHERE .*\s*\%s",  # Using parameterized queries
        r"execute_values\s*\(",  # psycopg2.extras.execute_values
        r"executemany\s*\(",  # executemany for multiple queries
    ]
    
    def __init__(self, config: Optional[Dict[str, Any]] = None):
        super().__init__(config)
        self.severity_map = {
            "HIGH": "high",
            "MEDIUM": "medium",
            "LOW": "low",
        }
    
    def run(self, target_path: str, **kwargs) -> Tuple[CheckResult, List[VulnerabilityIssue]]:
        """
        Run SQL injection check.
        
        Args:
            target_path: Path to directory or file for checking
            **kwargs: Additional parameters
            
        Returns:
            Tuple[CheckResult, List[VulnerabilityIssue]]: Check result and list of found vulnerabilities
        """
        self.issues = []
        
        try:
            # Check using bandit to detect SQL injections
            self._run_bandit_check(target_path)
            
            # Additional check using patterns
            self._check_sql_patterns(target_path)
            
            # Create result
            status = CheckStatus.PASSED if not self.issues else CheckStatus.FAILED
            result = self.create_result(
                status=status,
                message=f"Found {len(self.issues)} potential SQL injections" if self.issues else "No SQL injections detected",
            )
            
            return result, self.issues
            
        except Exception as e:
            # In case of error, return ERROR status
            result = self.create_result(
                status=CheckStatus.ERROR,
                message=f"Error during SQL injection check: {str(e)}",
            )
            return result, self.issues
    
    def _run_bandit_check(self, target_path: str) -> None:
        """
        Run check using bandit.
        
        Args:
            target_path: Path to directory or file for checking
        """
        # Configure bandit
        b_mgr = manager.BanditManager(
            config_file=None,
            agg_type="file",
            debug=False,
            verbose=False,
            quiet=True,
            profile=None,
        )
        
        # Run scanning
        b_mgr.discover_files([target_path])
        b_mgr.run_tests()
        
        # Process results
        for fname, issues in b_mgr.results_by_filename.items():
            for issue in issues:
                # Filter only SQL injections
                if "sql" in issue.test_id.lower() or "sql" in issue.test_name.lower():
                    self.add_issue(
                        issue_type="SQL Injection",
                        severity=self.severity_map.get(issue.severity, "medium"),
                        description=issue.test_text,
                        file_path=fname,
                        line_number=issue.line_number,
                        code_snippet=issue.raw_context,
                        confidence=issue.confidence,
                        cwe_id="CWE-89",
                        owasp_category="A1:2017-Injection",
                        remediation="Use parameterized queries or ORM instead of direct string concatenation in SQL queries.",
                    )
    
    def _check_sql_patterns(self, target_path: str) -> None:
        """
        Check files for SQL injection patterns.
        
        Args:
            target_path: Path to directory or file for checking
        """
        path = Path(target_path)
        
        # Traverse files
        for file_path in self._find_python_files(path):
            try:
                with open(file_path, "r", encoding="utf-8") as f:
                    content = f.read()
                    
                # Compile patterns
                sql_patterns = [re.compile(pattern) for pattern in self.SQL_INJECTION_PATTERNS]
                safe_patterns = [re.compile(pattern) for pattern in self.SAFE_PATTERNS]
                
                # Check by lines to get line number
                lines = content.splitlines()
                for i, line in enumerate(lines):
                    # Search for potentially dangerous patterns
                    for pattern in sql_patterns:
                        if pattern.search(line):
                            # Check if there are no safe patterns
                            is_safe = any(safe_pattern.search(line) for safe_pattern in safe_patterns)
                            if not is_safe:
                                self.add_issue(
                                    issue_type="SQL Injection",
                                    severity="high",
                                    description=f"Potential SQL injection: using non-parameterized query",
                                    file_path=str(file_path),
                                    line_number=i + 1,
                                    code_snippet=line.strip(),
                                    confidence="MEDIUM",
                                    cwe_id="CWE-89",
                                    owasp_category="A1:2017-Injection",
                                    remediation="Use parameterized queries instead of string concatenation or formatting.",
                                )
            except Exception as e:
                # Skip files with errors
                continue
    
    def _find_python_files(self, path: Path) -> Set[Path]:
        """
        Find all Python files in directory.
        
        Args:
            path: Path to directory or file
            
        Returns:
            Set[Path]: Set of paths to Python files
        """
        results = set()
        
        if path.is_file() and path.suffix == ".py":
            if not self.should_exclude_path(path):
                results.add(path)
            return results
        
        for root, dirs, files in os.walk(path):
            # Filter directories to exclude
            dirs[:] = [d for d in dirs if not self.should_exclude_path(os.path.join(root, d))]
            
            for file in files:
                if file.endswith(".py"):
                    file_path = Path(root) / file
                    if not self.should_exclude_path(file_path):
                        results.add(file_path)
        
        return results 