"""
Web Sentinel Source Code Security Scanner

Main orchestrator for static application security testing (SAST).
Integrates with the license system and supports multiple programming languages.
"""

from __future__ import annotations

import logging
import mimetypes
import os
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Set, Union

from ...model import Finding, ScanRequest

LOGGER = logging.getLogger("web_sentinel.checks.source_code")

# Supported file extensions by language
LANGUAGE_EXTENSIONS = {
    'php': {'.php', '.phtml', '.php3', '.php4', '.php5', '.phar'},
    'javascript': {'.js', '.jsx', '.ts', '.tsx', '.vue', '.mjs'},
    'python': {'.py', '.pyw', '.wsgi', '.pyx'},
    'java': {'.java', '.jsp', '.jspx'},
    'csharp': {'.cs', '.aspx', '.ascx', '.ashx', '.asmx'},
    'go': {'.go'},
    'sql': {'.sql'},
    'html': {'.html', '.htm', '.xhtml'},
    'css': {'.css', '.scss', '.sass', '.less'},
    'xml': {'.xml', '.config', '.xsd', '.xsl'},
    'json': {'.json'},
    'yaml': {'.yml', '.yaml'},
}

# Common exclusion patterns
DEFAULT_EXCLUSIONS = {
    # Dependencies
    'vendor/', 'node_modules/', '.venv/', 'venv/', '__pycache__/',
    '.git/', '.svn/', '.hg/', '.bzr/',
    
    # Build outputs
    'build/', 'dist/', 'target/', 'bin/', 'obj/',
    
    # Minified files
    '*.min.js', '*.min.css', '*.bundle.js',
    
    # IDE files
    '.vscode/', '.idea/', '*.iml', '.project', '.classpath',
    
    # OS files
    '.DS_Store', 'Thumbs.db', 'desktop.ini',
    
    # Logs and temporary files
    '*.log', '*.tmp', '*.temp', '*.bak',
}

@dataclass
class SourceScanConfig:
    """Configuration for source code scanning."""

    source_path: Optional[Path] = None
    source_files: List[Path] = field(default_factory=list)
    languages: Set[str] = field(default_factory=set)
    exclude_patterns: Set[str] = field(default_factory=lambda: DEFAULT_EXCLUSIONS.copy())
    min_severity: str = "medium"
    max_files: int = -1  # -1 for unlimited
    max_size_mb: int = -1  # -1 for unlimited
    advanced_rules: bool = False
    recursive: bool = True
    detailed_report: bool = False

    def __post_init__(self):
        if not self.languages:
            self.languages = {"auto"}  # Auto-detect


