from datetime import datetime, timezone

from sqlalchemy import Boolean, DateTime, Float, Index, Integer, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column

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


class RetrievalLog(Base):
    """One row per semantic-retrieval call, for KB-injection tuning.

    Captures what a query asked for, what came back, and the score
    distribution of the results — the empirical basis for tuning the
    similarity threshold and top-k per surface. `result_ids` holds the ranked
    hits (id + score + rank) so a later pass can correlate "what we surfaced"
    against "what the agent then fetched/referenced".

    Deliberately FK-free on user_id (mirrors AppLog): telemetry should outlive
    the row it describes, and a deleted user shouldn't cascade away history.
    """

    __tablename__ = "retrieval_logs"

    id: Mapped[int] = mapped_column(primary_key=True)
    # Declared here rather than via CreatedAtMixin on purpose: the composite
    # index below orders on `created_at.desc()`, which needs the column object
    # in this class body — a mixin's column is not in scope there.
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
    )
    user_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
    # Retrieval surface: 'mcp_search' | 'rest_search' | 'auto_inject' | ...
    source: Mapped[str] = mapped_column(Text, nullable=False)
    query: Mapped[str | None] = mapped_column(Text, nullable=True)
    # Effective parameters actually used for this call.
    threshold: Mapped[float | None] = mapped_column(Float, nullable=True)
    limit_n: Mapped[int | None] = mapped_column(Integer, nullable=True)
    project_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
    # The content-type filter as passed to semantic_search_notes: True=tasks,
    # False=notes, NULL=any.
    is_task: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
    result_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
    top_score: Mapped[float | None] = mapped_column(Float, nullable=True)
    min_score: Mapped[float | None] = mapped_column(Float, nullable=True)
    # [{"id": int, "score": float, "rank": int}, ...], highest-first.
    result_ids: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
    duration_ms: Mapped[float | None] = mapped_column(Float, nullable=True)

    __table_args__ = (
        Index("ix_retrieval_logs_created_at", "created_at"),
        Index("ix_retrieval_logs_user_id", "user_id"),
        Index("ix_retrieval_logs_source", "source"),
        Index("ix_retrieval_logs_source_created_at", "source", created_at.desc()),
    )

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "created_at": iso(self.created_at),
            "user_id": self.user_id,
            "source": self.source,
            "query": self.query,
            "threshold": self.threshold,
            "limit_n": self.limit_n,
            "project_id": self.project_id,
            "is_task": self.is_task,
            "result_count": self.result_count,
            "top_score": self.top_score,
            "min_score": self.min_score,
            "result_ids": self.result_ids,
            "duration_ms": self.duration_ms,
        }
