-- ============================================================
-- Meridian — Supabase Database Schema (unified, idempotent)
-- Run this entire file in: Supabase Dashboard → SQL Editor → New Query
--
-- Safe to re-run: every CREATE uses IF NOT EXISTS, every ALTER uses
-- IF NOT EXISTS, the function is DROPped before CREATE so return-type
-- changes don't error.
-- ============================================================

-- ── Enable pgvector ──────────────────────────────────────────────
CREATE EXTENSION IF NOT EXISTS vector;

-- ── Profiles ──────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS public.profiles (
  id                 UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  display_name       TEXT,
  age                INT CHECK (age BETWEEN 13 AND 120),
  preferred_modality TEXT DEFAULT 'cbt'
                     CHECK (preferred_modality IN ('cbt','act','dbt','mindfulness','sfbt')),
  consent_given      BOOLEAN NOT NULL DEFAULT false,
  consent_at         TIMESTAMPTZ,
  created_at         TIMESTAMPTZ DEFAULT now(),
  updated_at         TIMESTAMPTZ DEFAULT now()
);

-- ── Sessions ──────────────────────────────────────────────────────
-- Replaces the in-memory session_store dict in main.py.
-- prob_history is Phase 10's per-turn SVM depression probability
-- trajectory — see backend/agents/diagnosis.py::_decide().
CREATE TABLE IF NOT EXISTS public.sessions (
  id                TEXT PRIMARY KEY,
  user_id           UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  mode              TEXT NOT NULL CHECK (mode IN ('diagnosis','therapy')),
  modality          TEXT CHECK (modality IN ('cbt','act','dbt','mindfulness','sfbt')),
  embedding_buffer  JSONB NOT NULL DEFAULT '[]',
  char_buffer_total INT   NOT NULL DEFAULT 0,
  prob_history      JSONB NOT NULL DEFAULT '[]',
  diagnosis_ready   BOOLEAN NOT NULL DEFAULT false,
  crisis_detected   BOOLEAN NOT NULL DEFAULT false,
  risk_scores       JSONB NOT NULL DEFAULT '{}',
  created_at        TIMESTAMPTZ DEFAULT now(),
  updated_at        TIMESTAMPTZ DEFAULT now()
);

-- Phase 10 backfill — ensures existing DBs without prob_history
-- get the column added. No-op on fresh DBs (column already exists
-- from the CREATE TABLE above).
ALTER TABLE public.sessions
  ADD COLUMN IF NOT EXISTS prob_history JSONB NOT NULL DEFAULT '[]';

CREATE INDEX IF NOT EXISTS sessions_user_id_idx ON public.sessions(user_id);

