"""
Dependency Security Scanner for PyGenAI Security Framework
Scans Python dependencies for known vulnerabilities and security issues.
"""

import json
import re
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple
import subprocess
import sys

from ..core.vulnerability import Vulnerability, ThreatLevel, VulnerabilityCategory
from ..utils.logger import get_logger

logger = get_logger(__name__)


class DependencyScanner:
    """
    Advanced dependency security scanner for Python packages
    
    Features:
    - Known vulnerability detection (CVE database)
    - License compatibility analysis
    - Outdated package identification
    - Transitive dependency analysis
    - Security advisory integration
    """
    
    def __init__(self, config: Dict[str, Any] = None):
        self.config = config or {}
        self.enabled_rules = self.config.get('rules', [
            'known_vulnerabilities', 'license_issues', 'outdated_packages', 'insecure_versions'
        ])
        self.confidence_threshold = self.config.get('confidence_threshold', 0.9)
        self.logger = get_logger(f'{__name__}.DependencyScanner')
        
        # Mock vulnerability database (in production, integrate with safety, snyk, etc.)
        self.vulnerability_db = {
            'requests': {
                '2.25.1': {
                    'cve_id': 'CVE-2021-33503',
                    'description': 'ReDoS vulnerability in URL parsing',
                    'cvss_score': 7.5,
                    'threat_level': ThreatLevel.HIGH,
                    'fixed_versions': ['2.26.0', '2.27.0']
                },
                '2.24.0': {
                    'cve_id': 'CVE-2021-33503', 
                    'description': 'HTTPS proxy tunnel vulnerability',
                    'cvss_score': 6.5,
                    'threat_level': ThreatLevel.MEDIUM,
                    'fixed_versions': ['2.25.2', '2.26.0']
                }
            },
            'urllib3': {
                '1.26.4': {
                    'cve_id': 'CVE-2021-28363',
                    'description': 'HTTPS proxy tunnel attack',
                    'cvss_score': 6.5,
                    'threat_level': ThreatLevel.MEDIUM,
                    'fixed_versions': ['1.26.5']
                }
            },
            'pillow': {
                '8.1.0': {
                    'cve_id': 'CVE-2021-25287',
                    'description': 'Out-of-bounds read vulnerability',
                    'cvss_score': 9.1,
                    'threat_level': ThreatLevel.CRITICAL,
                    'fixed_versions': ['8.1.1', '8.2.0']
                }
            },
            'flask': {
                '1.1.4': {
                    'cve_id': 'CVE-2023-30861',
                    'description': 'Cookie parsing vulnerability',
                    'cvss_score': 7.5,
                    'threat_level': ThreatLevel.HIGH,
                    'fixed_versions': ['2.3.0']
                }
            },
            'django': {
                '3.1.0': {
                    'cve_id': 'CVE-2021-35042',
                    'description': 'SQL injection in QuerySet.extra()',
                    'cvss_score': 9.1,
                    'threat_level': ThreatLevel.CRITICAL,
                    'fixed_versions': ['3.1.13', '3.2.5']
                }
            }
        }
    
    def scan_files(self, files: List[Path]) -> List[Vulnerability]:
        """Scan dependency files for security vulnerabilities"""
        vulnerabilities = []
        
        # Find dependency files
        dependency_files = self._find_dependency_files(files)
        
        for dep_file in dependency_files:
            try:
                file_vulnerabilities = self._scan_dependency_file(dep_file)
                vulnerabilities.extend(file_vulnerabilities)
            except Exception as e:
                self.logger.error(f"Error scanning dependency file {dep_file}: {e}")
        
        return vulnerabilities
    
    def scan_file(self, file_path: Path) -> List[Vulnerability]:
        """Scan single dependency file"""
        if self._is_dependency_file(file_path):
            return self._scan_dependency_file(file_path)
        return []
    
    def _find_dependency_files(self, files: List[Path]) -> List[Path]:
        """Find dependency specification files"""
        dependency_files = []
        
        dependency_patterns = [
            'requirements.txt', 'requirements-dev.txt', 'requirements-test.txt',
            'Pipfile', 'Pipfile.lock', 'poetry.lock', 'pyproject.toml',
            'setup.py', 'setup.cfg', 'conda.yml', 'environment.yml'
        ]
        
        for file_path in files:
            if any(pattern in file_path.name for pattern in dependency_patterns):
                dependency_files.append(file_path)
        
        return dependency_files
    
    def _is_dependency_file(self, file_path: Path) -> bool:
        """Check if file is a dependency specification file"""
        return file_path.name in [
            'requirements.txt', 'Pipfile', 'pyproject.toml', 'setup.py', 
            'poetry.lock', 'Pipfile.lock', 'conda.yml'
        ]
    
    def _scan_dependency_file(self, dep_file: Path) -> List[Vulnerability]:
        """Scan individual dependency file for vulnerabilities"""
        vulnerabilities = []
        
        try:
            # Parse dependencies based on file type
            dependencies = self._parse_dependencies(dep_file)
            
            for dep_info in dependencies:
                package_name = dep_info['name']
                version = dep_info.get('version')
                line_number = dep_info.get('line_number', 0)
                
                # Check for known vulnerabilities
                if self._is_enabled('known_vulnerabilities'):
                    vulns = self._check_known_vulnerabilities(
                        package_name, version, dep_file, line_number
                    )
                    vulnerabilities.extend(vulns)
                
                # Check for outdated packages  
                if self._is_enabled('outdated_packages'):
                    vulns = self._check_outdated_packages(
                        package_name, version, dep_file, line_number
                    )
                    vulnerabilities.extend(vulns)
                
                # Check for insecure versions
                if self._is_enabled('insecure_versions'):
                    vulns = self._check_insecure_versions(
                        package_name, version, dep_file, line_number
                    )
                    vulnerabilities.extend(vulns)
        
        except Exception as e:
            self.logger.error(f"Failed to scan dependency file {dep_file}: {e}")
        
        return vulnerabilities
    
    def _parse_dependencies(self, dep_file: Path) -> List[Dict[str, Any]]:
        """Parse dependencies from various file formats"""
        dependencies = []
        
        try:
            with open(dep_file, 'r', encoding='utf-8') as f:
                content = f.read()
            
            if dep_file.name == 'requirements.txt':
                dependencies = self._parse_requirements_txt(content)
            elif dep_file.name == 'setup.py':
                dependencies = self._parse_setup_py(content)
            elif dep_file.name == 'pyproject.toml':
                dependencies = self._parse_pyproject_toml(content)
            elif dep_file.name in ['Pipfile', 'Pipfile.lock']:
                dependencies = self._parse_pipfile(content)
            
        except Exception as e:
            self.logger.error(f"Failed to parse {dep_file}: {e}")
        
        return dependencies
    
    def _parse_requirements_txt(self, content: str) -> List[Dict[str, Any]]:
        """Parse requirements.txt format"""
        dependencies = []
        lines = content.split('\n')
        
        for line_num, line in enumerate(lines, 1):
            line = line.strip()
            if not line or line.startswith('#') or line.startswith('-'):
                continue
            
            # Parse package and version
            parsed = self._parse_requirement_line(line)
            if parsed:
                parsed['line_number'] = line_num
                dependencies.append(parsed)
        
        return dependencies
    
    def _parse_setup_py(self, content: str) -> List[Dict[str, Any]]:
        """Parse setup.py dependencies (simplified)"""
        dependencies = []
        
        # Extract install_requires and extras_require
        install_requires_match = re.search(
            r'install_requires\s*=\s*\[(.*?)\]', 
            content, 
            re.DOTALL | re.IGNORECASE
        )
        
        if install_requires_match:
            requires_content = install_requires_match.group(1)
            # Parse each requirement
            for req in re.findall(r'[""]([^""]+)[""]', requires_content):
                parsed = self._parse_requirement_line(req.strip())
                if parsed:
                    dependencies.append(parsed)
        
        return dependencies
    
    def _parse_pyproject_toml(self, content: str) -> List[Dict[str, Any]]:
        """Parse pyproject.toml format (simplified)"""
        dependencies = []
        
        # Simple regex-based parsing (in production, use toml library)
        deps_match = re.search(
            r'dependencies\s*=\s*\[(.*?)\]',
            content,
            re.DOTALL | re.IGNORECASE
        )
        
        if deps_match:
            deps_content = deps_match.group(1)
            for req in re.findall(r'["]([^"]+)["]', deps_content):
                parsed = self._parse_requirement_line(req.strip())
                if parsed:
                    dependencies.append(parsed)
        
        return dependencies
    
    def _parse_pipfile(self, content: str) -> List[Dict[str, Any]]:
        """Parse Pipfile format (simplified)"""
        dependencies = []
        
        # Extract packages section
        packages_match = re.search(
            r'\[packages\](.*?)(?=\[|$)',
            content,
            re.DOTALL | re.IGNORECASE
        )
        
        if packages_match:
            packages_content = packages_match.group(1)
            for line in packages_content.split('\n'):
                line = line.strip()
                if '=' in line and not line.startswith('#'):
                    parts = line.split('=', 1)
                    if len(parts) == 2:
                        name = parts[0].strip()
                        version_part = parts[1].strip().strip('"')
                        
                        dependencies.append({
                            'name': name,
                            'version': version_part if version_part != '*' else None,
                            'original_line': line
                        })
        
        return dependencies
    
    def _parse_requirement_line(self, line: str) -> Optional[Dict[str, Any]]:
        """Parse a single requirement line"""
        # Handle various formats: package==version, package>=version, etc.
        patterns = [
            r'^([a-zA-Z0-9_.-]+)\s*==\s*([0-9.]+)',
            r'^([a-zA-Z0-9_.-]+)\s*>=\s*([0-9.]+)',
            r'^([a-zA-Z0-9_.-]+)\s*~=\s*([0-9.]+)',
            r'^([a-zA-Z0-9_.-]+)\s*>\s*([0-9.]+)',
            r'^([a-zA-Z0-9_.-]+)\s*<\s*([0-9.]+)',
            r'^([a-zA-Z0-9_.-]+)\s*!=\s*([0-9.]+)',
            r'^([a-zA-Z0-9_.-]+)$'  # No version specified
        ]
        
        for pattern in patterns:
            match = re.match(pattern, line.strip())
            if match:
                name = match.group(1)
                version = match.group(2) if len(match.groups()) > 1 else None
                
                return {
                    'name': name,
                    'version': version,
                    'original_line': line
                }
        
        return None
    
    def _check_known_vulnerabilities(self, package_name: str, version: str, 
                                   dep_file: Path, line_number: int) -> List[Vulnerability]:
        """Check for known vulnerabilities in package version"""
        vulnerabilities = []
        
        if not version or package_name not in self.vulnerability_db:
            return vulnerabilities
        
        package_vulns = self.vulnerability_db[package_name]
        
        if version in package_vulns:
            vuln_info = package_vulns[version]
            
            vulnerability = Vulnerability(
                title=f"Known Vulnerability in {package_name}",
                description=f"{package_name} {version} has known vulnerability: {vuln_info['description']}",
                threat_level=vuln_info['threat_level'],
                category=VulnerabilityCategory.VULNERABLE_COMPONENTS,
                file_path=str(dep_file),
                line_number=line_number,
                code_snippet=f"{package_name}=={version}",
                remediation=f"Update {package_name} to a secure version",
                remediation_examples=[
                    f"Update to: {', '.join(vuln_info['fixed_versions'])}",
                    f"pip install {package_name}>={vuln_info['fixed_versions'][0]}",
                    "Review security advisories before updating"
                ],
                business_impact="Vulnerable dependencies can be exploited to compromise application security",
                technical_impact=f"CVE: {vuln_info['cve_id']} - {vuln_info['description']}",
                cvss_score=vuln_info['cvss_score'],
                cwe_id="CWE-1104",
                owasp_category="A06:2021 – Vulnerable and Outdated Components",
                confidence=0.95,
                scanner_name="dependency_scanner",
                scan_rule_id=f"KNOWN_VULN_{vuln_info['cve_id']}",
                external_references=[
                    f"https://cve.mitre.org/cgi-bin/cvename.cgi?name={vuln_info['cve_id']}"
                ]
            )
            
            vulnerabilities.append(vulnerability)
        
        return vulnerabilities
    
    def _check_outdated_packages(self, package_name: str, version: str,
                               dep_file: Path, line_number: int) -> List[Vulnerability]:
        """Check for outdated packages (simplified implementation)"""
        vulnerabilities = []
        
        # Mock implementation - in production, check against PyPI API
        outdated_packages = {
            'requests': {'current': '2.25.1', 'latest': '2.31.0'},
            'flask': {'current': '1.1.4', 'latest': '2.3.2'},
            'django': {'current': '3.1.0', 'latest': '4.2.5'}
        }
        
        if package_name in outdated_packages and version:
            package_info = outdated_packages[package_name]
            if version == package_info['current']:
                vulnerability = Vulnerability(
                    title=f"Outdated Package: {package_name}",
                    description=f"{package_name} {version} is outdated. Latest version: {package_info['latest']}",
                    threat_level=ThreatLevel.LOW,
                    category=VulnerabilityCategory.VULNERABLE_COMPONENTS,
                    file_path=str(dep_file),
                    line_number=line_number,
                    code_snippet=f"{package_name}=={version}",
                    remediation=f"Update {package_name} to latest version",
                    remediation_examples=[
                        f"pip install {package_name}=={package_info['latest']}",
                        f"pip install --upgrade {package_name}",
                        "Check changelog for breaking changes"
                    ],
                    business_impact="Outdated packages may contain security vulnerabilities",
                    cvss_score=3.1,
                    confidence=0.8,
                    scanner_name="dependency_scanner",
                    scan_rule_id="OUTDATED_PACKAGE"
                )
                vulnerabilities.append(vulnerability)
        
        return vulnerabilities
    
    def _check_insecure_versions(self, package_name: str, version: str,
                               dep_file: Path, line_number: int) -> List[Vulnerability]:
        """Check for versions known to be insecure"""
        vulnerabilities = []
        
        # Known insecure version patterns
        insecure_patterns = {
            'django': {
                'pattern': r'^[12]\.',
                'message': 'Django 1.x and 2.x are no longer supported',
                'recommendation': 'Upgrade to Django 4.x for security updates'
            },
            'flask': {
                'pattern': r'^0\.',
                'message': 'Flask 0.x versions have known security issues',
                'recommendation': 'Upgrade to Flask 2.x'
            }
        }
        
        if package_name in insecure_patterns and version:
            pattern_info = insecure_patterns[package_name]
            if re.match(pattern_info['pattern'], version):
                vulnerability = Vulnerability(
                    title=f"Insecure Version: {package_name}",
                    description=f"{package_name} {version}: {pattern_info['message']}",
                    threat_level=ThreatLevel.MEDIUM,
                    category=VulnerabilityCategory.VULNERABLE_COMPONENTS,
                    file_path=str(dep_file),
                    line_number=line_number,
                    code_snippet=f"{package_name}=={version}",
                    remediation=pattern_info['recommendation'],
                    business_impact="Using unsupported versions increases security risk",
                    cvss_score=5.3,
                    confidence=0.9,
                    scanner_name="dependency_scanner",
                    scan_rule_id="INSECURE_VERSION"
                )
                vulnerabilities.append(vulnerability)
        
        return vulnerabilities
    
    def _is_enabled(self, rule_name: str) -> bool:
        """Check if specific rule is enabled"""
        return rule_name in self.enabled_rules
    
    def configure(self, config: Dict[str, Any]):
        """Configure scanner with new settings"""
        self.config.update(config)
        
        if 'rules' in config:
            self.enabled_rules = config['rules']
        
        if 'confidence_threshold' in config:
            self.confidence_threshold = config['confidence_threshold']
