"""
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

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",
            )
            
            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 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:
                result = subprocess.run(
                    [
                        "safety",
                        "scan",
                        "--output",
                        "json",
                        "--file",
                        requirements_txt,
                    ],
                    capture_output=True,
                    text=True,
                    check=False,
                )
                if result.returncode != 0 and not result.stdout:
                    print(f"Warning: safety scan failed: {result.stderr}")
                    vulns = []
                else:
                    output = json.loads(result.stdout)
                    vulns = output.get("vulnerabilities", [])
            except Exception as e:
                print(f"Warning: error checking dependencies: {str(e)}")
                vulns = []
            
            # Обработка результатов
            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
            os.unlink(temp_file.name)
            
        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 