from typing import Literal

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 OsvEvent(SeretoBaseModel):
    """OSV event information."""

    introduced: str | None = None
    fixed: str | None = None
    last_affected: str | None = None
    limit: str | None = None


class OsvRange(SeretoBaseModel, extra="allow"):
    """OSV affected range information."""

    type: Literal["SEMVER", "GIT", "ECOSYSTEM"]
    events: list[OsvEvent] = Field(default_factory=list)
    repo: str | None = None


class OsvPackage(SeretoBaseModel):
    """OSV package information."""

    name: str | None = None
    ecosystem: str | None = None
    purl: str | None = None


class OsvAffected(SeretoBaseModel, extra="allow"):
    """OSV affected entry."""

    package: OsvPackage | None = None
    ranges: list[OsvRange] = Field(default_factory=list)
    versions: list[str] = Field(default_factory=list)


class OsvReference(SeretoBaseModel):
    """OSV references link."""

    url: HttpUrl
    type: str | None = None


class OsvSeverity(SeretoBaseModel):
    """OSV severity record."""

    type: str | None = None
    score: float | str | None = None


class OsvVulnerability(SeretoBaseModel, extra="allow"):
    """OSV vulnerability entry."""

    id: str
    summary: str | None = None
    details: str | None = None
    aliases: list[str] = Field(default_factory=list)
    upstream: list[str] = Field(default_factory=list)
    references: list[OsvReference] = Field(default_factory=list)
    affected: list[OsvAffected] = Field(default_factory=list)
    severity: list[OsvSeverity] = Field(default_factory=list)

    def to_unified(self) -> Vulnerability:
        """Map OSV entry to the unified Vulnerability model."""
        vulnerability_id = self.id
        refs = {r.url for r in self.references if r.type == "ADVISORY"}

        affected: list[Affected] = _extract_affected(self.affected)

        severity = _extract_severity(self.severity)

        return Vulnerability(
            id=vulnerability_id,
            source="OSV",
            vuln_status=None,
            aliases=self.aliases + self.upstream,
            summary=self.summary if self.summary else self.details,
            references=refs,
            affected=affected,
            severity=severity,
        )


class OsvResult(SeretoBaseModel):
    """Normalized OSV."""

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

    @classmethod
    def model_validate_osv(cls, data):
        """Create OsvResult from arbitrary OSV input."""
        if isinstance(data, dict) and "vulns" in data:
            return cls(
                vulnerabilities=[
                    OsvVulnerability.model_validate(v) for v in data["vulns"]
                ]
            )
        if isinstance(data, list):
            return cls(
                vulnerabilities=[OsvVulnerability.model_validate(v) for v in data]
            )
        if isinstance(data, dict):
            return cls(vulnerabilities=[OsvVulnerability.model_validate(data)])
        return cls(vulnerabilities=[])

    def to_unified_list(self) -> list[Vulnerability]:
        """Convert all OSV vulnerabilities into unified Vulnerability."""
        return [v.to_unified() for v in self.vulnerabilities]


# -------------
# Normalizers
# -------------
def _extract_affected(osv_affected: list[OsvAffected]) -> list[Affected]:
    """Extract affected packages."""
    affected: list[Affected] = []

    for a in osv_affected:
        pkg = a.package
        if not pkg:
            continue
        pkg_info = PackageInfo(
            name=pkg.name,
            ecosystem=pkg.ecosystem,
            purl=pkg.purl,
        )
        ranges: list[Range] = []
        for rng in a.ranges:
            events: list[Event] = []
            for event in rng.events:
                events.append(
                    Event(
                        introduced=event.introduced,
                        fixed=event.fixed,
                        last_affected=event.last_affected,
                        limit=event.limit,
                    )
                )
            ranges.append(Range(type=rng.type, events=events))
        affected.append(Affected(package=pkg_info, ranges=ranges, versions=a.versions))
        # TODO: Extract affected from GIT
    return affected


def _extract_severity(severities: list[OsvSeverity]) -> Severity:
    """Extract the best numeric CVSS score and map to normalized Severity."""
    best_risk = Risk.none

    if not severities:
        return Severity(severity=Risk.none.value, score=0)

    for s in severities:
        score = getattr(s, "score", None)
        if score is None:
            continue
        try:
            if score.isdigit():
                numeric = float(score)
            else:
                cvss = parse_cvss_from_text(score)
                numeric = float(cvss[0].scores()[0])
        except Exception:
            continue

        risk = Risk.from_score(numeric)
        if risk > best_risk:
            best_risk = risk

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