"""Dependency vulnerability scanner using OSV.dev API."""

import json
import ssl
import urllib.request
import urllib.error
from pathlib import Path
from typing import Any

OSV_API_URL = "https://api.osv.dev/v1/query"
OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch"
REQUEST_TIMEOUT = 15  # seconds


def _parse_package_json_deps(content: str) -> list[dict[str, str]]:
    """Extract dependencies and their versions from package.json content."""
    try:
        data = json.loads(content)
    except (json.JSONDecodeError, ValueError):
        return []

    deps: list[dict[str, str]] = []
    for section in ("dependencies", "devDependencies"):
        section_deps = data.get(section, {})
        if not isinstance(section_deps, dict):
            continue
        for name, version_spec in section_deps.items():
            # Strip range operators to get base version
            version = version_spec.lstrip("^~>=<! ")
            if version and version[0].isdigit():
                deps.append({"name": name, "version": version, "ecosystem": "npm"})
    return deps


def _parse_requirements_txt_deps(content: str) -> list[dict[str, str]]:
    """Extract dependencies from requirements.txt."""
    deps: list[dict[str, str]] = []
    for line in content.splitlines():
        line = line.strip()
        if not line or line.startswith("#") or line.startswith("-"):
            continue
        # Handle formats: package==version, package>=version
        for sep in ("==", ">=", "<=", "~=", "!="):
            if sep in line:
                name, version = line.split(sep, 1)
                name = name.strip().split("[")[0]  # handle extras like package[extra]
                version = version.strip().split(",")[0].split(";")[0].strip()
                if version and version[0].isdigit():
                    deps.append({"name": name, "version": version, "ecosystem": "PyPI"})
                break
    return deps


def parse_manifest_deps(file_path: str, content: str) -> list[dict[str, str]]:
    """Parse dependencies from a manifest file based on filename."""
    name = Path(file_path).name.lower()
    if name == "package.json":
        return _parse_package_json_deps(content)
    if name in ("requirements.txt", "requirements-dev.txt"):
        return _parse_requirements_txt_deps(content)
    return []


def query_osv_batch(deps: list[dict[str, str]]) -> list[dict[str, Any]] | None:
    """Query OSV.dev API for known vulnerabilities in a batch of dependencies.

    Returns a list of findings on success, or None if the API call failed.
    Callers must distinguish None (API error) from [] (no vulnerabilities found).
    """
    if not deps:
        return []

    queries = []
    for dep in deps:
        queries.append({
            "version": dep["version"],
            "package": {
                "name": dep["name"],
                "ecosystem": dep["ecosystem"],
            },
        })

    payload = json.dumps({"queries": queries}).encode("utf-8")
    req = urllib.request.Request(
        OSV_BATCH_URL,
        data=payload,
        headers={"Content-Type": "application/json"},
        method="POST",
    )

    try:
        import certifi
        ssl_ctx = ssl.create_default_context(cafile=certifi.where())
    except ImportError:
        ssl_ctx = ssl.create_default_context()

    result = None
    last_exc: Exception | None = None
    for _attempt in range(3):
        try:
            with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT, context=ssl_ctx) as resp:
                result = json.loads(resp.read().decode("utf-8"))
            break  # success
        except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, json.JSONDecodeError, ssl.SSLError) as exc:
            last_exc = exc
    if result is None:
        return None  # Distinguish API failure from "no vulnerabilities found"

    findings: list[dict[str, Any]] = []
    results_list = result.get("results", [])

    for i, query_result in enumerate(results_list):
        if i >= len(deps):
            break
        vulns = query_result.get("vulns", [])
        if not vulns:
            continue
        dep = deps[i]
        for vuln in vulns:
            vuln_id = vuln.get("id", "")
            summary = vuln.get("summary", "")
            severity_list = vuln.get("severity", [])
            cvss_score = None
            for sev in severity_list:
                if sev.get("type") == "CVSS_V3":
                    score_str = sev.get("score", "")
                    # Extract base score from CVSS vector if present
                    if "/" in score_str:
                        # It's a vector string, not a numeric score
                        pass
                    else:
                        try:
                            cvss_score = float(score_str)
                        except (ValueError, TypeError):
                            pass

            aliases = vuln.get("aliases", [])
            cve_ids = [a for a in aliases if a.startswith("CVE-")]

            findings.append({
                "package": dep["name"],
                "version": dep["version"],
                "ecosystem": dep["ecosystem"],
                "vuln_id": vuln_id,
                "cve_ids": cve_ids,
                "summary": summary or f"Known vulnerability in {dep['name']}@{dep['version']}",
                "cvss_score": cvss_score,
                "severity": _cvss_to_severity(cvss_score),
            })

    return findings


def _cvss_to_severity(score: float | None) -> str:
    """Convert CVSS score to severity label."""
    if score is None:
        return "medium"
    if score >= 9.0:
        return "critical"
    if score >= 7.0:
        return "high"
    if score >= 4.0:
        return "medium"
    return "low"


def scan_dependencies(file_contents: dict[str, str]) -> tuple[list[dict[str, Any]], str | None]:
    """Scan all manifest files in the provided file contents for vulnerable dependencies.

    Args:
        file_contents: Dict mapping file paths to their contents.

    Returns:
        (findings, error) — findings is the list of vulnerability dicts;
        error is a human-readable message when the OSV API was unreachable,
        or None on success.  findings may be empty even on success (no known CVEs).
    """
    all_deps: list[dict[str, str]] = []
    dep_to_file: dict[str, str] = {}

    for file_path, content in file_contents.items():
        deps = parse_manifest_deps(file_path, content)
        for dep in deps:
            key = f"{dep['ecosystem']}:{dep['name']}@{dep['version']}"
            if key not in dep_to_file:
                dep_to_file[key] = file_path
                all_deps.append(dep)

    if not all_deps:
        return [], None

    osv_findings = query_osv_batch(all_deps)

    if osv_findings is None:
        return [], (
            "OSV.dev API unavailable — dependency vulnerability scan was skipped. "
            f"Could not check {len(all_deps)} package(s). "
            "Retry the scan or check your network connection."
        )

    results: list[dict[str, Any]] = []
    for finding in osv_findings:
        key = f"{finding['ecosystem']}:{finding['package']}@{finding['version']}"
        file_path = dep_to_file.get(key, "package.json")
        cve_str = ", ".join(finding["cve_ids"]) if finding["cve_ids"] else finding["vuln_id"]

        osv_url = f"https://osv.dev/vulnerability/{finding['vuln_id']}"
        results.append({
            "ruleId": "a06_vulnerable_dependency",
            "title": f"OWASP A06:2021 (Vulnerable and Outdated Components): Vulnerable Dependency \u2014 {finding['package']}@{finding['version']} ({cve_str})",
            "description": (
                f"The package '{finding['package']}' at version {finding['version']} has a known "
                f"security vulnerability ({cve_str}). {finding['summary']}"
            ),
            "severity": finding["severity"],
            "confidence": "high",
            "owaspCategory": "A06",
            "cweId": "CWE-1395",
            "filePath": file_path,
            "lineStart": 1,
            "recommendation": (
                f"1. Update '{finding['package']}' to a patched version in {file_path}.\n"
                f"2. Run your package manager's install/update command to apply the change.\n"
                f"3. Run tests to verify nothing breaks with the updated version.\n"
                f"Reference: {osv_url}"
            ),
            "referenceLinks": [osv_url],
        })

    return results, None
