import asyncio
from typing import Any

import httpx
import nvdlib
import requests

from vulnsearch.exceptions import UtilRuntimeError, UtilValueError
from vulnsearch.limits import RateLimiter
from vulnsearch.models.nvd import NvdResult
from vulnsearch.models.osv import OsvResult
from vulnsearch.models.vulnerability import Vulnerability

# NVD public API limit: 5 requests / 30 seconds (without API key).
NVD_LIMITER = RateLimiter(max_calls=5, period=30.0)


# --------------------
# OSV (sync)
# --------------------
def osv_search_by_id(vulnerability_id: str) -> Vulnerability:
    """
    Search OSV by vulnerability ID and return a single matching Vulnerability.
    """
    response = httpx.get(f"https://api.osv.dev/v1/vulns/{vulnerability_id}")

    if response.status_code != httpx.codes.OK:
        raise UtilRuntimeError(
            f"request failed with HTTP status {response.status_code}"
        )

    vulnerability = OsvResult.model_validate_osv(response.json()).to_unified_list()[0]

    return vulnerability


def osv_search_by_id_aliases(vulnerability_id: str) -> list[Vulnerability]:
    """
    Search for vulnerabilities and its aliases by id in the OSV(only with clear entries).
    """
    vulnerabilities: list[Vulnerability] = []

    # Fetch main vulnerability
    main_vuln = osv_search_by_id(vulnerability_id)
    vulnerabilities.append(main_vuln)

    # Fetch related vulnerabilities by aliases
    for alias in main_vuln[0].aliases:
        alias_vuln = osv_search_by_id(alias)

        if alias_vuln:
            vulnerabilities.append(alias_vuln)

    return vulnerabilities


def osv_search_by_cpe(
    name: str, version: str | None, ecosystem: str | None
) -> list[Vulnerability]:
    """
    Search for vulnerabilities by CPE identifier in the OSV(only with clear entries).
    """
    vulnerabilities: list[Vulnerability] = []
    query: dict[str, Any] = {"package": {"name": name}}

    if version:
        query["version"] = version

    if ecosystem:
        query["package"]["ecosystem"] = ecosystem

    response = httpx.post("https://api.osv.dev/v1/query", json=query)

    if response.status_code != httpx.codes.OK:
        raise UtilRuntimeError(
            f"request failed with HTTP status {response.status_code}"
        )

    if not response.json():
        return vulnerabilities

    vulnerabilities = OsvResult.model_validate_osv(response.json()).to_unified_list()

    return vulnerabilities


def osv_search_by_purl(purl: str) -> list[Vulnerability]:
    """Search OSV by PURL."""
    vulnerabilities: list[Vulnerability] = []
    query: dict[str, Any] = {"package": {"purl": purl}}

    response = httpx.post("https://api.osv.dev/v1/query", json=query)

    if response.status_code != httpx.codes.OK:
        raise UtilRuntimeError(
            f"request failed with HTTP status {response.status_code}"
        )

    if not response.json():
        return vulnerabilities

    vulnerabilities = OsvResult.model_validate_osv(response.json()).to_unified_list()

    return vulnerabilities


# --------------------
# NVD(sync)
# --------------------
def nvd_search_by_id(vulnerability_id: str) -> list[Vulnerability]:
    """
    Search for vulnerabilities by CVE ID in the NVD and return the single matching result.
    """
    vulnerabilities: list[Vulnerability] = []

    if not vulnerability_id.startswith("CVE-"):
        raise UtilValueError(f"Invalid CVE: {vulnerability_id}")

    try:
        response = nvdlib.searchCVE(cveId=vulnerability_id)
    except requests.RequestException as e:
        raise UtilRuntimeError("NVD API request failed") from e

    if not response:
        return vulnerabilities

    vulnerabilities = NvdResult.model_validate_nvd(response).to_unified_list()
    return vulnerabilities


def nvd_search_by_cpe(name: str, version: str | None) -> list[Vulnerability]:
    """
    Search for vulnerabilities by CPE identifier in the NVD(only with clear entries).
    """
    if not name or "/" in name:
        return []

    vulnerabilities: list[Vulnerability] = []

    cpe = ":".join(["cpe:2.3:*:*", name if name else "*", version if version else "*"])

    try:
        response = nvdlib.searchCVE(virtualMatchString=cpe)
    except requests.RequestException as e:
        raise UtilRuntimeError("NVD API request failed") from e

    if not response:
        return vulnerabilities

    vulnerabilities = NvdResult.model_validate_nvd(response).to_unified_list()
    return vulnerabilities


def search_cpe_by_keyword(keyword: str) -> dict:
    """
    Provide a list of potentially vulnerable products and their versions, using a keyword for searching.
    """
    cpe_results: dict[str, set[str]] = {}

    cpe_list = nvdlib.searchCPE(keywordSearch=keyword)
    for cpe in cpe_list:
        cpe_str = getattr(cpe, "cpeName")
        if cpe_str is None:
            continue

        parts = cpe_str.split(":")
        base = ":".join(parts[:5])

        if base not in cpe_results:
            cpe_results[base] = set()
        cpe_results[base].add(cpe_str)

    return cpe_results


def search_by_id(vulnerability_id: str) -> list[Vulnerability]:
    """
    Search for vulnerabilities by id across OSV and NVD.
    """
    # NVD supports only CVE-style id
    if vulnerability_id.startswith("CVE-"):
        osv = osv_search_by_id_aliases(vulnerability_id)
        if osv:
            return osv
        else:
            return nvd_search_by_id(vulnerability_id)

    return osv_search_by_id_aliases(vulnerability_id)


# --------------------
# Async
# --------------------
async def search_by_cpe_async(
    name: str, version: str | None, ecosystem: str | None
) -> list[Vulnerability]:
    """
    Search for vulnerabilities by CPE identifier across OSV and NVD.
    """
    vulnerabilities: list[Vulnerability] = []

    osv_task = osv_search_by_cpe_async(name, version, ecosystem)
    nvd_task = nvd_search_by_cpe_async(name, version)

    osv_result, nvd_result = await asyncio.gather(osv_task, nvd_task)

    if osv_result:
        vulnerabilities.extend(osv_result)

    if nvd_result:
        vulnerabilities.extend(nvd_result)

    return vulnerabilities


async def osv_search_by_purl_async(purl: str) -> list[Vulnerability]:
    """Async wrapper for osv_search_by_purl."""
    return await asyncio.to_thread(osv_search_by_purl, purl)


async def osv_search_by_cpe_async(
    name: str, version: str | None, ecosystem: str | None
) -> list[Vulnerability]:
    """Async wrapper for osv_search_by_cpe."""
    return await asyncio.to_thread(osv_search_by_cpe, name, version, ecosystem)


async def nvd_search_by_cpe_async(
    name: str, version: str | None
) -> list[Vulnerability]:
    """Async wrapper for nvd_search_by_cpe."""
    return await NVD_LIMITER.run_sync(lambda: nvd_search_by_cpe(name, version))


def search_by_cpe(
    name: str,
    version: str | None,
    ecosystem: str | None,
) -> list["Vulnerability"]:
    """Sync facade for async search_by_cpe_async."""
    return asyncio.run(search_by_cpe_async(name, version, ecosystem))
