from typing import Any

from cvss.parser import parse_cvss_from_text
from pydantic import Field, HttpUrl

from vulnsearch.models.vulnerability import (
    Affected,
    Event,
    PackageInfo,
    Range,
    SeretoBaseModel,
    Severity,
    Vulnerability,
)
from vulnsearch.risk import Risk


class NvdSummary(SeretoBaseModel, from_attributes=True):
    """Model for NVD summary information."""

    lang: str | None = None
    value: str | None = None


class NvdReference(SeretoBaseModel, from_attributes=True):
    """Model for NVD reference information."""

    url: HttpUrl | None = None
    tags: list[str] | None = None


class NvdVulnerability(SeretoBaseModel, from_attributes=True):
    """Model for NVD vulnerability information."""

    id: str
    vulnStatus: str | None = None
    descriptions: list[NvdSummary] = Field(default_factory=list)
    url: HttpUrl | None = None
    references: list[NvdReference] = Field(default_factory=list)
    configurations: Any = None
    metrics: Any = None

    def to_sereto(self) -> Vulnerability:
        """Convert NVD entry into unified Vulnerability."""
        summary = _extract_desc(self.descriptions)
        refs = _extract_references(self.url, self.references)
        affected = _extract_affected(self.configurations)
        severity = _extract_severity(self.metrics)

        return Vulnerability(
            id=self.id,
            source="NVD",
            vuln_status=self.vulnStatus,
            aliases=[],
            summary=summary,
            references=refs,
            affected=affected,
            severity=severity,
        )


class NvdResult(SeretoBaseModel):
    """Normalized container for NVD results."""

    vulnerabilities: list[NvdVulnerability] = Field(default_factory=list)

    @classmethod
    def model_validate_nvd(cls, data):
        return cls(vulnerabilities=[NvdVulnerability.model_validate(v) for v in data])

    def to_unified_list(self) -> list[Vulnerability]:
        """Convert NVD entry into unified Vulnerability."""
        allowed_statuses = {
            "analyzed",
            "modified",
            "received",
            "awaiting analysis",
            "deferred",
        }

        result: list[Vulnerability] = []

        for cve in self.vulnerabilities:
            if not cve.id or not cve.id.startswith("CVE-"):
                continue
            status = (cve.vulnStatus or "").lower()
            if status and status not in allowed_statuses:
                continue
            result.append(cve.to_sereto())
        return result


# -------------
# Normalizers
# -------------
def _extract_desc(desc: list[NvdSummary]) -> str | None:
    """Extract the first English description."""
    if not desc:
        return None
    for d in desc:
        if d.lang.lower() == "en":
            return d.value.strip()
    return None


def _extract_references(
    url: HttpUrl | None, refs: list[NvdReference] | None
) -> set[HttpUrl]:
    """Extract references."""
    references = set()
    if url:
        references.add(url)

    for r in refs:
        if not r.url:
            continue
        tags = getattr(r, "tags", None)
        if not tags:
            continue
        if "Vendor Advisory" in tags or "Third Party Advisory" in tags:
            references.add(r.url)
    return references


def _extract_affected(configs: Any) -> list[Affected]:
    """Extract affected packages."""
    affected: list[Affected] = []
    if not configs:
        return []

    for config in configs:
        nodes = getattr(config, "nodes")
        if not nodes:
            continue
        for node in nodes:
            for cm in getattr(node, "cpeMatch", []):
                if getattr(cm, "vulnerable", True) is False:
                    continue
                cpe = getattr(cm, "criteria") or getattr(cm, "cpe23Uri")
                if not cpe:
                    continue

                parts = cpe.split(":")
                part = parts[2] if len(parts) > 2 else None
                vendor = parts[3] if len(parts) > 3 else None
                product = parts[4] if len(parts) > 4 else None
                version = parts[5] if len(parts) > 5 else None

                events: list[Event] = []

                introduced = getattr(cm, "versionStartIncluding", None)
                limit = getattr(cm, "versionStartExcluding", None)
                fixed = getattr(cm, "versionEndExcluding", None)
                last_affected = getattr(cm, "versionEndIncluding", None)

                events.append(
                    Event(
                        introduced=introduced,
                        limit=limit,
                        fixed=fixed,
                        last_affected=last_affected,
                    )
                )

                versions: list[str] = []
                ver = getattr(cm, "version", None)
                if not ver:
                    ver = version
                if ver and ver != "*":
                    versions.append(ver)

                affected.append(
                    Affected(
                        package=PackageInfo(
                            cpe=cpe, name=product, vendor=vendor, part=part
                        ),
                        ranges=[Range(type="CPE", events=events)],
                        versions=versions,
                    )
                )
    return affected


def _severity_from_metric(metric) -> Risk:
    data = getattr(metric, "cvssData", None)
    if not data:
        return Risk.none

    label = getattr(data, "baseSeverity", None)
    base_score = getattr(data, "baseScore", None)
    vector = getattr(data, "vectorString", None)

    if label:
        try:
            severity = Risk(label)
        except ValueError:
            severity = Risk.none
    else:
        severity = Risk.none

    if severity is Risk.none and base_score is not None:
        severity = Risk.from_score(base_score)

    if severity is Risk.none and vector is not None:
        cvss = parse_cvss_from_text(vector)
        parsed_label = cvss[0].severeties()[0]
        try:
            severity = Risk(parsed_label)
        except ValueError:
            severity = Risk.none
    return severity


def _extract_severity(metrics: Any) -> Severity:
    """Extract severity."""
    severity = Risk.none

    for key in ("cvssMetricV31", "cvssMetricV30", "cvssMetricV2"):
        lst = getattr(metrics, key, None)
        if not lst:
            continue

        primary = next(
            (
                m
                for m in lst
                if getattr(m, "type", "") and getattr(m, "type").lower() == "primary"
            ),
            None,
        )

        if primary is not None:
            risk = _severity_from_metric(primary)
            if risk is not Risk.none:
                severity = risk
                break

        local_best_severity = Risk.none

        for metric in lst:
            risk = _severity_from_metric(metric)
            if risk > local_best_severity:
                local_best_severity = risk

        if local_best_severity is not Risk.none:
            severity = local_best_severity
            break

    return Severity(severity=severity.value, score=severity.to_score())
