"""Campaign — operation-level grouping of resolved attacker identities."""
from datetime import datetime, timezone
from typing import Any, List, Optional

from pydantic import BaseModel
from sqlalchemy import Column, Text
from sqlmodel import Field, SQLModel


class Campaign(SQLModel, table=True):
    """
    Campaign — one operation, one or more identities.

    Sits one level above ``AttackerIdentity``: an actor (identity) may
    appear in multiple campaigns over time, and a campaign may have
    several distinct identities cooperating (e.g. a night-shift and
    day-shift operator on the same job — fixture F5 multi_operator).

    Populated by the campaign clusterer worker (downstream of identity
    resolution). Empty rows are valid; the table ships empty until the
    clusterer lands. ``schema_version`` is non-negotiable from day one
    for the same federation-gossip reason ``AttackerIdentity`` carries
    one — bumping campaign-level feature definitions without a version
    field silently poisons cross-operator gossip in V2.

    See ``development/CAMPAIGN_CLUSTERING.md`` for the signal taxonomy
    (phase-handoff, shared-infra, temporal overlap, cohort).
    """
    __tablename__ = "campaigns"
    uuid: str = Field(primary_key=True)
    schema_version: int = Field(default=1)
    first_seen_at: Optional[datetime] = Field(default=None, index=True)
    last_seen_at: Optional[datetime] = Field(default=None, index=True)
    created_at: datetime = Field(
        default_factory=lambda: datetime.now(timezone.utc), index=True
    )
    updated_at: datetime = Field(
        default_factory=lambda: datetime.now(timezone.utc), index=True
    )
    # Campaign-cohesion score from the clusterer. Range [0, 1]; null
    # until the clusterer writes. Higher = more confident the linked
    # identities are part of the same operation.
    confidence: Optional[float] = Field(default=None)
    # Denormalized count of FK'd ``AttackerIdentity`` rows.
    identity_count: int = Field(default=0)
    # Aggregated fingerprint summary across member identities. Same
    # JSON-serialized list[str] in TEXT shape as
    # ``AttackerIdentity.{ja3,hassh,payload_simhashes,c2_endpoints}`` —
    # federation gossip wants the same wire shape at every layer.
    ja3_hashes: Optional[str] = Field(
        default=None, sa_column=Column("ja3_hashes", Text, nullable=True)
    )
    hassh_hashes: Optional[str] = Field(
        default=None, sa_column=Column("hassh_hashes", Text, nullable=True)
    )
    tls_cert_sha256: Optional[str] = Field(
        default=None, sa_column=Column("tls_cert_sha256", Text, nullable=True)
    )
    payload_simhashes: Optional[str] = Field(
        default=None, sa_column=Column("payload_simhashes", Text, nullable=True)
    )
    c2_endpoints: Optional[str] = Field(
        default=None, sa_column=Column("c2_endpoints", Text, nullable=True)
    )
    # Soft-merge audit trail — same revocable-merge pattern as
    # ``AttackerIdentity.merged_into_uuid``. When the clusterer
    # collapses two campaigns, the loser's row stays in place with this
    # set to the winner's UUID; resolvers follow the chain.
    merged_into_uuid: Optional[str] = Field(
        default=None, foreign_key="campaigns.uuid", index=True
    )
    # Operator-editable free-form notes — annotation surface for
    # human analysts ("APT-XX Q2 campaign", "matches CTI report 5678").
    notes: Optional[str] = Field(
        default=None, sa_column=Column("notes", Text, nullable=True)
    )


class CampaignsResponse(BaseModel):
    total: int
    limit: int
    offset: int
    data: List[dict[str, Any]]
