-- 025 — MODEL SNAPSHOTS (retention store). Phase 2, priority zero.
--
-- Why this exists: as of 2026-07-20 the ONLY store of model history was
-- ledger_entries (640 rows, 6 game days) and it holds no model INPUTS. Every
-- other warehouse table in the schema is empty. That means we could score the
-- grades we emitted but could NOT replay a different model against the same
-- conditions — which is the only question a backtest exists to answer.
--
-- This table captures inputs AND outputs at lock time, immutably, so history
-- starts compounding tonight.
--
-- Append-only: ONE ROW PER GRADED PROP PER SNAPSHOT CYCLE. A re-grade at the
-- next cycle writes a NEW row on purpose — that is what lets us ask whether the
-- 14:00 read or the 22:00 read was sharper.

create table if not exists public.model_snapshots (
  id               bigserial primary key,

  -- provenance — never mix model eras (ledger_entries already has this
  -- contamination with no marker; everything from here is stamped)
  snapshot_id      uuid        not null,
  captured_at      timestamptz not null,
  cycle_hour_utc   smallint,
  model_version    text        not null,
  code_sha         text,

  -- identity (mirrors the ledger natural key so outcomes can be stamped)
  sport            text        not null,
  game_id          text        not null,
  game_date        date        not null,
  player_key       text        not null,
  player_name      text        not null,
  team             text,
  opponent         text,
  stat             text        not null,
  line             numeric     not null,
  side             text        not null,

  -- MARKET — real book numbers captured at a timestamp, never model output
  book             text,
  book_odds        integer,
  over_odds        integer,
  under_odds       integer,
  fair_odds        integer,
  fair_prob        numeric,
  overround        numeric,
  devig_method     text,

  -- MODEL OUTPUT
  grade            text,
  grade_11         text,
  confidence       numeric,
  confidence_basis text,
  p_win            numeric,
  ev_pct           numeric,
  projection       numeric,
  edge_pct         numeric,
  takeable         boolean,
  value            boolean,
  archetype        text,

  -- REFUSALS ARE TRAINING DATA. The ledger drops them entirely, so "was the
  -- gate right to refuse this?" is currently unanswerable. A too-aggressive
  -- gate costs real edge and is invisible without these rows.
  refused          boolean     not null default false,
  refusal_reason   text,

  -- THE COUNTERFACTUAL ENABLER. Schema-free so a new feature never needs a
  -- migration. Nulled after 90 days by the retention sweep; scalars stay forever.
  features         jsonb,

  -- OUTCOME — stamped later by the settle pass, per cycle row
  outcome          text,
  actual_value     numeric,
  settled_at       timestamptz,

  created_at       timestamptz not null default now()
);

-- One row per prop per cycle; a retry of the same cycle must not duplicate.
create unique index if not exists model_snapshots_cycle_prop_uniq
  on public.model_snapshots (snapshot_id, player_key, stat, line, side);

create index if not exists model_snapshots_date_sport_idx
  on public.model_snapshots (game_date, sport);
create index if not exists model_snapshots_player_idx
  on public.model_snapshots (player_key, stat, game_date);
create index if not exists model_snapshots_version_idx
  on public.model_snapshots (model_version, game_date);
-- Settle pass looks up unsettled rows by natural key.
create index if not exists model_snapshots_settle_idx
  on public.model_snapshots (game_date, player_key, stat, line, side)
  where outcome is null;

comment on table public.model_snapshots is
  'Append-only model history: inputs (features) + outputs per graded prop per snapshot cycle. Feeds the backtest harness, calibration, and metric validation. Features nulled after 90 days; scalars kept forever.';
comment on column public.model_snapshots.features is
  'Full feature vector at lock time. THE counterfactual enabler — without it a backtest can only grade our own homework. Nulled after 90 days by the retention sweep.';
comment on column public.model_snapshots.refused is
  'True when the engine refused to grade. Refusals are training data: the only way to detect a gate that is refusing props that would have won.';
comment on column public.model_snapshots.model_version is
  'Which engine produced this row. A backtest that mixes model eras is worthless. ledger_entries lacks this and is permanently contaminated across the 2026-07-19 fix boundary.';

-- RLS: service-role writes only, same posture as ledger_entries. This is
-- internal model telemetry — no client reads it.
alter table public.model_snapshots enable row level security;
