/**
 * SQLite table-creation SQL.
 *
 * SQLite stores only indexes and aggregates: users / login sessions / Project authorization /
 * Agent & Session indexes / usage summaries / error records / UI preferences. Agent State,
 * Trace, and Workspace still follow the local directory-based storage rules.
 * Product not yet released: no migration branches — everything is CREATE IF NOT EXISTS, formed
 * once. The one exception: columns added to an existing table after release of a web.db are
 * ALTERed in by the idempotent per-column guard in database.ts (ensureColumn), since CREATE
 * TABLE IF NOT EXISTS never touches an existing table.
 */

export const SCHEMA_SQL = `
CREATE TABLE IF NOT EXISTS users (
  user_id             TEXT PRIMARY KEY,            -- semantic id doubles as login name: ^[a-z][a-z0-9_-]{1,31}$
  password_hash       TEXT NOT NULL,               -- scrypt$N$r$p$salt$hash(base64)
  is_admin            INTEGER NOT NULL DEFAULT 0,  -- 1 for the built-in admin (seeded at startup)
  password_is_initial INTEGER NOT NULL DEFAULT 0,  -- 1=initial password (seeded/admin-set); cleared once the user changes it
  created_at          TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS auth_sessions (
  token_hash TEXT PRIMARY KEY,               -- sha256(token) hex; the cookie stores only the raw token
  user_id    TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
  created_at TEXT NOT NULL,
  expires_at TEXT NOT NULL,                  -- 7-day sliding renewal (topped up when <6 days remain)
  via        TEXT                            -- 'password' | 'desktop'; NULL = legacy row (password)
);
CREATE TABLE IF NOT EXISTS projects (
  project_id    TEXT PRIMARY KEY,            -- directory name doubles as id; display name lives in project_config.toml
  owner_user_id TEXT NOT NULL REFERENCES users(user_id),
  created_at    TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS project_members (  -- member grants only; owners are not stored here
  project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
  user_id    TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
  created_at TEXT NOT NULL,
  PRIMARY KEY (project_id, user_id)
);
CREATE TABLE IF NOT EXISTS agents (           -- index only; name/description live in system_config.yaml
  project_id TEXT NOT NULL,
  agent_id   TEXT NOT NULL,
  created_at TEXT NOT NULL,
  PRIMARY KEY (project_id, agent_id)
);
CREATE TABLE IF NOT EXISTS sessions (
  session_id    TEXT PRIMARY KEY,
  project_id    TEXT NOT NULL,
  agent_id      TEXT NOT NULL,
  provider      TEXT NOT NULL,                     -- provider group of the session model (paired with model_id as a model reference)
  model_id      TEXT NOT NULL,                     -- upstream model id (sent to AgentHub as-is; never concatenate <provider>/<id>)
  workspace     TEXT NOT NULL,
  approval_mode TEXT NOT NULL DEFAULT 'allow-all',   -- allow-all|deny-all|read-only|always-ask
  title         TEXT,                                -- auto-generated by the model after the first exchange; NULL=not yet (frontend shows "New chat")
  archived_at   TEXT,                                -- archive time; NULL=not archived (shown by default; archived ones move under "Archived")
  client        TEXT,                                -- creating client: 'web' (created via the Web App) | 'cli' (adopted from a CLI Trace); NULL = legacy row, treated as web
  has_trace     INTEGER NOT NULL DEFAULT 0,          -- cache: a Trace record exists (set at task start / adoption / subagent registration; lazily backfilled by list hydration)
  created_at    TEXT NOT NULL
);                                            -- the schedule/subagent SOURCE is NOT stored: session_meta in the Trace is the single source of truth (see runtime/session-sources.ts); "client" is a different, DB-only axis (who created the row)
CREATE INDEX IF NOT EXISTS idx_sessions_agent_created ON sessions(project_id, agent_id, created_at DESC);
CREATE TABLE IF NOT EXISTS usage_records (
  id                INTEGER PRIMARY KEY AUTOINCREMENT,
  ts                TEXT NOT NULL,
  date              TEXT NOT NULL,            -- local date yyyy-mm-dd (aggregation key; same convention as Trace date directories)
  project_id        TEXT NOT NULL,
  agent_id          TEXT NOT NULL,
  session_id        TEXT NOT NULL,            -- top-level Session (child-session usage counts toward its main Session)
  origin_session_id TEXT,                     -- immediate source child Session (last of the origin chain); NULL=main session
  provider          TEXT NOT NULL,            -- provider group: paired with model_id as the attribution key; always GROUP BY provider, model_id
  model_id          TEXT NOT NULL,            -- upstream model id (paired with provider; same model_id across providers aggregates separately)
  cache_read        INTEGER NOT NULL,
  cache_write       INTEGER NOT NULL,
  output            INTEGER NOT NULL,
  total             INTEGER NOT NULL,         -- from token_usage.request (one row per Request)
  status            TEXT NOT NULL DEFAULT 'completed'  -- request outcome: completed=success (with tokens); others=failure (0 tokens, for success rate)
);                                            -- cost is not stored: computed at query time from current pricing
CREATE INDEX IF NOT EXISTS idx_usage_project_date ON usage_records(project_id, date);
CREATE INDEX IF NOT EXISTS idx_usage_session ON usage_records(session_id);
CREATE TABLE IF NOT EXISTS error_records (     -- server-side error capture (the Costs page's "Errors" tab)
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  ts         TEXT NOT NULL,
  date       TEXT NOT NULL,            -- local date yyyy-mm-dd (same convention as usage_records)
  project_id TEXT,                     -- nullable: sign-in/registration and process-level errors have no Project context
  agent_id   TEXT,
  session_id TEXT,
  source     TEXT NOT NULL,            -- http | session | usage | title | subagent | process | llm | environment | compaction | schedule
  kind       TEXT NOT NULL,            -- expected (HttpError, business 4xx) | unexpected (500/runtime)
  code       TEXT NOT NULL,            -- HttpError.code / internal / session_run_failed / ...
  status     INTEGER,                  -- HTTP status code; NULL for non-HTTP sources
  message    TEXT NOT NULL             -- truncated to 500 chars (no stack stored: stacks go to logs only)
);
CREATE INDEX IF NOT EXISTS idx_error_project_date ON error_records(project_id, date);
CREATE TABLE IF NOT EXISTS schedule_state (    -- schedule runtime state (files are declarative intent; the system never writes back)
  project_id      TEXT NOT NULL,
  agent_id        TEXT NOT NULL,
  name            TEXT NOT NULL,               -- file name (without .toml) doubles as the identifier
  creator_user_id TEXT,                        -- creator (recorded on API create; hand-edited file reconciliation falls back to the Project owner)
  start_at_ms     INTEGER NOT NULL,            -- defines identity: a start_at change counts as a new task instance, resetting fire state
  def_hash        TEXT NOT NULL,               -- file content fingerprint: any change clears the invalid flag (the file takes effect again after edits)
  last_slot_ms    INTEGER,                     -- latest due slot already consumed (advances on fire or skip; no re-fire across restarts)
  last_fired_at   TEXT,                        -- last actual fire time (for display)
  fired_once      INTEGER NOT NULL DEFAULT 0,  -- one-shot task has fired
  missed          INTEGER NOT NULL DEFAULT 0,  -- one-shot task was missed (flagged by startup/registration reconciliation; not backfilled)
  invalid_reason  TEXT,                        -- invalidation reason (e.g. the bound Session was deleted); NULL=healthy
  PRIMARY KEY (project_id, agent_id, name)
);
CREATE TABLE IF NOT EXISTS goal_state (        -- goal-mode runtime state (GOAL.yaml on disk is the model-facing protocol; this row is what the UI reads)
  id          INTEGER PRIMARY KEY AUTOINCREMENT,  -- one row per goal run; a Session may run goals repeatedly, latest row wins for display
  session_id  TEXT NOT NULL,
  project_id  TEXT NOT NULL,
  agent_id    TEXT NOT NULL,
  objective   TEXT NOT NULL,
  status      TEXT NOT NULL,               -- active | complete | blocked | budget_limited | aborted (aborted is server-side only; on-disk status stays active)
  budget      INTEGER NOT NULL,            -- token budget; -1 = unlimited
  used        INTEGER NOT NULL DEFAULT 0,  -- uncached input + output, refreshed per round (mirrors the runner's accounting)
  rounds      INTEGER NOT NULL DEFAULT 0,
  created_at  TEXT NOT NULL,
  updated_at  TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_goal_session ON goal_state(session_id);
CREATE TABLE IF NOT EXISTS ui_prefs (
  user_id    TEXT PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE,
  prefs_json TEXT NOT NULL                    -- {theme?, lastProjectId?, ...} free-form JSON
);
CREATE TABLE IF NOT EXISTS server_settings (   -- admin-level server-global settings (GET/PUT /api/admin/settings)
  key   TEXT PRIMARY KEY,                     -- setting name, e.g. 'proxy_for_app'
  value TEXT NOT NULL                         -- JSON-encoded value; an absent row means the setting's built-in default
);
CREATE TABLE IF NOT EXISTS trace_files (       -- DERIVED CACHE of the on-disk Trace tree (services/trace-index.ts): the directories stay the single source of truth, every row is rebuildable from disk, and a row is never authority for absence — consumers reconcile + retry on a miss, so a stale index costs one extra scan, never a false 404
  project_id TEXT NOT NULL,
  agent_id   TEXT NOT NULL,
  session_id TEXT NOT NULL,
  file_index INTEGER NOT NULL,               -- shard index NNN of <session_id>_NNN.jsonl (name/path are reconstructed from the row, never stored: the data root may move)
  date       TEXT NOT NULL,                  -- date directory name (local yyyy-mm-dd)
  size_bytes INTEGER NOT NULL,               -- last observed size (listings stat-refresh the returned page and write back; an actively-appended shard may lag in between)
  page_stats TEXT,                           -- cached message-window scan state at this shard's END (services/message-window.ts ShardPrefixRecord JSON); immutable shards only, NULL = not yet computed, nulled whenever size_bytes changes
  PRIMARY KEY (project_id, agent_id, session_id, file_index)
);
CREATE INDEX IF NOT EXISTS idx_trace_files_agent_date ON trace_files(project_id, agent_id, date);
CREATE INDEX IF NOT EXISTS idx_trace_files_session ON trace_files(session_id);
CREATE TABLE IF NOT EXISTS trace_sessions (    -- per-session facts read ONCE at registration from the earliest shard's head (same derived-cache rules as trace_files; listing needs zero head-reads)
  session_id TEXT PRIMARY KEY,
  project_id TEXT NOT NULL,
  agent_id   TEXT NOT NULL,
  source     TEXT,                           -- session_meta origin: 'subagent' | 'schedule' | NULL = user-created (or head not yet readable)
  workspace  TEXT NOT NULL DEFAULT '',
  title      TEXT,                           -- first-prompt fallback title (sessions.title always wins when present)
  provider   TEXT,                           -- model reference from session_meta (CLI adoption reads it from here; NULL = meta unreadable / legacy without provider)
  model_id   TEXT,
  first_ts   TEXT,                           -- first record's timestamp (adoption's createdAt fallback when the id embeds no time)
  meta_read  INTEGER NOT NULL DEFAULT 0      -- 1 once the head parsed; 0 = facts unknown, retried by the next reconcile that touches the session
);
CREATE INDEX IF NOT EXISTS idx_trace_sessions_agent ON trace_sessions(project_id, agent_id);
`;