class SourceCodeScanner:
    """Main scanner for static application security testing."""
    
    def __init__(self, config: SourceScanConfig):
        self.config = config
        self._total_size = 0
        self._file_count = 0
        self._root_path = self._determine_root()

    def _determine_root(self) -> Optional[Path]:
        """Determine the root path used for relative exclusions."""
        if self.config.source_path:
            if self.config.source_path.is_dir():
                return self.config.source_path
            return self.config.source_path.parent

        if self.config.source_files:
            parents = [path.parent for path in self.config.source_files if path.exists()]
            if parents:
                try:
                    common = Path(os.path.commonpath([str(parent) for parent in parents]))
                    return common
                except ValueError:
                    return parents[0]
        return None
        
    def scan(self) -> List[Finding]:
        """Execute the source code security scan."""
        findings: List[Finding] = []

        if not self.config.source_path and not self.config.source_files:
            findings.append(
                Finding(
                    check="source-code",
                    title="No source target configured",
                    severity="info",
                    description="Static analysis requires either a source path or explicit files.",
                    remediation="Provide a --source-path argument or select files in the SAST tab.",
                    i18n_key="sast.missing_target",
                )
            )
            return findings

        # Validate source path
        if self.config.source_path and not self.config.source_path.exists():
            findings.append(
                Finding(
                    check="source-code",
                    title="Source path not found",
                    severity="critical",
                    description=f"Source path {self.config.source_path} does not exist",
                    remediation="Verify the source path and ensure it's accessible",
                    evidence=f"Path: {self.config.source_path}",
                    i18n_key="sast.path_not_found",
                    i18n_params={"path": str(self.config.source_path)}
                )
            )
            return findings

        # Get files to scan
        files_to_scan = self._discover_files()
        
        if not files_to_scan:
            target_info = (
                str(self.config.source_path)
                if self.config.source_path
                else ", ".join(str(path) for path in self.config.source_files[:5])
            )
            findings.append(
                Finding(
                    check="source-code",
                    title="No source files found",
                    severity="info",
                    description="No source files matching the criteria were found",
                    remediation="Check file extensions and exclusion patterns",
                    evidence=f"Languages: {self.config.languages}, Target: {target_info}",
                    i18n_key="sast.no_files_found",
                    i18n_params={
                        "languages": ', '.join(self.config.languages),
                        "path": target_info
                    }
                )
            )
            return findings
        
        # Check file limits
        if self.config.max_files > 0 and len(files_to_scan) > self.config.max_files:
            findings.append(
                Finding(
                    check="source-code",
                    title="File limit exceeded",
                    severity="medium",
                    description=f"Found {len(files_to_scan)} files, but license limits to {self.config.max_files}",
                    remediation="Upgrade license or reduce scope with exclusion patterns",
                    evidence=f"Files found: {len(files_to_scan)}, Limit: {self.config.max_files}",
                    i18n_key="sast.file_limit_exceeded",
                    i18n_params={
                        "found": len(files_to_scan),
                        "limit": self.config.max_files
                    }
                )
            )
            files_to_scan = files_to_scan[:self.config.max_files]
        self._file_count = len(files_to_scan)
        
        # Scan each file
        for file_path in files_to_scan:
            try:
                findings.extend(self._scan_file(file_path))
            except Exception as exc:
                LOGGER.warning(f"Failed to scan {file_path}: {exc}")
                findings.append(
                    Finding(
                        check="source-code",
                        title=f"Scan error in {file_path.name}",
                        severity="low",
                        description=f"Could not analyze file: {exc}",
                        remediation="Check file encoding and permissions",
                        evidence=f"File: {file_path}, Error: {exc}",
                        i18n_key="sast.scan_error",
                        i18n_params={"file": str(file_path), "error": str(exc)}
                    )
                )
        
        return findings
    
    def _discover_files(self) -> List[Path]:
        """Discover source files to scan."""
        files: List[Path] = []

        def add_candidate(file_path: Path) -> None:
            if not self._should_scan_file(file_path):
                return
            if self.config.max_size_mb > 0:
                file_size_mb = file_path.stat().st_size / (1024 * 1024)
                if self._total_size + file_size_mb > self.config.max_size_mb:
                    LOGGER.info("Size limit reached, skipping %s", file_path)
                    return
                self._total_size += file_size_mb
            files.append(file_path)
        
        if self.config.source_files:
            for file_path in self.config.source_files:
                path_obj = Path(file_path)
                if path_obj.exists() and path_obj.is_file():
                    add_candidate(path_obj)
            self._file_count = len(files)
            return files

        base_path = self.config.source_path
        if not base_path:
            return files

        if base_path.is_file():
            add_candidate(base_path)
            self._file_count = len(files)
            return files

        iterator = base_path.rglob("*") if self.config.recursive else base_path.glob("*")
        for file_path in iterator:
            if file_path.is_file():
                add_candidate(file_path)
        
        self._file_count = len(files)
        
        return files
    
    def _should_scan_file(self, file_path: Path) -> bool:
        """Determine if a file should be scanned."""
        # Check exclusion patterns
        relative_path = self._relative_path(file_path)
        for pattern in self.config.exclude_patterns:
            if self._matches_pattern(relative_path, pattern):
                return False
        
        # Check file extension
        if 'auto' in self.config.languages:
            return self._is_source_file(file_path)
        
        # Check specific languages
        file_ext = file_path.suffix.lower()
        for lang in self.config.languages:
            if file_ext in LANGUAGE_EXTENSIONS.get(lang, set()):
                return True
        
        return False

    def _relative_path(self, file_path: Path) -> str:
        """Return a normalized relative path for exclusion matching."""
        candidate = str(file_path)
        if self._root_path:
            try:
                candidate = str(file_path.relative_to(self._root_path))
            except ValueError:
                pass
        return candidate.replace("\\", "/")
    
    def _matches_pattern(self, path: str, pattern: str) -> bool:
        """Check if path matches exclusion pattern."""
        if pattern.endswith('/'):
            return pattern in path + '/'
        if '*' in pattern:
            import fnmatch
            return fnmatch.fnmatch(path, pattern)
        return pattern in path
    
    def _is_source_file(self, file_path: Path) -> bool:
        """Check if file is a source code file."""
        ext = file_path.suffix.lower()
        for extensions in LANGUAGE_EXTENSIONS.values():
            if ext in extensions:
                return True
        return False
    
    def _detect_language(self, file_path: Path) -> Optional[str]:
        """Detect programming language from file extension."""
        ext = file_path.suffix.lower()
        for lang, extensions in LANGUAGE_EXTENSIONS.items():
            if ext in extensions:
                return lang
        return None
    
    def _scan_file(self, file_path: Path) -> List[Finding]:
        """Scan a single file for security vulnerabilities."""
        findings: List[Finding] = []
        language = self._detect_language(file_path)
        
        if not language:
            return findings
        
        try:
            content = file_path.read_text(encoding='utf-8', errors='ignore')
        except Exception as exc:
            LOGGER.warning(f"Could not read {file_path}: {exc}")
            return findings
        
        # Language-specific scanning
        if language == 'php':
            findings.extend(self._scan_php(file_path, content))
        elif language == 'javascript':
            findings.extend(self._scan_javascript(file_path, content))
        elif language == 'python':
            findings.extend(self._scan_python(file_path, content))
        elif language == 'java':
            findings.extend(self._scan_java(file_path, content))
        elif language == 'csharp':
            findings.extend(self._scan_csharp(file_path, content))
        elif language == 'go':
            findings.extend(self._scan_go(file_path, content))
        elif language == 'sql':
            findings.extend(self._scan_sql(file_path, content))
        
        # Generic patterns applicable to all languages
        findings.extend(self._scan_generic_patterns(file_path, content))
        
        return findings
    
    def _scan_php(self, file_path: Path, content: str) -> List[Finding]:
        """Scan PHP file for security vulnerabilities."""
        findings: List[Finding] = []
        lines = content.split('\n')
        
        # PHP-specific vulnerability patterns
        php_patterns = {
            'sql_injection': [
                r'mysql_query\s*\(\s*[\'"]?.*\$.*[\'"]?\s*\)',
                r'mysqli_query\s*\(\s*\$\w+\s*,\s*[\'"]?.*\$.*[\'"]?\s*\)',
                r'\$\w+->query\s*\(\s*[\'"]?.*\$.*[\'"]?\s*\)',
                r'[\'"]SELECT.*\$.*[\'"]',
                r'[\'"]INSERT.*\$.*[\'"]',
                r'[\'"]UPDATE.*\$.*[\'"]',
                r'[\'"]DELETE.*\$.*[\'"]',
            ],
            'xss': [
                r'echo\s+\$_GET\[',
                r'echo\s+\$_POST\[',
                r'print\s+\$_REQUEST\[',
                r'echo.*\$_.*\[.*\]',
                r'<\?=\s*\$_',
            ],
            'file_inclusion': [
                r'include\s*\(\s*\$_',
                r'require\s*\(\s*\$_',
                r'include_once\s*\(\s*\$_',
                r'require_once\s*\(\s*\$_',
            ],
            'command_injection': [
                r'exec\s*\(\s*[\'"]?.*\$.*[\'"]?\s*\)',
                r'shell_exec\s*\(\s*[\'"]?.*\$.*[\'"]?\s*\)',
                r'system\s*\(\s*[\'"]?.*\$.*[\'"]?\s*\)',
                r'passthru\s*\(\s*[\'"]?.*\$.*[\'"]?\s*\)',
            ],
        }
        
        for line_num, line in enumerate(lines, 1):
            # SQL Injection
            for pattern in php_patterns['sql_injection']:
                if re.search(pattern, line, re.IGNORECASE):
                    findings.append(
                        Finding(
                            check="source-code-php",
                            title="Potential SQL Injection vulnerability",
                            severity="high",
                            description=f"Line {line_num} contains SQL query with user input concatenation",
                            remediation="Use prepared statements with parameter binding (PDO::prepare() or mysqli_prepare())",
                            evidence=f"File: {file_path.name}, Line: {line_num}\nCode: {line.strip()}",
                            impact="Attackers may execute arbitrary SQL commands and access/modify database",
                            i18n_key="sast.php.sql_injection",
                            i18n_params={
                                "file": str(file_path),
                                "line": line_num,
                                "code": line.strip()
                            }
                        )
                    )
            
            # XSS
            for pattern in php_patterns['xss']:
                if re.search(pattern, line, re.IGNORECASE):
                    findings.append(
                        Finding(
                            check="source-code-php",
                            title="Potential Cross-Site Scripting (XSS) vulnerability",
                            severity="high",
                            description=f"Line {line_num} outputs user input without sanitization",
                            remediation="Use htmlspecialchars() or filter_var() to sanitize output",
                            evidence=f"File: {file_path.name}, Line: {line_num}\nCode: {line.strip()}",
                            impact="Attackers may inject malicious scripts and steal user data",
                            i18n_key="sast.php.xss",
                            i18n_params={
                                "file": str(file_path),
                                "line": line_num,
                                "code": line.strip()
                            }
                        )
                    )
            
            # File Inclusion
            for pattern in php_patterns['file_inclusion']:
                if re.search(pattern, line, re.IGNORECASE):
                    findings.append(
                        Finding(
                            check="source-code-php",
                            title="Potential Local/Remote File Inclusion vulnerability",
                            severity="critical",
                            description=f"Line {line_num} includes file with user-controlled input",
                            remediation="Validate and whitelist allowed file paths, use basename() for safety",
                            evidence=f"File: {file_path.name}, Line: {line_num}\nCode: {line.strip()}",
                            impact="Attackers may include arbitrary files and execute code",
                            i18n_key="sast.php.file_inclusion",
                            i18n_params={
                                "file": str(file_path),
                                "line": line_num,
                                "code": line.strip()
                            }
                        )
                    )
            
            # Command Injection
            for pattern in php_patterns['command_injection']:
                if re.search(pattern, line, re.IGNORECASE):
                    findings.append(
                        Finding(
                            check="source-code-php",
                            title="Potential Command Injection vulnerability",
                            severity="critical",
                            description=f"Line {line_num} executes system command with user input",
                            remediation="Use escapeshellarg() and validate input, prefer safe alternatives",
                            evidence=f"File: {file_path.name}, Line: {line_num}\nCode: {line.strip()}",
                            impact="Attackers may execute arbitrary system commands",
                            i18n_key="sast.php.command_injection",
                            i18n_params={
                                "file": str(file_path),
                                "line": line_num,
                                "code": line.strip()
                            }
                        )
                    )
        
        return findings
    
    def _scan_javascript(self, file_path: Path, content: str) -> List[Finding]:
        """Scan JavaScript file for security vulnerabilities."""
        findings: List[Finding] = []
        lines = content.split('\n')
        
        js_patterns = {
            'eval': [r'eval\s*\(', r'new\s+Function\s*\(', r'setTimeout\s*\([\'"][^\'\"]*[\'"],'],
            'dom_xss': [r'innerHTML\s*=.*\+', r'document\.write\s*\(.*\+'],
            'prototype_pollution': [r'__proto__', r'constructor\.prototype'],
        }
        
        for line_num, line in enumerate(lines, 1):
            # Dangerous eval usage
            for pattern in js_patterns['eval']:
                if re.search(pattern, line, re.IGNORECASE):
                    findings.append(
                        Finding(
                            check="source-code-js",
                            title="Dangerous eval() usage detected",
                            severity="high",
                            description=f"Line {line_num} uses eval() which can execute arbitrary code",
                            remediation="Avoid eval(), use JSON.parse() for JSON or safe alternatives",
                            evidence=f"File: {file_path.name}, Line: {line_num}\nCode: {line.strip()}",
                            impact="Code injection and arbitrary script execution",
                            i18n_key="sast.js.eval_usage",
                            i18n_params={
                                "file": str(file_path),
                                "line": line_num,
                                "code": line.strip()
                            }
                        )
                    )
            
            # DOM XSS
            for pattern in js_patterns['dom_xss']:
                if re.search(pattern, line, re.IGNORECASE):
                    findings.append(
                        Finding(
                            check="source-code-js",
                            title="Potential DOM XSS vulnerability",
                            severity="medium",
                            description=f"Line {line_num} may inject untrusted content into DOM",
                            remediation="Use textContent instead of innerHTML, sanitize user input",
                            evidence=f"File: {file_path.name}, Line: {line_num}\nCode: {line.strip()}",
                            impact="Client-side code injection and data theft",
                            i18n_key="sast.js.dom_xss",
                            i18n_params={
                                "file": str(file_path),
                                "line": line_num,
                                "code": line.strip()
                            }
                        )
                    )
        
        return findings
    
    def _scan_python(self, file_path: Path, content: str) -> List[Finding]:
        """Scan Python file for security vulnerabilities."""
        findings: List[Finding] = []
        lines = content.split('\n')
        
        python_patterns = {
            'eval': [r'eval\s*\(', r'exec\s*\('],
            'sql_injection': [r'\.execute\s*\([\'"].*%.*[\'"]', r'\.execute\s*\([\'"].*\+.*[\'"]'],
            'command_injection': [r'os\.system\s*\(', r'subprocess\.call\s*\(.*shell=True'],
            'pickle': [r'pickle\.loads?\s*\(', r'cPickle\.loads?\s*\('],
        }
        
        for line_num, line in enumerate(lines, 1):
            # Eval usage
            for pattern in python_patterns['eval']:
                if re.search(pattern, line, re.IGNORECASE):
                    findings.append(
                        Finding(
                            check="source-code-python",
                            title="Dangerous eval/exec usage",
                            severity="high",
                            description=f"Line {line_num} uses eval/exec which can execute arbitrary code",
                            remediation="Use ast.literal_eval() for safe evaluation or avoid dynamic execution",
                            evidence=f"File: {file_path.name}, Line: {line_num}\nCode: {line.strip()}",
                            impact="Arbitrary code execution and system compromise",
                            i18n_key="sast.python.eval_usage",
                            i18n_params={
                                "file": str(file_path),
                                "line": line_num,
                                "code": line.strip()
                            }
                        )
                    )
        
        return findings
    
    def _scan_java(self, file_path: Path, content: str) -> List[Finding]:
        """Scan Java file for security vulnerabilities."""
        findings: List[Finding] = []
        # TODO: Implement Java-specific patterns
        return findings
    
    def _scan_csharp(self, file_path: Path, content: str) -> List[Finding]:
        """Scan C# file for security vulnerabilities."""
        findings: List[Finding] = []
        # TODO: Implement C#-specific patterns
        return findings
    
    def _scan_go(self, file_path: Path, content: str) -> List[Finding]:
        """Scan Go file for security vulnerabilities."""
        findings: List[Finding] = []
        # TODO: Implement Go-specific patterns
        return findings
    
    def _scan_sql(self, file_path: Path, content: str) -> List[Finding]:
        """Scan SQL file for security issues."""
        findings: List[Finding] = []
        # TODO: Implement SQL-specific patterns
        return findings
    
    def _scan_generic_patterns(self, file_path: Path, content: str) -> List[Finding]:
        """Scan for generic security patterns applicable to all languages."""
        findings: List[Finding] = []
        lines = content.split('\n')
        
        # Generic patterns
        sensitive_patterns = {
            'hardcoded_secrets': [
                r'password\s*=\s*[\'"][^\'\"]{8,}[\'"]',
                r'api[_-]?key\s*=\s*[\'"][^\'\"]{16,}[\'"]',
                r'secret\s*=\s*[\'"][^\'\"]{16,}[\'"]',
                r'token\s*=\s*[\'"][^\'\"]{20,}[\'"]',
            ],
            'weak_crypto': [
                r'md5\s*\(',
                r'sha1\s*\(',
                r'DES\s*\(',
                r'RC4\s*\(',
            ],
        }
        
        for line_num, line in enumerate(lines, 1):
            # Hardcoded secrets
            for pattern in sensitive_patterns['hardcoded_secrets']:
                if re.search(pattern, line, re.IGNORECASE):
                    findings.append(
                        Finding(
                            check="source-code-generic",
                            title="Hardcoded credentials detected",
                            severity="high",
                            description=f"Line {line_num} contains hardcoded credentials",
                            remediation="Move credentials to environment variables or secure configuration",
                            evidence=f"File: {file_path.name}, Line: {line_num}\nCode: {line.strip()}",
                            impact="Credential exposure in source code repositories",
                            i18n_key="sast.generic.hardcoded_secrets",
                            i18n_params={
                                "file": str(file_path),
                                "line": line_num,
                                "code": line.strip()
                            }
                        )
                    )
            
            # Weak cryptography
            for pattern in sensitive_patterns['weak_crypto']:
                if re.search(pattern, line, re.IGNORECASE):
                    findings.append(
                        Finding(
                            check="source-code-generic",
                            title="Weak cryptographic algorithm",
                            severity="medium",
                            description=f"Line {line_num} uses weak cryptographic algorithm",
                            remediation="Use strong algorithms like SHA-256, bcrypt, or scrypt",
                            evidence=f"File: {file_path.name}, Line: {line_num}\nCode: {line.strip()}",
                            impact="Cryptographic weakness may compromise data security",
                            i18n_key="sast.generic.weak_crypto",
                            i18n_params={
                                "file": str(file_path),
                                "line": line_num,
                                "code": line.strip()
                            }
                        )
                    )
        
        return findings