-- ── Messages ──────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS public.messages (
  id          BIGSERIAL PRIMARY KEY,
  session_id  TEXT NOT NULL REFERENCES public.sessions(id) ON DELETE CASCADE,
  role        TEXT NOT NULL CHECK (role IN ('user','assistant')),
  content     TEXT NOT NULL,
  created_at  TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX IF NOT EXISTS messages_session_id_idx ON public.messages(session_id);

-- ── Audit Log ─────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS public.audit_log (
  id          BIGSERIAL PRIMARY KEY,
  user_id     UUID REFERENCES auth.users(id) ON DELETE SET NULL,
  session_id  TEXT,
  event_type  TEXT NOT NULL,
  -- event_type values:
  --   'consent_accepted'   — user accepted ToS on onboarding page
  --   'crisis_detected'    — crisis node fired during a chat turn
  --   'diagnosis_result'   — MentalRoBERTa SVM classification completed
  --   'guardian_block'     — guardian agent blocked a request
  --   'rate_limit_hit'     — user exceeded 20 req/min
  --   'phq9_assessment'    — completed 9-question PHQ-9 cycle
  metadata    JSONB NOT NULL DEFAULT '{}',
  created_at  TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX IF NOT EXISTS audit_log_user_id_idx    ON public.audit_log(user_id);
CREATE INDEX IF NOT EXISTS audit_log_event_type_idx ON public.audit_log(event_type);
CREATE INDEX IF NOT EXISTS audit_log_created_at_idx ON public.audit_log(created_at DESC);

-- ── Memory Embeddings (pgvector — replaces in-memory ChromaDB) ────
-- Uses sentence-transformers/all-MiniLM-L6-v2 → 384 dimensions.
CREATE TABLE IF NOT EXISTS public.memory_embeddings (
  id          BIGSERIAL PRIMARY KEY,
  session_id  TEXT REFERENCES public.sessions(id) ON DELETE CASCADE,
  user_id     UUID REFERENCES auth.users(id)     ON DELETE CASCADE,
  content     TEXT NOT NULL,
  embedding   vector(384),
  created_at  TIMESTAMPTZ DEFAULT now()
);

-- IVFFlat index for approximate nearest-neighbour search.
-- Run AFTER inserting at least 100 rows for best performance.
CREATE INDEX IF NOT EXISTS memory_embeddings_vector_idx
  ON public.memory_embeddings
  USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 50);

-- ── PHQ-9 Trajectory ──────────────────────────────────────────────
-- phq9_responses    — every individual answered question
-- phq9_assessments  — sealed 9-question totals + severity (chart source)

CREATE TABLE IF NOT EXISTS public.phq9_responses (
  id              BIGSERIAL PRIMARY KEY,
  user_id         UUID NOT NULL REFERENCES auth.users(id)      ON DELETE CASCADE,
  session_id      TEXT NOT NULL REFERENCES public.sessions(id) ON DELETE CASCADE,
  question_index  INT  NOT NULL CHECK (question_index BETWEEN 0 AND 8),
  score           INT  NOT NULL CHECK (score          BETWEEN 0 AND 3),
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS phq9_responses_session_idx
  ON public.phq9_responses(session_id);
CREATE INDEX IF NOT EXISTS phq9_responses_user_idx
  ON public.phq9_responses(user_id, created_at DESC);


CREATE TABLE IF NOT EXISTS public.phq9_assessments (
  id           BIGSERIAL PRIMARY KEY,
  user_id      UUID NOT NULL REFERENCES auth.users(id)      ON DELETE CASCADE,
  session_id   TEXT NOT NULL REFERENCES public.sessions(id) ON DELETE CASCADE,
  total_score  INT  NOT NULL CHECK (total_score BETWEEN 0 AND 27),
  severity     TEXT NOT NULL CHECK (severity IN
                  ('minimal','mild','moderate','moderately severe','severe')),
  -- 9 scores in question-index order [s0, s1, ..., s8]
  responses    JSONB NOT NULL DEFAULT '[]',
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS phq9_assessments_user_idx
  ON public.phq9_assessments(user_id, created_at DESC);


-- ── Row Level Security ────────────────────────────────────────────
-- Users can ONLY read/write their own rows. Enforced at DB level.

ALTER TABLE public.profiles          ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.sessions          ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.messages          ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.memory_embeddings ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.phq9_responses    ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.phq9_assessments  ENABLE ROW LEVEL SECURITY;
-- audit_log is write-only for users, readable only via service key (server-side)
ALTER TABLE public.audit_log         ENABLE ROW LEVEL SECURITY;

-- Profiles: own row only
DROP POLICY IF EXISTS "profiles: own row" ON public.profiles;
CREATE POLICY "profiles: own row" ON public.profiles
  FOR ALL USING (auth.uid() = id);

-- Sessions: own rows only
DROP POLICY IF EXISTS "sessions: own rows" ON public.sessions;
CREATE POLICY "sessions: own rows" ON public.sessions
  FOR ALL USING (auth.uid() = user_id);

-- Messages: own sessions' messages only
DROP POLICY IF EXISTS "messages: own sessions" ON public.messages;
CREATE POLICY "messages: own sessions" ON public.messages
  FOR ALL USING (
    session_id IN (
      SELECT id FROM public.sessions WHERE user_id = auth.uid()
    )
  );

-- Memory: own rows only
DROP POLICY IF EXISTS "memory: own rows" ON public.memory_embeddings;
CREATE POLICY "memory: own rows" ON public.memory_embeddings
  FOR ALL USING (auth.uid() = user_id);

-- PHQ-9: own rows only
DROP POLICY IF EXISTS "phq9_responses: own rows" ON public.phq9_responses;
CREATE POLICY "phq9_responses: own rows" ON public.phq9_responses
  FOR ALL USING (auth.uid() = user_id);

DROP POLICY IF EXISTS "phq9_assessments: own rows" ON public.phq9_assessments;
CREATE POLICY "phq9_assessments: own rows" ON public.phq9_assessments
  FOR ALL USING (auth.uid() = user_id);

-- Audit log: users cannot read; service key (backend) can write via service role
DROP POLICY IF EXISTS "audit_log: service write only" ON public.audit_log;
CREATE POLICY "audit_log: service write only" ON public.audit_log
  FOR INSERT WITH CHECK (true);  -- backend uses service key which bypasses RLS

-- ── Auto-update updated_at ────────────────────────────────────────
CREATE OR REPLACE FUNCTION public.set_updated_at()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS set_profiles_updated_at ON public.profiles;
CREATE TRIGGER set_profiles_updated_at
  BEFORE UPDATE ON public.profiles
  FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();

DROP TRIGGER IF EXISTS set_sessions_updated_at ON public.sessions;
CREATE TRIGGER set_sessions_updated_at
  BEFORE UPDATE ON public.sessions
  FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();

-- ============================================================
-- Memory retrieval RPC
-- Called from backend/agents/memory.py::retrieve_similar.
-- Returns the top-N most semantically similar past exchanges
-- for a given user, across ALL of their sessions.
--
-- IMPORTANT: we DROP first because Postgres won't let
-- CREATE OR REPLACE change a function's return type. If you've
-- ever run a version of this with a different return signature
-- (e.g. session_id uuid instead of text), the CREATE OR REPLACE
-- alone will fail with error 42P13. The explicit DROP makes
-- the rerun safe regardless of what's already in the DB.
-- ============================================================

DROP FUNCTION IF EXISTS public.match_memory_embeddings(vector, uuid, int);

CREATE OR REPLACE FUNCTION public.match_memory_embeddings(
  query_embedding vector(384),
  target_user_id  uuid,
  match_count     int DEFAULT 2
)
RETURNS TABLE (
  id          bigint,
  session_id  text,
  content     text,
  similarity  float
)
LANGUAGE plpgsql
STABLE
AS $$
BEGIN
  RETURN QUERY
  SELECT
    m.id,
    m.session_id,
    m.content,
    -- pgvector's <=> is cosine distance (0 = identical, 2 = opposite).
    -- We return similarity = 1 - distance so callers can sort/threshold
    -- on the more intuitive "higher is better" scale if they want.
    1 - (m.embedding <=> query_embedding) AS similarity
  FROM public.memory_embeddings m
  WHERE m.user_id = target_user_id
    AND m.embedding IS NOT NULL
  ORDER BY m.embedding <=> query_embedding
  LIMIT match_count;
END;
$$;

GRANT EXECUTE ON FUNCTION public.match_memory_embeddings(vector, uuid, int)
  TO authenticated, service_role;

-- ── Useful admin queries (run manually in SQL editor) ─────────────
-- Crisis events last 7 days:
-- SELECT u.email, a.session_id, a.metadata, a.created_at
-- FROM public.audit_log a JOIN auth.users u ON a.user_id = u.id
-- WHERE a.event_type = 'crisis_detected'
--   AND a.created_at > now() - interval '7 days'
-- ORDER BY a.created_at DESC;

-- Daily active users (last 30 days):
-- SELECT date_trunc('day', created_at) AS day, COUNT(DISTINCT user_id)
-- FROM public.sessions GROUP BY 1 ORDER BY 1 DESC LIMIT 30;

-- PHQ-9 severity distribution last 30 days:
-- SELECT severity, COUNT(*)
-- FROM public.phq9_assessments
-- WHERE created_at > now() - interval '30 days'
-- GROUP BY 1 ORDER BY 1;

-- Per-user PHQ-9 trajectory (deltas):
-- SELECT user_id, created_at, total_score,
--        total_score - LAG(total_score) OVER (
--          PARTITION BY user_id ORDER BY created_at
--        ) AS delta
-- FROM public.phq9_assessments
-- ORDER BY user_id, created_at;

-- Adaptive diagnosis — average turns to fire (Phase 10):
-- SELECT AVG(jsonb_array_length(prob_history)) AS avg_turns_to_fire
-- FROM public.sessions
-- WHERE mode = 'diagnosis' AND diagnosis_ready = true;