from sqlalchemy import Index, Integer, Text
from sqlalchemy.orm import Mapped, mapped_column

from scribe.models import Base
from scribe.models.base import CreatedAtMixin, iso

SURFACED = "surfaced"
PULLED = "pulled"


class NoteUsageEvent(Base, CreatedAtMixin):
    """One row per time a note was SURFACED to the agent, or PULLED in full.

    Answers the question RetrievalLog cannot: not "what did the ranker return
    and with what scores" but "did anyone ever actually open this?" A snippet
    nobody opens is not neutral — it competes for the injection budget on every
    future turn and dilutes the menu — so the surfaced:pulled ratio is what
    makes dead weight visible and prunable.

    WHY A SEPARATE TABLE FROM RetrievalLog. RetrievalLog is one row per *call*,
    keyed on the score distribution it exists to capture; folding un-scored
    events into it would corrupt exactly the distribution threshold tuning reads
    (see the KNOWN GAP note this closes in plugin_context.build_write_path_hint).
    This table is one row per *note per event*, which is the grain the usage
    readout needs and the grain RetrievalLog's JSONB `result_ids` can't be
    indexed at. The two are complements: RetrievalLog tunes the threshold, this
    tunes the corpus.

    WHY NOT IN-SESSION CORRELATION. The original framing was "correlate
    result_ids against a later get_note in the same session". There is no
    session identity server-side — the MCP endpoint is stateless and hooks send
    no session id — and introducing one would mean threading an opaque,
    client-supplied token through every read path for a signal that does not
    need it. Two independent counters answer the question without it: a note
    surfaced 40 times and never pulled is dead weight regardless of which
    sessions those events fell in.

    Deliberately FK-free on user_id and note_id (mirrors RetrievalLog/AppLog):
    telemetry outlives the row it describes, and deleting a note should not
    erase the evidence that it was surfaced 40 times and never once opened.
    """

    __tablename__ = "note_usage_events"

    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
    note_id: Mapped[int] = mapped_column(Integer, nullable=False)
    # 'surfaced' | 'pulled'
    event: Mapped[str] = mapped_column(Text, nullable=False)
    # Which surface produced it. Kept granular so the place arm and the
    # semantic arm can be compared — that comparison is the whole reason the
    # place arm needed logging at all.
    #
    # A CONVENTION, not a fixed vocabulary: `mcp_<tool>` for an agent call,
    # `rest_<kind>` for a human opening a detail view, and a bare name for a
    # hook or background surface ('auto_inject', 'write_path_place',
    # 'write_path_semantic'). This comment deliberately no longer lists the
    # members — the previous list had gone stale, naming 'rest_note' that
    # nothing wrote while omitting sources that existed, and a half-true
    # enumeration reads as authoritative in exactly the way that misleads
    # (#2476). `grep -rn record_pulled\\\|record_surfaced src/` is the
    # authoritative list, and unlike a comment it cannot drift.
    #
    # The mcp_/rest_ split is load-bearing. "Is this dead weight?" is served by
    # any pull; "was that injected line useful?" is served by AGENT pulls only,
    # so never aggregate across the prefix without saying why (#1038, #2085).
    source: Mapped[str] = mapped_column(Text, nullable=False)

    __table_args__ = (
        # The readout is always "these note ids, split by event" — a covering
        # composite beats separate single-column indexes for it.
        Index("ix_note_usage_note_event", "note_id", "event"),
        Index("ix_note_usage_created_at", "created_at"),
        Index("ix_note_usage_user_id", "user_id"),
    )

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "created_at": iso(self.created_at),
            "user_id": self.user_id,
            "note_id": self.note_id,
            "event": self.event,
            "source": self.source,
        }