def evaluate_source_code(request: ScanRequest) -> Iterable[Finding]:
    """
    Entry point for source code security scanning.
    
    This function integrates with the existing Web Sentinel architecture
    and is compatible with the license system and CLI/GUI interfaces.
    """
    findings: List[Finding] = []
    
    def _normalize_iterable(value, *, split_tokens: bool = True) -> List[str]:
        if not value:
            return []
        if isinstance(value, (list, tuple, set, frozenset)):
            normalized: List[str] = []
            for item in value:
                normalized.extend(_normalize_iterable(item, split_tokens=split_tokens))
            return normalized
        if isinstance(value, str):
            if split_tokens:
                parts = [part.strip() for part in re.split(r"[;,]\s*|\s+", value) if part.strip()]
                return parts
            return [value.strip()]
        return [str(value).strip()]

    raw_source_path = getattr(request, "source_path", None)
    source_path = None
    if isinstance(raw_source_path, (str, Path)) and raw_source_path:
        source_path = Path(raw_source_path).expanduser()

    raw_source_files = getattr(request, "source_files", ())
    normalized_files = _normalize_iterable(raw_source_files, split_tokens=False)
    source_files = [Path(item).expanduser() for item in normalized_files if item]

    if source_path is None and not source_files:
        findings.append(
            Finding(
                check="source-code",
                title="Source code scanning not configured",
                severity="info",
                description="Source code scanning requires --source-path parameter",
                remediation="Provide source code path using --source-path option",
                i18n_key="sast.not_configured",
            )
        )
        return findings

    raw_languages = getattr(request, "source_languages", ()) or getattr(request, "languages", ())
    languages_set = {lang.lower() for lang in _normalize_iterable(raw_languages)} or {"auto"}

    raw_exclude = getattr(request, "source_exclude", ()) or getattr(request, "exclude_patterns", ())
    exclude_patterns = set(DEFAULT_EXCLUSIONS)
    exclude_patterns.update(_normalize_iterable(raw_exclude))

    config = SourceScanConfig(
        source_path=source_path,
        source_files=source_files,
        languages=languages_set,
        exclude_patterns=exclude_patterns,
        min_severity=getattr(request, "source_min_severity", "medium"),
        max_files=getattr(request, "source_max_files", -1),
        max_size_mb=getattr(request, "source_max_size_mb", -1),
        advanced_rules=getattr(request, "source_advanced_rules", False),
        recursive=getattr(request, "source_recursive", True),
        detailed_report=getattr(request, "source_detailed_report", False),
    )
    
    # Execute scan
    scanner = SourceCodeScanner(config)
    findings.extend(scanner.scan())
    
    return findings
