"""Data models for the CSAF VEX vulnerabilities section."""

from datetime import datetime
from typing import Any

import attrs
from cvss import CVSS2, CVSS3, CVSSError

from csaf_vex.models.common import Note, Reference, SerializableModel


@attrs.define
class CWE(SerializableModel):
    """Common Weakness Enumeration information."""

    # Required fields per CSAF spec (nullable to allow parsing invalid documents)
    id: str | None = attrs.field(default=None)
    name: str | None = attrs.field(default=None)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "CWE":
        """Create a CWE from a dictionary."""
        return cls(
            id=data.get("id"),
            name=data.get("name"),
        )


@attrs.define
class Flag(SerializableModel):
    """Machine-readable flag for product-specific vulnerability information."""

    # Required fields per CSAF spec (nullable to allow parsing invalid documents)
    label: str | None = attrs.field(default=None)

    # Optional fields (CSAF spec order)
    product_ids: list[str] = attrs.field(factory=list)

    # date: datetime | None = attrs.field(default=None)
    # group_ids: list[str] = attrs.field(factory=list)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Flag":
        """Create a Flag from a dictionary."""
        # date_str = data.get("date")
        return cls(
            label=data.get("label"),
            product_ids=data.get("product_ids", []),
            # date=datetime.fromisoformat(date_str) if date_str else None,
            # group_ids=data.get("group_ids", []),
        )


@attrs.define
class ID(SerializableModel):
    """Unique label or tracking ID for the vulnerability."""

    # Required fields per CSAF spec (nullable to allow parsing invalid documents)
    system_name: str | None = attrs.field(default=None)
    text: str | None = attrs.field(default=None)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "ID":
        """Create an ID from a dictionary."""
        return cls(
            system_name=data.get("system_name"),
            text=data.get("text"),
        )


@attrs.define
class Involvement(SerializableModel):
    """Level of involvement in vulnerability identification and remediation."""

    # Required fields per CSAF spec (nullable to allow parsing invalid documents)
    party: str | None = attrs.field(default=None)
    status: str | None = attrs.field(default=None)

    # Optional fields (CSAF spec order)
    date: datetime | None = attrs.field(default=None)
    summary: str | None = attrs.field(default=None)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Involvement":
        """Create an Involvement from a dictionary."""
        date_str = data.get("date")
        return cls(
            party=data.get("party"),
            status=data.get("status"),
            date=datetime.fromisoformat(date_str) if date_str is not None else None,
            summary=data.get("summary"),
        )


@attrs.define
class ProductStatus(SerializableModel):
    """Product status lists for the vulnerability."""

    # Fields we're currently using (all optional per spec)
    fixed: list[str] = attrs.field(factory=list)
    known_affected: list[str] = attrs.field(factory=list)
    known_not_affected: list[str] = attrs.field(factory=list)
    under_investigation: list[str] = attrs.field(factory=list)

    # first_affected: list[str] = attrs.field(factory=list)
    # first_fixed: list[str] = attrs.field(factory=list)
    # last_affected: list[str] = attrs.field(factory=list)
    # recommended: list[str] = attrs.field(factory=list)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "ProductStatus":
        """Create a ProductStatus from a dictionary."""
        return cls(
            fixed=data.get("fixed", []),
            known_affected=data.get("known_affected", []),
            known_not_affected=data.get("known_not_affected", []),
            under_investigation=data.get("under_investigation", []),
            # first_affected=data.get("first_affected", []),
            # first_fixed=data.get("first_fixed", []),
            # last_affected=data.get("last_affected", []),
            # recommended=data.get("recommended", []),
        )


@attrs.define
class RestartRequired(SerializableModel):
    """Information on restart required for remediation."""

    # Required fields per CSAF spec (nullable to allow parsing invalid documents)
    category: str | None = attrs.field(default=None)

    # Optional fields (CSAF spec order)
    details: str | None = attrs.field(default=None)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "RestartRequired":
        """Create a RestartRequired from a dictionary."""
        return cls(
            category=data.get("category"),
            details=data.get("details"),
        )


@attrs.define
class Remediation(SerializableModel):
    """Details on how to handle/fix a vulnerability."""

    # Required fields per CSAF spec (nullable to allow parsing invalid documents)
    category: str | None = attrs.field(default=None)
    details: str | None = attrs.field(default=None)

    # Optional fields (CSAF spec order)
    date: datetime | None = attrs.field(default=None)
    product_ids: list[str] = attrs.field(factory=list)
    url: str | None = attrs.field(default=None)

    # entitlements: list[str] = attrs.field(factory=list)
    # group_ids: list[str] = attrs.field(factory=list)
    # restart_required: RestartRequired | None = attrs.field(default=None)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Remediation":
        """Create a Remediation from a dictionary."""
        date_str = data.get("date")

        # restart_data = data.get("restart_required")

        return cls(
            category=data.get("category"),
            details=data.get("details"),
            date=datetime.fromisoformat(date_str) if date_str is not None else None,
            product_ids=data.get("product_ids", []),
            url=data.get("url"),
            # entitlements=data.get("entitlements", []),
            # group_ids=data.get("group_ids", []),
            # restart_required=(
            #     RestartRequired.from_dict(restart_data) if restart_data is not None else None
            # ),
        )


