"""
SQL Injection vulnerability check.
"""

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

import bandit
from bandit.core import manager
from bandit.core import config

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",
        }
        self.logger = logging.getLogger(__name__)
    
    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 via subprocess
            bandit_success = self._run_bandit_check(target_path)
            
            # Additional check using patterns
            self._check_sql_patterns(target_path)
            
            # Create result
            if not bandit_success:
                # Bandit failed but we still checked patterns
                if self.issues:
                    status = CheckStatus.FAILED
                    message = f"Found {len(self.issues)} potential SQL injections (bandit check failed)"
                else:
                    # Fix: We're completing the check partially but with no findings, so PASSED instead of ERROR
                    status = CheckStatus.PASSED
                    message = "No SQL injections detected (bandit check failed but pattern check completed)"
            else:
                status = CheckStatus.PASSED if not self.issues else CheckStatus.FAILED
                message = f"Found {len(self.issues)} potential SQL injections" if self.issues else "No SQL injections detected"
            
            result = self.create_result(
                status=status,
                message=message,
            )
            
            return result, self.issues
            
        except Exception as e:
            self.logger.error(f"Error during SQL injection check: {str(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) -> bool:
        """
        Run check using bandit as a subprocess.
        
        Args:
            target_path: Path to directory or file for checking
            
        Returns:
            bool: True if bandit check completed successfully, False otherwise
        """
        try:
            # Construct Bandit command
            command = [
                "bandit",
                "--format", "json",
                "--recursive", # Enable recursive scan for directories
                target_path
            ]
            
            # Run Bandit as a subprocess
            # Use capture_output=True to get stdout and stderr
            # Use text=True to decode output as text
            # Use check=False to prevent exception on non-zero exit code
            result = subprocess.run(
                command,
                capture_output=True,
                text=True,
                check=False,
            )
            
            # Check for errors during execution
            if result.returncode != 0:
                # Log Bandit's output on non-zero exit code for debugging, but don't treat as a fatal error
                self.logger.debug(f"Bandit stdout:\n{result.stdout}")
                self.logger.debug(f"Bandit stderr:\n{result.stderr}")
            else:
                # Parse JSON output
                try:
                    bandit_output = json.loads(result.stdout)
                except json.JSONDecodeError as e:
                    self.logger.error(f"Failed to parse Bandit JSON output: {str(e)}\nOutput:\n{result.stdout}")
                    self.logger.error(f"Bandit stdout on JSON error:\n{result.stdout}") # Log stdout again on JSON error for clarity
                    self.logger.error(f"Bandit stderr on JSON error:\n{result.stderr}") # Log stderr again on JSON error for clarity
                    return False
                
                # Process results
                # The structure of bandit JSON output might vary slightly, 
                # but typically issues are under a key like 'results' or 'vulnerabilities'
                # Let's assume issues are in 'results' and each has a 'test_id'
                issues = bandit_output.get('results', [])
                
                for issue in issues:
                    # Filter only SQL injections (based on test_id)
                    if issue.get('test_id', '').lower() == "b608": # B608 is Bandit's test ID for SQL Injection
                        self.add_issue(
                            issue_type="SQL Injection",
                            severity=issue.get('severity', 'medium').lower(),
                            description=issue.get('text', 'SQL Injection vulnerability'),
                            file_path=issue.get('filename', 'unknown'),
                            line_number=issue.get('line_number', 0),
                            code_snippet=issue.get('code', ''),
                            confidence=issue.get('confidence', 'MEDIUM').lower(),
                            cwe_id=issue.get('cwe'),
                            owasp_category=issue.get('owasp'),
                            remediation="Use parameterized queries or ORM instead of direct string concatenation in SQL queries.",
                        )
                return True
            
        except FileNotFoundError:
            self.logger.error("Bandit command not found. Please ensure Bandit is installed and in your PATH.")
            return False
        except Exception as e:
            self.logger.error(f"Error running Bandit as subprocess: {str(e)}")
            return False
    
    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 queries",
                                    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:
                self.logger.error(f"Error checking file {file_path}: {str(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 