from typing import Literal

from pydantic import (
    BaseModel,
    ConfigDict,
    Field,
    HttpUrl,
    field_serializer,
    field_validator,
)

from vulnsearch.risk import Risk


class SeretoBaseModel(BaseModel):
    """Pydantic BaseModel with custom strict configuration."""

    model_config = ConfigDict(extra="forbid", strict=True, validate_assignment=True)


class Event(SeretoBaseModel):
    """Model for event information."""

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


class Range(SeretoBaseModel):
    """Model for range information."""

    type: Literal["SEMVER", "GIT", "ECOSYSTEM", "CPE"]
    events: list[Event] = Field(default_factory=list)


class PackageInfo(SeretoBaseModel):
    """Model for package information."""

    name: str | None = None

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

    vendor: str | None = None
    part: Literal["a", "o", "h"] | None = None
    cpe: str | None = None


class Affected(SeretoBaseModel):
    """Model for affected packages."""

    package: PackageInfo
    ranges: list[Range] = Field(default_factory=list)
    versions: list[str] = Field(default_factory=list)


class Severity(SeretoBaseModel):
    """Model for severity information."""

    severity: Risk | None = None
    score: int | None = Field(default=None, ge=0, le=4)

    @field_validator("severity", mode="before")
    @classmethod
    def parse_severity(cls, v):
        if v is None:
            return v
        if isinstance(v, Risk):
            return v
        if isinstance(v, str):
            return Risk(v)
        raise TypeError(f"invalid severity value: {v}")


class Vulnerability(SeretoBaseModel):
    """Unified vulnerability model (OSV/NVD)."""

    id: str
    vuln_status: str | None = None
    aliases: list[str] = Field(default_factory=list)
    source: Literal["OSV", "NVD"]
    summary: str | None = None
    references: set[HttpUrl] = Field(default_factory=set)
    affected: list[Affected] = Field(default_factory=list)
    severity: Severity | None = None

    @field_serializer("references")
    def serialize_links(self, references: set[HttpUrl], _info):
        """Serialize links to a list of strings."""
        return [str(link) for link in references]


class ReportVulnerability(SeretoBaseModel):
    id: str
    summary: str = ""
    links: list[str] = Field(default_factory=list)
    events: list[Event] = Field(default_factory=list)

    @field_serializer("events")
    def serialize_events(self, events: list[Event], _info):
        """Serialize events to a list of non-empty events."""
        result_events = []
        for event in events:
            d = event.model_dump(exclude_none=True)
            if d:
                result_events.append(d)

        return result_events


class Package(SeretoBaseModel):
    """Package with associated vulnerabilities."""

    name: str
    version: str | None = None
    ecosystem: str | None = None  # TODO: add an ecosystem
    vulnerabilities: list[Vulnerability] = Field(default_factory=list)

    @field_validator("vulnerabilities")
    @classmethod
    def sort_vulnerabilities(
        cls, vulnerabilities: list[Vulnerability]
    ) -> list[Vulnerability]:
        """Sort by severity score descending."""
        vulnerabilities.sort(
            key=lambda v: v.severity.score,
            reverse=True,
        )
        return vulnerabilities

    def report_vulnerabilities(self) -> list[ReportVulnerability]:
        """Build report vulnerability list for this package."""
        result: list[ReportVulnerability] = []

        for vuln in self.vulnerabilities:
            events: list[Event] = []

            for aff in vuln.affected:
                if aff.package.name != self.name:
                    continue
                for rng in aff.ranges:
                    events.extend(rng.events)

            result.append(
                ReportVulnerability(
                    id=vuln.id,
                    summary=vuln.summary or "",
                    links=[str(url) for url in vuln.references],
                    events=events,
                )
            )

        return result