@attrs.define
class Score(SerializableModel):
    """Vulnerability score information (CVSS)."""

    # Required fields per CSAF spec (nullable to allow parsing invalid documents)
    products: list[str] = attrs.field(factory=list)

    # Optional fields - CVSS scores
    # These can be either parsed CVSS objects or raw dicts (if parsing failed)
    cvss_v2: CVSS2 | dict[str, Any] | None = attrs.field(default=None)
    cvss_v3: CVSS3 | dict[str, Any] | None = attrs.field(default=None)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Score":
        """Create a Score from a dictionary.

        Attempts to parse CVSS scores using the cvss library. If parsing fails,
        preserves the raw dict data so validation can catch it later.
        """
        # Map field names to their parser classes
        cvss_parsers = {
            "cvss_v2": CVSS2,
            "cvss_v3": CVSS3,
        }

        cvss_scores = {}

        for field_name, parser_class in cvss_parsers.items():
            cvss_data = data.get(field_name)

            if cvss_data and isinstance(cvss_data, dict):
                vector = cvss_data.get("vectorString")
                if vector:
                    try:
                        # Try to parse the CVSS vector string
                        cvss_scores[field_name] = parser_class(vector)
                    except CVSSError:
                        # Parsing failed - keep raw data for validation to catch
                        cvss_scores[field_name] = cvss_data
                else:
                    # No vector string - keep raw data
                    cvss_scores[field_name] = cvss_data
            else:
                cvss_scores[field_name] = None

        return cls(
            products=data.get("products", []),
            **cvss_scores,
        )


@attrs.define
class Threat(SerializableModel):
    """Vulnerability threat information that can change over time."""

    # Required fields per CSAF spec (nullable to allow parsing invalid documents)
    category: str | None = attrs.field(default=None)
    details: str | None = attrs.field(default=None)

    # Optional fields (CSAF spec order)
    product_ids: list[str] = attrs.field(factory=list)

    # date: datetime | None = attrs.field(default=None)
    # group_ids: list[str] = attrs.field(factory=list)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Threat":
        """Create a Threat from a dictionary."""

        # date_str = data.get("date")
        return cls(
            category=data.get("category"),
            details=data.get("details"),
            product_ids=data.get("product_ids", []),
            # date=datetime.fromisoformat(date_str) if date_str else None,
            # group_ids=data.get("group_ids", []),
        )


@attrs.define
class Vulnerability(SerializableModel):
    """Container for all fields related to a single vulnerability."""

    # All fields are optional (at least one must be present per spec)
    cve: str | None = attrs.field(default=None)
    cwe: CWE | None = attrs.field(default=None)
    discovery_date: datetime | None = attrs.field(default=None)
    flags: list[Flag] = attrs.field(factory=list)
    notes: list[Note] = attrs.field(factory=list)
    product_status: ProductStatus | None = attrs.field(default=None)
    references: list[Reference] = attrs.field(factory=list)
    release_date: datetime | None = attrs.field(default=None)
    remediations: list[Remediation] = attrs.field(factory=list)
    ids: list[ID] = attrs.field(factory=list)
    scores: list[Score] = attrs.field(factory=list)
    threats: list[Threat] = attrs.field(factory=list)
    title: str | None = attrs.field(default=None)

    # acknowledgments: list[Acknowledgment] = attrs.field(factory=list)
    # involvements: list[Involvement] = attrs.field(factory=list)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "Vulnerability":
        """Create a Vulnerability from a dictionary.

        Args:
            data: A vulnerability object from parsed JSON
        """
        cwe_data = data.get("cwe")
        discovery_date_str = data.get("discovery_date")
        flags_data = data.get("flags", [])
        ids_data = data.get("ids", [])
        notes_data = data.get("notes", [])
        product_status_data = data.get("product_status")
        references_data = data.get("references", [])
        release_date_str = data.get("release_date")
        remediations_data = data.get("remediations", [])
        scores_data = data.get("scores", [])
        threats_data = data.get("threats", [])

        # acknowledgments_data = data.get("acknowledgments", [])
        # involvements_data = data.get("involvements", [])

        return cls(
            cve=data.get("cve"),
            cwe=CWE.from_dict(cwe_data) if cwe_data is not None else None,
            discovery_date=datetime.fromisoformat(discovery_date_str)
            if discovery_date_str is not None
            else None,
            flags=[Flag.from_dict(f) for f in flags_data],
            ids=[ID.from_dict(i) for i in ids_data],
            notes=[Note.from_dict(n) for n in notes_data],
            product_status=ProductStatus.from_dict(product_status_data)
            if product_status_data is not None
            else None,
            references=[Reference.from_dict(r) for r in references_data],
            release_date=datetime.fromisoformat(release_date_str)
            if release_date_str is not None
            else None,
            remediations=[Remediation.from_dict(r) for r in remediations_data],
            scores=[Score.from_dict(s) for s in scores_data],
            threats=[Threat.from_dict(t) for t in threats_data],
            title=data.get("title"),
            # acknowledgments=[Acknowledgment.from_dict(a) for a in acknowledgments_data],
            # involvements=[Involvement.from_dict(i) for i in involvements_data],
        )
