from sqlalchemy import BigInteger, Index, 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 RuleUsageEvent(Base, CreatedAtMixin):
    """One row per time a rule was SURFACED to the agent, or PULLED in full.

    The sibling `note_usage_events` has had since 2026-07, third in the line
    after `rule_embeddings` and `rule_versions` — and, like those, it exists
    because the rule side kept inheriting machinery built for notes and
    quietly getting the weaker version of it.

    WHY RULES NEED THEIR OWN AND CANNOT SHARE THE NOTE TABLE. Not squeamishness
    about a polymorphic column — the row shares no note-specific fields and the
    aggregate readout is the same shape, which is the strongest case for
    sharing that note #3163 admits. What decides it is IDENTITY AT RESTORE. A
    note id and a rule id are different namespaces resolved through different
    maps, and `note_usage_events`'s importer maps `note_id` through
    `note_id_map` and drops what does not resolve. A rule id parked in that
    column would come back from a backup silently reattached to whatever note
    happened to take that number — telemetry that is not merely lost but wrong,
    and wrong in a way nothing downstream could detect.

    WHAT THIS MEASURES, AND WHY IT DID NOT EXIST. The write-path standing-rule
    arm is the only retrieval surface in Scribe whose usefulness cannot be
    observed — and, not coincidentally, the only one that has never declined to
    fire (#3311: 296 calls, zero zero-result, 100% clearing its threshold).
    `retrieval_logs` gives it scores; scores say what the ranker thought, never
    whether the hint landed. Without a pull counter no install can tune the arm
    from evidence, only from the shape of a histogram.

    Deliberately FK-FREE on `rule_id` and `user_id`, matching `note_usage_events`,
    `retrieval_logs` and `app_logs` — and diverging from `rule_versions`, which
    does carry FKs. The difference is what the row is FOR: a version is part of
    a rule's history and dies with it, while telemetry outlives the row it
    describes. Deleting a rule must not erase the evidence that it was surfaced
    forty times and opened never, because that evidence is precisely the case
    for having deleted it.

    Cells left deliberately empty (note #3163's step 3): no share ACL — rules
    have none of their own; no soft delete — nothing restores a telemetry row,
    and the table is append-only; no embedding — an event is not a document.
    """

    __tablename__ = "rule_usage_events"

    # BigInteger throughout, where the note twin uses Integer. `rule_id` has to
    # be, since `rules.id` is BigInteger — and once one column is, matching the
    # rest costs nothing and keeps the row uniform. A high-churn append-only
    # telemetry table is a poor place to discover an id ceiling.
    id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
    user_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
    rule_id: Mapped[int] = mapped_column(BigInteger, nullable=False)

    # 'surfaced' | 'pulled'
    event: Mapped[str] = mapped_column(Text, nullable=False)

    # Which surface produced it. A CONVENTION, not a fixed vocabulary, and the
    # note twin's comment explains why this one deliberately does not enumerate
    # its members: the previous such list went stale, naming a source nothing
    # wrote while omitting ones that existed, and a half-true enumeration reads
    # as authoritative in exactly the way that misleads (#2476).
    # `grep -rn record_rule_pulled\|record_rule_surfaced src/` is the
    # authoritative list, and unlike a comment it cannot drift.
    #
    # The mcp_/rest_ prefix split is load-bearing here for the same reason it is
    # for notes, and more so: "is this rule dead weight?" is served by any pull,
    # but "did that injected hint land?" — the question this arm exists to
    # answer — is served by AGENT pulls only. Never aggregate across the prefix
    # without saying why.
    source: Mapped[str] = mapped_column(Text, nullable=False)

    __table_args__ = (
        # Every readout is "these rule ids, split by event" — a covering
        # composite beats separate single-column indexes for it.
        Index("ix_rule_usage_rule_event", "rule_id", "event"),
        Index("ix_rule_usage_created_at", "created_at"),
        Index("ix_rule_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,
            "rule_id": self.rule_id,
            "event": self.event,
            "source": self.source,
        }
