"""CVE detector backed by the Google OSV API."""
from __future__ import annotations

from typing import Any

from pyguard.models import Finding, Severity
from pyguard.utils.cache import get_default_cache
from pyguard.utils.osv_client import query_vulnerabilities

_SEVERITY_MAP: dict[str, Severity] = {
    "critical": Severity.CRITICAL,
    "high": Severity.HIGH,
    "medium": Severity.MEDIUM,
    "low": Severity.LOW,
}


def _osv_to_severity(vuln: dict[str, Any]) -> Severity:
    """Best-effort severity extraction from an OSV record."""
    # Check database_specific for NVD CVSS score
    db_specific = vuln.get("database_specific", {})
    if isinstance(db_specific, dict):
        severity_str = str(db_specific.get("severity", "")).lower()
        if severity_str in _SEVERITY_MAP:
            return _SEVERITY_MAP[severity_str]

    # Check affected ecosystem_specific
    for affected in vuln.get("affected", []):
        eco = affected.get("ecosystem_specific", {})
        if isinstance(eco, dict):
            sev = str(eco.get("severity", "")).lower()
            if sev in _SEVERITY_MAP:
                return _SEVERITY_MAP[sev]

    # Fallback based on severity array type
    for s in vuln.get("severity", []):
        sev_score = str(s.get("score", ""))
        # A rough heuristic: if CVSS vector contains C:H/I:H/A:H → HIGH or CRITICAL
        if "C:H" in sev_score and "I:H" in sev_score:
            return Severity.CRITICAL
        if "C:H" in sev_score or "I:H" in sev_score or "A:H" in sev_score:
            return Severity.HIGH

    return Severity.MEDIUM  # safe default


def _cve_ids(vuln: dict[str, Any]) -> list[str]:
    ids: list[str] = []
    for alias in vuln.get("aliases", []):
        if alias.startswith("CVE-"):
            ids.append(alias)
    vid = vuln.get("id", "")
    if vid.startswith("CVE-"):
        ids.insert(0, vid)
    return ids


async def find_cves(package_name: str, version: str) -> list[Finding]:
    if version in ("latest", "unknown", ""):
        return []

    cache = get_default_cache()
    cache_key = f"osv:{package_name}:{version}"
    cached = cache.get(cache_key)
    vulns: list[dict[str, Any]]
    if cached is not None:
        vulns = cached
    else:
        vulns = await query_vulnerabilities(package_name, version)
        cache.set(cache_key, vulns, ttl_seconds=3600)

    findings: list[Finding] = []
    for vuln in vulns:
        severity = _osv_to_severity(vuln)
        cve_ids = _cve_ids(vuln)
        primary_cve = cve_ids[0] if cve_ids else vuln.get("id", "unknown")
        summary = vuln.get("summary") or vuln.get("details", "No details available.")[:200]
        fixed_versions = _extract_fixed_versions(vuln, package_name)
        remediation = (
            f"Upgrade to {fixed_versions[0]}" if fixed_versions else "No fixed version available yet; consider removing the package."
        )
        findings.append(
            Finding(
                severity=severity,
                title=f"{primary_cve}: {summary[:80]}",
                description=summary,
                layer="static",
                package=package_name,
                version=version,
                evidence={
                    "osv_id": vuln.get("id", ""),
                    "aliases": cve_ids,
                    "fixed_versions": fixed_versions,
                },
                confidence=0.95,
                category="cve",
                cve_id=primary_cve,
                remediation=remediation,
            )
        )
    return findings


def _extract_fixed_versions(vuln: dict[str, Any], package_name: str) -> list[str]:
    """Extract the list of fixed versions from an OSV record."""
    fixed: list[str] = []
    for affected in vuln.get("affected", []):
        pkg = affected.get("package", {})
        if pkg.get("ecosystem", "").lower() != "pypi":
            continue
        if pkg.get("name", "").lower() != package_name.lower():
            continue
        for r in affected.get("ranges", []):
            for event in r.get("events", []):
                if "fixed" in event:
                    fixed.append(event["fixed"])
    return sorted(set(fixed))
