"""Threat-intel enrichment row — one per attacker IP, TTL-cached."""
from datetime import datetime, timezone
from typing import Any, Optional

from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel


class AttackerIntel(SQLModel, table=True):
    """Aggregated threat-intel verdict for a single attacker IP.

    Populated by the ``decnet enrich`` worker, which queries multiple
    free-tier intel providers (GreyNoise Community, AbuseIPDB,
    abuse.ch Feodo Tracker + ThreatFox) and writes one row per
    attacker IP. The row is TTL-cached via ``expires_at`` so re-firings
    inside the cache window short-circuit before any HTTP egress.

    Per-provider columns are nullable until each provider has answered;
    the enrichment pass writes whichever providers succeeded and leaves
    the rest unchanged on a partial failure.

    ``schema_version`` is committed to storage from day one — federation
    gossip in v2/v3 requires cross-operator compatibility, and
    retrofitting a version column after rows exist is painful. Mirrors
    the rationale on :class:`AttackerIdentity`'s ``schema_version``.
    """

    __tablename__ = "attacker_intel"

    uuid: str = Field(primary_key=True)  # uuid.uuid4().hex, generated by writer
    # Canonical key. One intel row per attacker UUID; FK guarantees no orphan
    # rows when an attacker is deleted, and UNIQUE keeps upserts honest.
    attacker_uuid: str = Field(
        foreign_key="attackers.uuid",
        unique=True,
        index=True,
    )
    # DENORMALISED — NOT a key. The IP the worker queried providers with at
    # write time. Useful for SIEM payloads and audit lookups; updated on every
    # upsert if the attacker rotates IPs. Never use this column as a lookup
    # key; ``attacker_uuid`` is the only canonical identifier here.
    attacker_ip: str = Field(index=True)
    schema_version: int = Field(default=1)

    # ── GreyNoise Community ─────────────────────────────────────────────
    # classification ∈ {"benign", "malicious", "suspicious", "unknown"}.
    # The Community endpoint does not return tags; ``greynoise_tags`` stays
    # empty unless an operator wires a non-Community provider that does.
    greynoise_classification: Optional[str] = Field(default=None, max_length=32)
    greynoise_name: Optional[str] = Field(default=None, max_length=128)
    greynoise_tags: list[str] = Field(
        default_factory=list,
        sa_column=Column("greynoise_tags", JSON, nullable=False),
    )
    greynoise_raw: dict[str, Any] = Field(
        default_factory=dict,
        sa_column=Column("greynoise_raw", JSON, nullable=False),
    )
    greynoise_queried_at: Optional[datetime] = Field(default=None)

    # ── AbuseIPDB ────────────────────────────────────────────────────────
    # 0..100 abuse confidence score
    abuseipdb_score: Optional[int] = Field(default=None)
    abuseipdb_categories: list[int] = Field(
        default_factory=list,
        sa_column=Column("abuseipdb_categories", JSON, nullable=False),
    )
    abuseipdb_raw: dict[str, Any] = Field(
        default_factory=dict,
        sa_column=Column("abuseipdb_raw", JSON, nullable=False),
    )
    abuseipdb_queried_at: Optional[datetime] = Field(default=None)

    # ── abuse.ch Feodo Tracker ───────────────────────────────────────────
    feodo_listed: Optional[bool] = Field(default=None)
    feodo_malware_family: Optional[str] = Field(default=None, max_length=64)
    feodo_raw: dict[str, Any] = Field(
        default_factory=dict,
        sa_column=Column("feodo_raw", JSON, nullable=False),
    )
    feodo_queried_at: Optional[datetime] = Field(default=None)

    # ── abuse.ch ThreatFox ───────────────────────────────────────────────
    # ThreatFox returns a list of matches for a queried IP. Each match has
    # a ``threat_type`` (botnet_cc / payload_delivery / payload /
    # cc_skimming) and an ``ioc_type`` (url / domain / ip:port / hash
    # variants). We flatten the unique sets across all matches; the
    # IntelLifter keys ATT&CK techniques on ``threat_type``, the canonical
    # taxonomy field per ThreatFox's API.
    threatfox_listed: Optional[bool] = Field(default=None)
    threatfox_threat_types: list[str] = Field(
        default_factory=list,
        sa_column=Column("threatfox_threat_types", JSON, nullable=False),
    )
    threatfox_ioc_types: list[str] = Field(
        default_factory=list,
        sa_column=Column("threatfox_ioc_types", JSON, nullable=False),
    )
    threatfox_malware_families: list[str] = Field(
        default_factory=list,
        sa_column=Column("threatfox_malware_families", JSON, nullable=False),
    )
    threatfox_raw: dict[str, Any] = Field(
        default_factory=dict,
        sa_column=Column("threatfox_raw", JSON, nullable=False),
    )
    threatfox_queried_at: Optional[datetime] = Field(default=None)

    # ── Aggregate verdict ────────────────────────────────────────────────
    # Synthesised from per-provider columns. ∈ {"malicious", "suspicious",
    # "benign", "unknown"}. Used by the dashboard and webhook consumers
    # that don't want to reason over four provider columns.
    aggregate_verdict: Optional[str] = Field(
        default=None, max_length=32, index=True
    )

    # ── TTL bookkeeping ──────────────────────────────────────────────────
    cached_at: datetime = Field(
        default_factory=lambda: datetime.now(timezone.utc), index=True
    )
    expires_at: datetime = Field(index=True)

    def to_intel_event_payload(
        self,
        *,
        providers: Optional[list[str]] = None,
    ) -> dict[str, Any]:
        """Project this row into the payload shape the IntelLifter consumes.

        Called by both the intel worker (on live publish of
        ``attacker.intel.enriched``) and the TTP worker (on
        ``attacker.session.ended`` catch-up). The two callers produce
        identical payloads for the same row, so IntelLifter tag UUIDs
        are deterministic regardless of which path delivered them.

        ``providers`` is included when the intel worker knows which
        providers contributed; the TTP catch-up path omits it (the
        IntelLifter does not predicate on ``providers``).
        """
        d: dict[str, Any] = {
            "attacker_uuid": self.attacker_uuid,
            "attacker_ip": self.attacker_ip,
            "aggregate_verdict": self.aggregate_verdict,
            # AbuseIPDB
            "abuseipdb_score": self.abuseipdb_score,
            "abuseipdb_categories": self.abuseipdb_categories,
            # GreyNoise
            "greynoise_classification": self.greynoise_classification,
            "greynoise_name": self.greynoise_name,
            "greynoise_tags": self.greynoise_tags,
            # Feodo
            "feodo_listed": self.feodo_listed,
            "feodo_malware_family": self.feodo_malware_family,
            # ThreatFox
            "threatfox_listed": self.threatfox_listed,
            "threatfox_threat_types": self.threatfox_threat_types,
            "threatfox_ioc_types": self.threatfox_ioc_types,
            "threatfox_malware_families": self.threatfox_malware_families,
        }
        if providers is not None:
            d["providers"] = providers
        return d
