"""
Dependency Analyzer for MCP Security Auditor.

Analyzes project dependencies for known vulnerabilities and security issues.
"""

import json
import asyncio
import subprocess
from pathlib import Path
from typing import List, Dict, Any, Optional

from ..core.config import AuditConfig
from ..core.models import (
    Finding, CodeLocation, Remediation, MCPServerInfo, Dependency
)
from .static import BaseAnalyzer


class DependencyAnalyzer(BaseAnalyzer):
    """
    Analyzes dependencies for vulnerabilities.
    
    Checks for:
    - Known vulnerabilities (CVEs)
    - Outdated packages
    - Deprecated packages
    - Packages with security advisories
    - Typosquatting risks
    """
    
    name = "dependencies"
    
    # Known vulnerable package patterns (typosquatting, deprecated, etc.)
    KNOWN_BAD_PACKAGES = {
        "python": [
            ("jeIlyfish", "Typosquatting of jellyfish"),
            ("python3-dateutil", "Typosquatting of python-dateutil"),
            ("colourama", "Typosquatting of colorama"),
            ("python-sqlite", "Malicious package"),
        ],
        "npm": [
            ("crossenv", "Typosquatting of cross-env"),
            ("cross-env.js", "Typosquatting of cross-env"),
            ("d3.js", "Typosquatting of d3"),
            ("fabric-js", "Typosquatting of fabric"),
            ("ffmepg", "Typosquatting of ffmpeg"),
            ("gruntcli", "Typosquatting of grunt-cli"),
            ("http-proxy.js", "Typosquatting of http-proxy"),
            ("mariadb", "Typosquatting of mysql"),
            ("mongose", "Typosquatting of mongoose"),
            ("mssql-node", "Typosquatting of mssql"),
            ("mssql.js", "Typosquatting of mssql"),
            ("mysqljs", "Typosquatting of mysql"),
            ("node-fabric", "Typosquatting of fabric"),
            ("node-opencv", "Typosquatting of opencv"),
            ("node-opensl", "Typosquatting of openssl"),
            ("node-openssl", "Typosquatting of openssl"),
            ("nodecaffe", "Typosquatting of caffe"),
            ("nodefabric", "Typosquatting of fabric"),
            ("nodeffmpeg", "Typosquatting of ffmpeg"),
            ("nodemailer-js", "Typosquatting of nodemailer"),
            ("noderequest", "Typosquatting of request"),
            ("nodesass", "Typosquatting of node-sass"),
            ("opencv.js", "Typosquatting of opencv"),
            ("openssl.js", "Typosquatting of openssl"),
            ("proxy.js", "Typosquatting of proxy"),
            ("shadowsock", "Typosquatting of shadowsocks"),
            ("smb", "Typosquatting of samba"),
            ("sqliter", "Typosquatting of sqlite"),
            ("sqlserver", "Typosquatting of mssql"),
            ("tkinter", "Typosquatting of Tkinter"),
        ]
    }
    
    # Deprecated/unmaintained packages
    DEPRECATED_PACKAGES = {
        "python": {
            "requests-oauthlib": "Consider using authlib instead",
            "pycrypto": "Use pycryptodome instead",
        },
        "npm": {
            "request": "Package is deprecated, use axios or node-fetch",
            "querystring": "Use URLSearchParams instead",
            "colors": "Package was compromised, use chalk instead",
        }
    }
    
    async def analyze(self, path: str, server_info: MCPServerInfo) -> List[Finding]:
        """Analyze dependencies for vulnerabilities."""
        findings = []
        path = Path(path)
        
        # Check for known bad packages
        findings.extend(self._check_known_bad_packages(server_info.dependencies, server_info.language))
        
        # Check for deprecated packages
        findings.extend(self._check_deprecated_packages(server_info.dependencies, server_info.language))
        
        # Check for vulnerability databases
        if self.config.check_vulnerabilities:
            findings.extend(await self._check_vulnerabilities(path, server_info))
        
        # Check for packages without version pinning
        findings.extend(self._check_version_pinning(path, server_info))
        
        # Check lockfile integrity
        findings.extend(self._check_lockfile(path, server_info))
        
        return findings
    
    def _check_known_bad_packages(self, dependencies: List[Dependency], language: str) -> List[Finding]:
        """Check for known malicious or typosquatting packages."""
        findings = []
        
        bad_packages = self.KNOWN_BAD_PACKAGES.get(
            "npm" if language in ["typescript", "javascript"] else "python",
            []
        )
        
        bad_package_names = {pkg[0].lower(): pkg[1] for pkg in bad_packages}
        
        for dep in dependencies:
            dep_name_lower = dep.name.lower()
            if dep_name_lower in bad_package_names:
                findings.append(Finding(
                    title=f"Malicious/typosquatting package: {dep.name}",
                    description=f"Package '{dep.name}' is known to be malicious or a typosquatting attack. "
                               f"Reason: {bad_package_names[dep_name_lower]}",
                    severity="critical",
                    category="dependencies",
                    analyzer=self.name,
                    remediation=Remediation(
                        description=f"Remove '{dep.name}' immediately and audit your system for compromise."
                    ),
                    cwe_id="CWE-506",
                    confidence="high"
                ))
        
        return findings
    
    def _check_deprecated_packages(self, dependencies: List[Dependency], language: str) -> List[Finding]:
        """Check for deprecated or unmaintained packages."""
        findings = []
        
        deprecated = self.DEPRECATED_PACKAGES.get(
            "npm" if language in ["typescript", "javascript"] else "python",
            {}
        )
        
        for dep in dependencies:
            if dep.name.lower() in deprecated:
                findings.append(Finding(
                    title=f"Deprecated package: {dep.name}",
                    description=f"Package '{dep.name}' is deprecated. {deprecated[dep.name.lower()]}",
                    severity="low",
                    category="dependencies",
                    analyzer=self.name,
                    remediation=Remediation(
                        description=deprecated[dep.name.lower()]
                    ),
                    confidence="high"
                ))
        
        return findings
    
    async def _check_vulnerabilities(self, path: Path, server_info: MCPServerInfo) -> List[Finding]:
        """Check for known vulnerabilities using external tools."""
        findings = []
        
        # Try using npm audit for JS/TS projects
        if server_info.language in ["typescript", "javascript"]:
            findings.extend(await self._npm_audit(path))
        
        # Try using pip-audit for Python projects
        elif server_info.language == "python":
            findings.extend(await self._pip_audit(path))
        
        return findings
    
    async def _npm_audit(self, path: Path) -> List[Finding]:
        """Run npm audit and parse results."""
        findings = []
        
        if not (path / "package.json").exists():
            return findings
        
        try:
            result = subprocess.run(
                ["npm", "audit", "--json"],
                cwd=str(path),
                capture_output=True,
                timeout=60
            )
            
            if result.stdout:
                audit_data = json.loads(result.stdout)
                vulnerabilities = audit_data.get("vulnerabilities", {})
                
                for pkg_name, vuln_info in vulnerabilities.items():
                    severity = vuln_info.get("severity", "unknown")
                    severity_map = {
                        "critical": "critical",
                        "high": "high",
                        "moderate": "medium",
                        "low": "low"
                    }
                    
                    via = vuln_info.get("via", [])
                    if isinstance(via, list) and via:
                        first_via = via[0]
                        if isinstance(first_via, dict):
                            title = first_via.get("title", "Unknown vulnerability")
                            url = first_via.get("url", "")
                        else:
                            title = f"Vulnerability in dependency {first_via}"
                            url = ""
                    else:
                        title = "Unknown vulnerability"
                        url = ""
                    
                    findings.append(Finding(
                        title=f"Vulnerable dependency: {pkg_name}",
                        description=f"{title}. Affects version range: {vuln_info.get('range', 'unknown')}",
                        severity=severity_map.get(severity, "medium"),
                        category="dependencies",
                        analyzer=self.name,
                        remediation=Remediation(
                            description=f"Update {pkg_name} to a patched version or run 'npm audit fix'.",
                            references=[url] if url else []
                        ),
                        confidence="high",
                        metadata={"package": pkg_name, "advisory_url": url}
                    ))
        
        except (subprocess.TimeoutExpired, subprocess.SubprocessError, json.JSONDecodeError):
            pass
        except FileNotFoundError:
            # npm not installed
            pass
        
        return findings
    
    async def _pip_audit(self, path: Path) -> List[Finding]:
        """Run pip-audit and parse results."""
        findings = []
        
        # Check if requirements.txt exists
        req_files = list(path.glob("requirements*.txt"))
        if not req_files and not (path / "pyproject.toml").exists():
            return findings
        
        try:
            # Try pip-audit
            result = subprocess.run(
                ["pip-audit", "--format", "json", "-r", str(req_files[0])] if req_files else ["pip-audit", "--format", "json"],
                cwd=str(path),
                capture_output=True,
                timeout=120
            )
            
            if result.stdout:
                audit_data = json.loads(result.stdout)
                
                for vuln in audit_data:
                    pkg_name = vuln.get("name", "unknown")
                    version = vuln.get("version", "unknown")
                    vulns = vuln.get("vulns", [])
                    
                    for v in vulns:
                        vuln_id = v.get("id", "unknown")
                        description = v.get("description", "No description available")
                        fix_versions = v.get("fix_versions", [])
                        
                        severity = "high"  # pip-audit doesn't always provide severity
                        if "critical" in description.lower():
                            severity = "critical"
                        elif "low" in description.lower():
                            severity = "low"
                        
                        findings.append(Finding(
                            title=f"Vulnerable dependency: {pkg_name} ({vuln_id})",
                            description=f"{description}. Current version: {version}",
                            severity=severity,
                            category="dependencies",
                            analyzer=self.name,
                            remediation=Remediation(
                                description=f"Update {pkg_name} to version {', '.join(fix_versions) or 'latest'}.",
                                references=[f"https://pypi.org/project/{pkg_name}/"]
                            ),
                            confidence="high",
                            metadata={"package": pkg_name, "vuln_id": vuln_id, "version": version}
                        ))
        
        except (subprocess.TimeoutExpired, subprocess.SubprocessError, json.JSONDecodeError):
            pass
        except FileNotFoundError:
            # pip-audit not installed
            pass
        
        return findings
    
    def _check_version_pinning(self, path: Path, server_info: MCPServerInfo) -> List[Finding]:
        """Check for packages without proper version pinning."""
        findings = []
        
        if server_info.language == "python":
            req_path = path / "requirements.txt"
            if req_path.exists():
                content = req_path.read_text()
                for i, line in enumerate(content.split('\n'), 1):
                    line = line.strip()
                    if line and not line.startswith('#'):
                        # Check for unpinned versions
                        if '==' not in line and '>=' not in line and '<=' not in line:
                            if not any(op in line for op in ['>=', '<=', '~=', '!=']):
                                findings.append(Finding(
                                    title=f"Unpinned dependency version",
                                    description=f"Dependency '{line}' does not have a pinned version. "
                                               f"This can lead to unexpected updates and potential security issues.",
                                    severity="low",
                                    category="dependencies",
                                    analyzer=self.name,
                                    location=CodeLocation(
                                        file="requirements.txt",
                                        line_start=i,
                                        line_end=i
                                    ),
                                    remediation=Remediation(
                                        description="Pin dependency versions using == for reproducible builds.",
                                        code_example=f"{line}==<specific_version>"
                                    ),
                                    confidence="high"
                                ))
        
        elif server_info.language in ["typescript", "javascript"]:
            pkg_path = path / "package.json"
            if pkg_path.exists():
                try:
                    pkg = json.loads(pkg_path.read_text())
                    for dep_type in ["dependencies", "devDependencies"]:
                        for name, version in pkg.get(dep_type, {}).items():
                            # Check for * or latest
                            if version in ["*", "latest", "x"]:
                                findings.append(Finding(
                                    title=f"Unpinned dependency: {name}",
                                    description=f"Dependency '{name}' uses version '{version}' which can lead to "
                                               f"unexpected updates.",
                                    severity="low",
                                    category="dependencies",
                                    analyzer=self.name,
                                    location=CodeLocation(
                                        file="package.json",
                                        line_start=0,
                                        line_end=0
                                    ),
                                    remediation=Remediation(
                                        description="Pin dependency to a specific version."
                                    ),
                                    confidence="high"
                                ))
                except json.JSONDecodeError:
                    pass
        
        return findings
    
    def _check_lockfile(self, path: Path, server_info: MCPServerInfo) -> List[Finding]:
        """Check for missing or outdated lockfiles."""
        findings = []
        
        if server_info.language == "python":
            # Check for poetry.lock or Pipfile.lock
            has_poetry = (path / "pyproject.toml").exists()
            has_poetry_lock = (path / "poetry.lock").exists()
            has_pipfile = (path / "Pipfile").exists()
            has_pipfile_lock = (path / "Pipfile.lock").exists()
            
            if has_poetry and not has_poetry_lock:
                findings.append(Finding(
                    title="Missing poetry.lock file",
                    description="Project uses Poetry but is missing poetry.lock file. "
                               "This can lead to inconsistent dependency resolution.",
                    severity="low",
                    category="dependencies",
                    analyzer=self.name,
                    remediation=Remediation(
                        description="Run 'poetry lock' to generate a lockfile."
                    ),
                    confidence="high"
                ))
            
            if has_pipfile and not has_pipfile_lock:
                findings.append(Finding(
                    title="Missing Pipfile.lock",
                    description="Project uses Pipenv but is missing Pipfile.lock. "
                               "This can lead to inconsistent dependency resolution.",
                    severity="low",
                    category="dependencies",
                    analyzer=self.name,
                    remediation=Remediation(
                        description="Run 'pipenv lock' to generate a lockfile."
                    ),
                    confidence="high"
                ))
        
        elif server_info.language in ["typescript", "javascript"]:
            has_package = (path / "package.json").exists()
            has_package_lock = (path / "package-lock.json").exists()
            has_yarn_lock = (path / "yarn.lock").exists()
            has_pnpm_lock = (path / "pnpm-lock.yaml").exists()
            
            if has_package and not (has_package_lock or has_yarn_lock or has_pnpm_lock):
                findings.append(Finding(
                    title="Missing package lockfile",
                    description="Project has package.json but no lockfile (package-lock.json, yarn.lock, or pnpm-lock.yaml). "
                               "This can lead to inconsistent dependency resolution.",
                    severity="medium",
                    category="dependencies",
                    analyzer=self.name,
                    remediation=Remediation(
                        description="Run 'npm install' or 'yarn' to generate a lockfile."
                    ),
                    confidence="high"
                ))
        
        return findings
