"""Comment + attachment wire shapes (Pydantic v2) — plan §5, M4.

ball_id replaces the DB's parent_id at this boundary: parent_type/parent_id
is the model's polymorphic-parent decision (only 'ball' exists today), but
every route here is already ball-scoped (/api/balls/{ball_id}/comments), so
the wire shape doesn't need to carry the polymorphism the DB keeps for
later parent kinds.
"""
from __future__ import annotations

import datetime
import uuid
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field


class AttachmentOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: uuid.UUID
    file_path: str
    mime: str
    bytes: int
    created_at: datetime.datetime


class AttachmentIn(BaseModel):
    """The staged-upload identity POST /api/attachments returned — the
    composer echoes these back to link them to the comment it's posting."""

    file_path: str
    mime: str
    bytes: int


class CommentOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    # M6: the GET list interleaves real comments with collapsed markers;
    # this literal is the discriminator the client switches on.
    kind: Literal["comment"] = "comment"
    id: uuid.UUID
    ball_id: uuid.UUID
    author_id: uuid.UUID | None = None
    # M6: set (with author_id nulled) when the author reps to a side label
    # for the viewer — a visible-node comment by an author beyond the
    # viewer's world (the Tracy-to-Rob case, services.perspective).
    author_label: str | None = None
    acting_node_id: uuid.UUID | None = None
    body: str
    created_at: datetime.datetime
    edited_at: datetime.datetime | None = None
    attachments: list[AttachmentOut] = Field(default_factory=list)
    # Populated on the POST (create) response only. Notifications — where
    # resolving these against real players matters — are M5; parsing is a
    # pure regex over the body (services.conversation.parse_mentions), not
    # persisted, so GET/PATCH responses leave this at its default.
    mentions: list[str] = Field(default_factory=list)


class CollapsedCommentsOut(BaseModel):
    """§4.3: a stretch of comments whose node the viewer cannot see —
    rendered as one line ("N comments on the Bowden side"): a count, no
    bodies, no authors. Sub-court conversation stays in the sub-court."""

    kind: Literal["collapsed_comments"] = "collapsed_comments"
    count: int


# The GET /api/balls/{id}/comments item type: a real comment or a
# collapsed stretch, discriminated by `kind`.
CommentEntryOut = CommentOut | CollapsedCommentsOut


class CommentCreate(BaseModel):
    """POST /api/balls/{ball_id}/comments. author/acting_node_id are
    stamped server-side — never accepted from the client (ref §2.7)."""

    body: str
    created_at: datetime.datetime | None = None
    attachments: list[AttachmentIn] = Field(default_factory=list)


class CommentPatch(BaseModel):
    """PATCH /api/comments/{id} — author-gated (services.conversation
    .update_comment). Either field alone is valid; an unset field is left
    untouched (the router reads this via model_dump(exclude_unset=True),
    same convention as BallPatch)."""

    body: str | None = None
    created_at: datetime.datetime | None = None
