"""Native vulnerability scanners wrapper for npm audit and OSV-Scanner."""

import json
import os
import platform
import shutil
import sqlite3
import subprocess
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from theauditor.utils.logging import logger

IS_WINDOWS = platform.system() == "Windows"


SEVERITY_RANK = {"critical": 4, "high": 3, "medium": 2, "low": 1, "": 0, "unknown": 0}


class VulnerabilityScanner:
    """Main vulnerability scanner orchestrator."""

    def __init__(self, db_path: str, offline: bool = False):
        """Initialize scanner."""
        self.db_path = db_path
        self.offline = offline
        self._package_file_map = {}

        self.tool_status = {
            "npm-audit": {"status": "not_run", "error": None, "findings_count": 0},
            "osv-scanner": {"status": "not_run", "error": None, "findings_count": 0},
        }

        try:
            self.conn = sqlite3.connect(db_path, timeout=60)
            self.conn.execute("PRAGMA journal_mode=WAL")
            self.cursor = self.conn.cursor()
        except sqlite3.Error as e:
            logger.error(f"Failed to connect to database: {e}")
            raise

    def scan(self) -> list[dict[str, Any]]:
        """Main entry point - run all detection sources and cross-reference."""
        logger.info("Starting vulnerability scan...")

        packages = self._load_packages_from_db()
        logger.info(f"Loaded {len(packages)} packages from database")

        if not packages:
            logger.warning("No packages found in database - npm-audit will be skipped")

        logger.info("Running npm audit...")
        npm_findings = self._run_npm_audit()
        logger.info(f"npm audit found {len(npm_findings)} vulnerabilities")

        logger.info("Running OSV-Scanner...")
        osv_findings = self._run_osv_scanner()
        logger.info(f"OSV-Scanner found {len(osv_findings)} vulnerabilities")

        logger.info("Cross-referencing findings...")
        findings_by_source = {
            "npm-audit": npm_findings,
            "osv-scanner": osv_findings,
        }
        validated = self._cross_reference(findings_by_source)
        logger.info(f"Validated {len(validated)} unique vulnerabilities")

        logger.info("Writing findings to database...")
        self._write_to_db(validated)

        logger.info("Vulnerability scan completed")
        return validated

    def _load_packages_from_db(self) -> list[dict[str, str]]:
        """Load packages from package_configs table."""
        packages = []

        self.cursor.execute("""
            SELECT package_name, version, file_path
            FROM package_configs
        """)

        for pkg_name, version, file_path in self.cursor.fetchall():
            if "package.json" in file_path:
                manager = "npm"
            elif "requirements.txt" in file_path or "pyproject.toml" in file_path:
                manager = "py"
            else:
                manager = "unknown"

            packages.append(
                {
                    "name": pkg_name,
                    "version": version or "unknown",
                    "manager": manager,
                    "file": file_path,
                }
            )

            cache_key = f"{manager}:{pkg_name}"
            self._package_file_map[cache_key] = file_path

        return packages

    def _run_npm_audit(self) -> list[dict[str, Any]]:
        """Run npm audit using sandboxed node runtime."""
        self.tool_status["npm-audit"]["status"] = "running"
        vulnerabilities = []

        try:
            project_root = Path.cwd()
            package_json = project_root / "package.json"
            if not package_json.exists():
                self.tool_status["npm-audit"]["status"] = "success"
                self.tool_status["npm-audit"]["findings_count"] = 0
                return vulnerabilities

            node_modules = project_root / "node_modules"
            if not node_modules.exists():
                self.tool_status["npm-audit"]["status"] = "success"
                self.tool_status["npm-audit"]["findings_count"] = 0
                return vulnerabilities

            sandbox_base = project_root / ".auditor_venv" / ".theauditor_tools"
            node_runtime = sandbox_base / "node-runtime"

            if IS_WINDOWS:
                node_exe = node_runtime / "node.exe"
                npm_cli = node_runtime / "node_modules" / "npm" / "bin" / "npm-cli.js"
                if npm_cli.exists():
                    npm_cmd = [str(node_exe), str(npm_cli), "audit", "--json"]
                else:
                    npm_cmd_path = node_runtime / "npm.cmd"
                    if npm_cmd_path.exists():
                        npm_cmd = [str(npm_cmd_path), "audit", "--json"]
                    else:
                        self.tool_status["npm-audit"]["status"] = "success"
                        self.tool_status["npm-audit"]["findings_count"] = 0
                        return vulnerabilities
            else:
                node_exe = node_runtime / "bin" / "node"
                npm_exe = node_runtime / "bin" / "npm"
                if npm_exe.exists():
                    npm_cmd = [str(npm_exe), "audit", "--json"]
                else:
                    self.tool_status["npm-audit"]["status"] = "success"
                    self.tool_status["npm-audit"]["findings_count"] = 0
                    return vulnerabilities

            if not node_exe.exists():
                self.tool_status["npm-audit"]["status"] = "success"
                self.tool_status["npm-audit"]["findings_count"] = 0
                return vulnerabilities

            result = subprocess.run(
                npm_cmd,
                cwd=str(project_root),
                capture_output=True,
                text=True,
                timeout=60,
                shell=False,
            )

            if result.stdout:
                audit_data = json.loads(result.stdout)

                if "vulnerabilities" in audit_data:
                    for pkg_name, pkg_data in audit_data["vulnerabilities"].items():
                        if not pkg_data.get("via"):
                            continue

                        for via_item in pkg_data.get("via", []):
                            if isinstance(via_item, str):
                                continue

                            if isinstance(via_item, dict):
                                severity = via_item.get("severity", "")

                                vuln_id = via_item.get("cve")
                                if not vuln_id:
                                    vuln_id = via_item.get("ghsa")
                                if not vuln_id:
                                    vuln_id = via_item.get("source", f"npm-audit-{pkg_name}")

                                aliases = []
                                if via_item.get("cve"):
                                    aliases.append(via_item["cve"])
                                if via_item.get("ghsa"):
                                    aliases.append(via_item["ghsa"])

                                advisory_url = via_item.get("url", "")
                                if advisory_url and not aliases:
                                    import re

                                    ghsa_match = re.search(
                                        r"GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}", advisory_url
                                    )
                                    if ghsa_match:
                                        aliases.append(ghsa_match.group(0))

                                fixed_version = None
                                if pkg_data.get("fixAvailable"):
                                    fix_info = pkg_data["fixAvailable"]
                                    if isinstance(fix_info, dict) and "version" in fix_info:
                                        fixed_version = fix_info["version"]

                                affected_range = pkg_data.get("range", "")
                                current_version = (
                                    affected_range.split(" ")[0].lstrip("<>=")
                                    if affected_range
                                    else ""
                                )

                                cwe_ids_full = via_item.get("cwe", [])
                                cwe_primary = cwe_ids_full[0] if cwe_ids_full else ""

                                cve_id = next((a for a in aliases if a.startswith("CVE-")), None)
                                ghsa_id = next((a for a in aliases if a.startswith("GHSA-")), None)

                                vulnerability = {
                                    "package": pkg_name,
                                    "version": current_version,
                                    "manager": "npm",
                                    "vulnerability_id": vuln_id,
                                    "severity": severity,
                                    "summary": via_item.get("title", "No summary available"),
                                    "details": via_item.get("overview", ""),
                                    "aliases": aliases,
                                    "published": via_item.get("created", ""),
                                    "modified": via_item.get("updated", ""),
                                    "references": [
                                        {"type": "ADVISORY", "url": via_item.get("url", "")}
                                    ]
                                    if via_item.get("url")
                                    else [],
                                    "affected_ranges": [pkg_data.get("range", "")]
                                    if pkg_data.get("range")
                                    else [],
                                    "fixed_version": fixed_version,
                                    "cwe": cwe_primary,
                                    "cwe_ids": cwe_ids_full,
                                    "cve_id": cve_id,
                                    "ghsa_id": ghsa_id,
                                    "source": "npm audit",
                                }

                                vulnerabilities.append(vulnerability)

            self.tool_status["npm-audit"]["status"] = "success"
            self.tool_status["npm-audit"]["findings_count"] = len(vulnerabilities)
            return vulnerabilities

        except Exception as e:
            self.tool_status["npm-audit"]["status"] = "error"
            self.tool_status["npm-audit"]["error"] = str(e)
            logger.error(f"npm-audit failed: {e}")
            return []

    def _find_osv_scanner(self) -> str:
        """Find bundled osv-scanner binary."""
        tools_dir = Path(".auditor_venv/.theauditor_tools/osv-scanner")

        binary = tools_dir / "osv-scanner.exe" if IS_WINDOWS else tools_dir / "osv-scanner"

        if binary.exists():
            return str(binary)

        system_osv = shutil.which("osv-scanner")
        if system_osv:
            return system_osv

        raise FileNotFoundError(
            f"osv-scanner not found at {binary} or in system PATH. "
            f"Run 'aud setup-ai --target .' to install vulnerability scanners."
        )

    def _run_osv_scanner(self) -> list[dict[str, Any]]:
        """Run OSV-Scanner using bundled binary."""
        self.tool_status["osv-scanner"]["status"] = "running"
        vulnerabilities = []

        try:
            project_root = Path.cwd()
            lockfiles = []

            if (project_root / "package-lock.json").exists():
                lockfiles.extend(["-L", str(project_root / "package-lock.json")])
            elif (project_root / "yarn.lock").exists():
                lockfiles.extend(["-L", str(project_root / "yarn.lock")])

            if (project_root / "requirements.txt").exists():
                lockfiles.extend(["-L", str(project_root / "requirements.txt")])
            elif (project_root / "Pipfile.lock").exists():
                lockfiles.extend(["-L", str(project_root / "Pipfile.lock")])

            if (project_root / "Cargo.lock").exists():
                lockfiles.extend(["-L", str(project_root / "Cargo.lock")])

            if not lockfiles:
                self.tool_status["osv-scanner"]["status"] = "success"
                self.tool_status["osv-scanner"]["findings_count"] = 0
                return vulnerabilities

            osv_scanner_path = self._find_osv_scanner()

            db_dir = Path(".auditor_venv/.theauditor_tools/osv-scanner/db")
            env = {**os.environ, "OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY": str(db_dir)}

            cmd = [osv_scanner_path, "scan"] + lockfiles + ["--format", "json"]

            cmd.append("--offline-vulnerabilities")

            result = subprocess.run(
                cmd, cwd=str(project_root), capture_output=True, text=True, timeout=120, env=env
            )

            if result.stdout:
                try:
                    scan_data = json.loads(result.stdout)

                    for result_item in scan_data.get("results", []):
                        for package_vuln in result_item.get("packages", []):
                            pkg_info = package_vuln.get("package", {})
                            pkg_name = pkg_info.get("name", "")
                            pkg_version = pkg_info.get("version", "")
                            pkg_ecosystem = pkg_info.get("ecosystem", "")

                            if pkg_ecosystem in ["npm", "NPM"]:
                                manager = "npm"
                            elif pkg_ecosystem in ["PyPI", "Python"]:
                                manager = "py"
                            elif pkg_ecosystem in ["crates.io", "Rust"]:
                                manager = "rust"
                            else:
                                manager = "unknown"

                            for vuln in package_vuln.get("vulnerabilities", []):
                                vuln_id = vuln.get("id", f"osv-{pkg_name}")

                                severity = ""
                                if vuln.get("database_specific", {}).get("severity"):
                                    severity = vuln["database_specific"]["severity"]

                                cwe_ids_full = []
                                db_specific = vuln.get("database_specific", {})
                                if db_specific and "cwe_ids" in db_specific:
                                    raw_cwe_ids = db_specific["cwe_ids"]
                                    if isinstance(raw_cwe_ids, list):
                                        cwe_ids_full = raw_cwe_ids

                                cwe_primary = cwe_ids_full[0] if cwe_ids_full else ""

                                aliases = vuln.get("aliases", [])
                                cve_id = next((a for a in aliases if a.startswith("CVE-")), None)
                                ghsa_id = next((a for a in aliases if a.startswith("GHSA-")), None)

                                vulnerability = {
                                    "package": pkg_name,
                                    "version": pkg_version,
                                    "manager": manager,
                                    "vulnerability_id": vuln_id,
                                    "severity": severity.lower() if severity else "",
                                    "summary": vuln.get("summary", "No summary available"),
                                    "details": vuln.get("details", ""),
                                    "aliases": aliases,
                                    "published": vuln.get("published", ""),
                                    "modified": vuln.get("modified", ""),
                                    "references": vuln.get("references", []),
                                    "affected_ranges": [],
                                    "fixed_version": None,
                                    "cwe": cwe_primary,
                                    "cwe_ids": cwe_ids_full,
                                    "cve_id": cve_id,
                                    "ghsa_id": ghsa_id,
                                    "source": "OSV-Scanner",
                                }

                                vulnerabilities.append(vulnerability)

                except json.JSONDecodeError:
                    pass

            self.tool_status["osv-scanner"]["status"] = "success"
            self.tool_status["osv-scanner"]["findings_count"] = len(vulnerabilities)
            return vulnerabilities

        except Exception as e:
            self.tool_status["osv-scanner"]["status"] = "error"
            self.tool_status["osv-scanner"]["error"] = str(e)
            logger.error(f"osv-scanner failed: {e}")
            return []

    def _cross_reference(
        self, findings_by_source: dict[str, list[dict[str, Any]]]
    ) -> list[dict[str, Any]]:
        """Cross-reference findings from all available sources for validation."""
        vuln_groups: dict[str, list[tuple[dict[str, Any], str]]] = {}

        for source_name, findings in findings_by_source.items():
            if not findings:
                continue

            for finding in findings:
                vuln_id = finding.get("vulnerability_id", "")
                if not vuln_id:
                    continue

                aliases = finding.get("aliases", []) or []
                matched_id: str | None = None

                for existing_id, existing_findings in vuln_groups.items():
                    if vuln_id == existing_id or existing_id in aliases:
                        matched_id = existing_id
                        break

                    for existing_finding, _ in existing_findings:
                        existing_aliases = existing_finding.get("aliases", []) or []
                        if (
                            vuln_id in existing_aliases
                            or any(alias in existing_aliases for alias in aliases)
                            or existing_id in aliases
                        ):
                            matched_id = existing_id
                            break

                    if matched_id:
                        break

                target_id = matched_id or vuln_id
                vuln_groups.setdefault(target_id, []).append((finding, source_name))

        validated_findings: list[dict[str, Any]] = []

        for vuln_id, findings_list in vuln_groups.items():
            sources = sorted({source for _, source in findings_list})
            source_count = len(sources)

            if source_count >= 3:
                confidence = 1.0
            elif source_count == 2:
                confidence = 0.9
            else:
                confidence = 0.7

            source_priority = {"osv-scanner": 0, "npm-audit": 1}
            findings_sorted = sorted(
                findings_list, key=lambda item: source_priority.get(item[1], 9)
            )
            base_finding = findings_sorted[0][0].copy()

            severities = [f.get("severity", "").lower() for f, _ in findings_list]
            highest_severity = max(
                severities, key=lambda s: SEVERITY_RANK.get(s, 0), default="medium"
            )

            all_aliases = set()
            for finding, _ in findings_list:
                all_aliases.update(finding.get("aliases", []) or [])

            all_references: list[Any] = []
            for finding, _ in findings_list:
                all_references.extend(finding.get("references", []) or [])

            validated_findings.append(
                {
                    "package": base_finding.get("package", ""),
                    "version": base_finding.get("version", ""),
                    "manager": base_finding.get("manager", ""),
                    "file": base_finding.get("file", "package.json"),
                    "vulnerability_id": vuln_id,
                    "severity": highest_severity,
                    "title": base_finding.get("summary", "No title"),
                    "summary": base_finding.get("summary", ""),
                    "details": base_finding.get("details", ""),
                    "aliases": list(all_aliases),
                    "published": base_finding.get("published", ""),
                    "modified": base_finding.get("modified", ""),
                    "references": all_references[:5],
                    "affected_ranges": base_finding.get("affected_ranges", []),
                    "fixed_version": base_finding.get("fixed_version"),
                    "cwe": base_finding.get("cwe", ""),
                    "cwe_ids": base_finding.get("cwe_ids", []),
                    "cve_id": base_finding.get("cve_id"),
                    "ghsa_id": base_finding.get("ghsa_id"),
                    "confidence": confidence,
                    "sources": sources,
                    "source_count": source_count,
                }
            )

        return validated_findings

    def _write_to_db(self, findings: list[dict[str, Any]]):
        """Write findings to findings_consolidated table."""
        if not findings:
            return

        timestamp = datetime.now(UTC).isoformat()

        for finding in findings:
            cache_key = f"{finding.get('manager', 'unknown')}:{finding.get('package', 'unknown')}"
            file_path = self._package_file_map.get(cache_key)

            if not file_path:
                file_path = "requirements.txt" if finding.get("manager") == "py" else "package.json"

            details = {
                "cwe_ids": finding.get("cwe_ids", []),
                "cve_id": finding.get("cve_id"),
                "ghsa_id": finding.get("ghsa_id"),
                "aliases": finding.get("aliases", []),
                "references": finding.get("references", [])[:5],
                "source_count": finding.get("source_count", 1),
                "sources": finding.get("sources", []),
                "confidence": finding.get("confidence", 0.7),
            }

            self.cursor.execute(
                """
                INSERT INTO findings_consolidated
                (file, line, column, rule, tool, message, severity, category,
                 confidence, code_snippet, cwe, timestamp, details_json)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
                (
                    file_path,
                    0,
                    None,
                    finding.get("vulnerability_id", "UNKNOWN"),
                    "vulnerability_scanner",
                    finding.get("summary", finding.get("title", "No summary")),
                    finding.get("severity", "medium"),
                    "dependency",
                    finding.get("confidence", 0.7),
                    f"{finding.get('package', 'unknown')}@{finding.get('version', 'unknown')}",
                    finding.get("cwe", ""),
                    timestamp,
                    json.dumps(details),
                ),
            )

        self.conn.commit()
        logger.info(f"Wrote {len(findings)} vulnerabilities to findings_consolidated table")


def scan_dependencies(
    deps_list: list[dict] | None = None, offline: bool = False
) -> list[dict[str, Any]]:
    """Scan dependencies for vulnerabilities (database-first architecture)."""
    if deps_list is not None:
        logger.warning("deps_list parameter is deprecated and ignored")
        logger.warning("Scanner now reads from database (package_configs table)")

    try:
        db_path = Path("./.pf/repo_index.db")

        if not db_path.exists():
            logger.warning("Database not found at .pf/repo_index.db")
            logger.warning("Run 'aud full' first to populate dependency information")
            return []

        scanner = VulnerabilityScanner(str(db_path), offline=offline)
        findings = scanner.scan()

        return findings

    except FileNotFoundError as e:
        logger.error(f"Scanner tool not found: {e}")
        logger.error("Run 'aud setup-ai --target .' to install vulnerability scanners")
        return []

    except Exception as e:
        logger.error(f"Vulnerability scan failed: {e}")
        import traceback

        logger.error(traceback.format_exc())
        raise


def format_vulnerability_report(vulnerabilities: list[dict]) -> str:
    """Format vulnerabilities as human-readable report."""
    if not vulnerabilities:
        return "[OK] No vulnerabilities found"

    severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
    for vuln in vulnerabilities:
        severity = vuln.get("severity", "unknown").lower()
        if severity in severity_counts:
            severity_counts[severity] += 1

    report_lines = []
    report_lines.append("=" * 60)
    report_lines.append("VULNERABILITY SCAN RESULTS")
    report_lines.append("=" * 60)
    report_lines.append("")
    report_lines.append(f"Total Vulnerabilities: {len(vulnerabilities)}")
    report_lines.append("")
    report_lines.append("Severity Breakdown:")
    report_lines.append(f"  CRITICAL: {severity_counts['critical']}")
    report_lines.append(f"  HIGH:     {severity_counts['high']}")
    report_lines.append(f"  MEDIUM:   {severity_counts['medium']}")
    report_lines.append(f"  LOW:      {severity_counts['low']}")
    report_lines.append("")
    report_lines.append("=" * 60)
    report_lines.append("FINDINGS:")
    report_lines.append("=" * 60)

    by_package = {}
    for vuln in vulnerabilities:
        package = vuln.get("package", "unknown")
        if package not in by_package:
            by_package[package] = []
        by_package[package].append(vuln)

    for package, findings in sorted(by_package.items()):
        report_lines.append("")
        report_lines.append(f"Package: {package}")
        report_lines.append("-" * 60)

        for finding in findings:
            vuln_id = finding.get("vulnerability_id", "UNKNOWN")
            severity = finding.get("severity", "unknown").upper()
            title = finding.get("title", "No title")
            confidence = finding.get("confidence", 0)
            sources = finding.get("sources", [])

            report_lines.append(f"  [{severity}] {vuln_id}: {title}")
            report_lines.append(
                f"    Confidence: {confidence:.1f} (found by: {', '.join(sources)})"
            )

            if finding.get("fixed_version"):
                report_lines.append(f"    Fix: Upgrade to {finding['fixed_version']}")

            if finding.get("references"):
                report_lines.append(f"    References: {finding['references'][0]}")

    report_lines.append("")
    report_lines.append("=" * 60)

    return "\n".join(report_lines)
