"""
Check dependencies for known vulnerabilities.
"""

import os
import re
import subprocess
import tempfile
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple
import json
import traceback

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


class DependencyCheck(BaseCheck):
    """
    Check dependencies for known vulnerabilities.
    """
    
    name = "dependency-check"
    description = "Check dependencies for known vulnerabilities"
    
    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 dependency 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:
            # Find dependency files
            requirements_files = self._find_requirements_files(target_path)
            
            if not requirements_files:
                # If dependency files are not found, check installed packages
                self._check_installed_packages()
            else:
                # Check each dependency file
                for req_file in requirements_files:
                    self._check_requirements_file(req_file)
            
            # Create result
            status = CheckStatus.PASSED if not self.issues else CheckStatus.FAILED
            result = self.create_result(
                status=status,
                message=f"Found {len(self.issues)} vulnerabilities in dependencies" if self.issues else "No vulnerabilities detected in dependencies",
            )
            
            print(f"Debug: DependencyCheck.run result: {result}")
            return result, self.issues
            
        except Exception as e:
            print(f"Debug: Exception in DependencyCheck.run: {str(e)}")
            # In case of error, return ERROR status
            result = self.create_result(
                status=CheckStatus.ERROR,
                message=f"Error checking dependencies: {str(e)}",
            )
            return result, self.issues
    
    def _find_requirements_files(self, target_path: str) -> List[str]:
        """
        Find dependency files in directory.
        
        Args:
            target_path: Path to directory
            
        Returns:
            List[str]: List of paths to dependency files
        """
        requirements_files = []
        
        path = Path(target_path)
        if path.is_file():
            # If target_path is a file, check if it's a dependency file
            if self._is_requirements_file(path.name) and not self.should_exclude_path(path):
                requirements_files.append(str(path))
            return requirements_files
        
        # Traverse directory
        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:
                file_path = os.path.join(root, file)
                if self._is_requirements_file(file) and not self.should_exclude_path(file_path):
                    requirements_files.append(file_path)
        
        return requirements_files
    
    def _is_requirements_file(self, filename: str) -> bool:
        """
        Check if file is a dependency file.
        
        Args:
            filename: File name
            
        Returns:
            bool: True if it's a dependency file
        """
        return (
            filename == "requirements.txt" or 
            filename.startswith("requirements") and filename.endswith(".txt") or
            filename == "Pipfile" or
            filename == "Pipfile.lock" or
            filename == "pyproject.toml" or
            filename == "setup.py"
        )
    
    def _check_requirements_file(self, requirements_file: str) -> None:
        """
        Check dependency file for vulnerabilities.
        
        Args:
            requirements_file: Path to dependency file
        """
        try:
            # Convert file format if needed
            if not requirements_file.endswith(".txt"):
                requirements_txt = self._convert_to_requirements_txt(requirements_file)
                if not requirements_txt:
                    return
            else:
                requirements_txt = requirements_file
            
            # Запуск safety scan через subprocess
            try:
                print(f"Debug: Attempting to run safety scan on {requirements_txt}")
                result = subprocess.run(
                    [
                        "safety",
                        "scan",
                        "--output",
                        "json",
                        "--file",
                        requirements_txt,
                    ],
                    capture_output=True,
                    text=True,
                    check=False,
                    timeout=30
                )
                # Log the full traceback for better debugging
                print(f"Warning: An error occurred during safety scan subprocess for {requirements_txt}.\nTraceback:\n{traceback.format_exc()}")
                vulns = []
                result = None # Set result to None to skip processing output
            except subprocess.TimeoutExpired:
                print(f"Warning: safety scan timed out after 30 seconds for {requirements_txt}")
                # Treat as failure, no vulns found via safety
                result = None # Set result to None to skip processing output
            except Exception as e:
                # Log the full traceback for better debugging
                print(f"Warning: error checking dependencies during safety scan: {str(e)}\nTraceback:\n{traceback.format_exc()}")
                vulns = []
            
            # Check if result is valid before processing (handles timeout case)
            if result is not None:
                # Regardless of return code, try to parse JSON output if stdout is not empty
                if not result.stdout:
                    # safety produced no output, assume no vulns found
                    vulns = []
                    print(f"Info: safety scan produced no output for {requirements_txt}. Assumed no vulnerabilities found.")
                else:
                    try:
                        output = json.loads(result.stdout)
                        vulns = output.get("vulnerabilities", [])
                    except json.JSONDecodeError as e:
                        # Handle JSON decode error specifically
                        print(f"Warning: Failed to parse safety JSON output from {requirements_txt}: {str(e)}")
                        # Log the full traceback for better debugging
                        print(f"Warning: error checking dependencies during JSON parsing: {str(e)}")
                        print(f"Warning: error checking dependencies: {str(e)}\nTraceback:\n{traceback.format_exc()}")
                        vulns = [] # Treat as no vulnerabilities found due to parse error
            
            # Обработка результатов
            for vuln in vulns:
                # Пример структуры vuln:
                # {
                #   "package_name": "django",
                #   "affected_versions": ["2.2.0"],
                #   "vulnerability_id": "PYSEC-2020-123",
                #   "description": "...",
                #   ...
                # }
                package_name = vuln.get("package_name") or vuln.get("name")
                affected_versions = vuln.get("affected_versions") or [vuln.get("version")]
                vulnerability_id = vuln.get("vulnerability_id") or vuln.get("id")
                vulnerability_details = vuln.get("description") or vuln.get("details")
                remediation = vuln.get("remediation") or vuln.get("fix_version")
                cwe_id = vuln.get("cwe")
                owasp_category = vuln.get("owasp")
                confidence = "HIGH"
                severity = vuln.get("severity", "high").lower()
                for affected_version in affected_versions or [""]:
                    self.add_issue(
                        issue_type="Vulnerable Dependency",
                        severity=severity,
                        description=f"Vulnerability in dependency: {package_name} {affected_version} - {vulnerability_details}",
                        file_path=requirements_file,
                        line_number=self._find_package_line(requirements_file, package_name),
                        code_snippet=f"{package_name}=={affected_version}",
                        confidence=confidence,
                        cwe_id=cwe_id,
                        owasp_category=owasp_category,
                        remediation=f"Update package {package_name} to a secure version or replace it with an alternative." if not remediation else remediation,
                    )
        except Exception as e:
            # Skip files with errors
            print(f"Error checking {requirements_file}: {str(e)}")
    
    def _convert_to_requirements_txt(self, file_path: str) -> Optional[str]:
        """
        Convert other dependency file formats to requirements.txt.
        
        Args:
            file_path: Path to dependency file
            
        Returns:
            Optional[str]: Path to generated requirements.txt or None in case of error
        """
        try:
            # Temporary file for requirements.txt
            temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".txt")
            temp_file.close()
            
            # Convert depending on file type
            if file_path.endswith("setup.py"):
                subprocess.run(
                    ["pip", "install", "-e", os.path.dirname(file_path)],
                    check=True,
                    capture_output=True,
                )
                subprocess.run(
                    ["pip", "freeze", "-l"],
                    check=True,
                    stdout=open(temp_file.name, "w"),
                )
                
            elif file_path.endswith("Pipfile") or file_path.endswith("Pipfile.lock"):
                subprocess.run(
                    ["pipenv", "lock", "-r"],
                    check=True,
                    cwd=os.path.dirname(file_path),
                    stdout=open(temp_file.name, "w"),
                )
                
            elif file_path.endswith("pyproject.toml"):
                try:
                    if os.path.exists(os.path.join(os.path.dirname(file_path), "poetry.lock")):
                        try:
                            # Try to use poetry export
                            result = subprocess.run(
                                ["poetry", "export", "-f", "requirements.txt"],
                                capture_output=True,
                                text=True,
                                cwd=os.path.dirname(file_path),
                                check=False,  # Don't raise exception on error
                            )
                            
                            # Check execution success
                            if result.returncode == 0:
                                with open(temp_file.name, "w") as f:
                                    f.write(result.stdout)
                            else:
                                # If poetry export didn't work, proceed to file parsing
                                raise Exception(f"Poetry export failed: {result.stderr}")
                        except Exception as e:
                            print(f"Error using poetry export: {str(e)}. Using manual parsing.")
                            # Proceed to manual parsing
                            raise Exception("Fallback to manual parsing")
                    else:
                        # No poetry.lock, use manual parsing
                        raise Exception("No poetry.lock found")
                        
                except Exception:
                    # Use manual parsing in case of any problems with poetry
                    print(f"Extracting dependencies from {file_path} manually")
                    with open(file_path, "r") as f:
                        content = f.read()
                    
                    dependencies = []
                    # Check different dependency definition formats in pyproject.toml
                    
                    # Format 1: dependencies = [...]
                    array_pattern = r"dependencies\s*=\s*\[(.*?)\]"
                    array_match = re.search(array_pattern, content, re.DOTALL)
                    if array_match:
                        deps = array_match.group(1)
                        for dep in re.finditer(r'"(.*?)"', deps):
                            dependencies.append(dep.group(1))
                    
                    # Format 2: [tool.poetry.dependencies] or [project.dependencies]
                    table_patterns = [
                        r"\[tool\.poetry\.dependencies\](.*?)(\[|\Z)",
                        r"\[project\.dependencies\](.*?)(\[|\Z)"
                    ]
                    
                    for pattern in table_patterns:
                        table_match = re.search(pattern, content, re.DOTALL)
                        if table_match:
                            section = table_match.group(1)
                            # Find dependencies in "package = "version" format
                            for dep_match in re.finditer(r'([a-zA-Z0-9_-]+)\s*=\s*["\'](.*?)["\']', section):
                                package_name = dep_match.group(1)
                                if package_name != "python":  # Ignore python requirement
                                    dependencies.append(package_name)
                    
                    with open(temp_file.name, "w") as f:
                        for dep in dependencies:
                            f.write(f"{dep}\n")
            
            return temp_file.name
            
        except Exception as e:
            print(f"Error converting {file_path}: {str(e)}")
            return None
    
    def _check_installed_packages(self) -> None:
        """
        Check installed packages for vulnerabilities.
        """
        try:
            # Create temporary file with installed packages
            temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".txt")
            temp_file.close()
            
            # Get list of installed packages
            subprocess.run(
                ["pip", "freeze"],
                check=True,
                stdout=open(temp_file.name, "w"),
            )
            
            # Check dependencies
            self._check_requirements_file(temp_file.name)
            
            # Delete temporary file
            try:
                os.unlink(temp_file.name)
            except OSError as e:
                print(f"Warning: Failed to delete temporary file {temp_file.name}: {str(e)}")
            
        except Exception as e:
            print(f"Error checking installed packages: {str(e)}")
    
    def _find_package_line(self, file_path: str, package_name: str) -> int:
        """
        Find line number of package in file.
        
        Args:
            file_path: Path to file
            package_name: Package name
            
        Returns:
            int: Line number of package or 1 if not found
        """
        try:
            with open(file_path, "r") as f:
                lines = f.readlines()
                
            for i, line in enumerate(lines):
                if line.strip().startswith(package_name):
                    return i + 1
                
            return 1
            
        except Exception:
            return 1 