// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model User {
  id                 String    @id @default(cuid())
  email              String    @unique
  passwordHash       String
  name               String?
  isAdmin            Boolean   @default(false)
  lastLoginAt        DateTime?
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt

  // Relations
  sessions           Session[]
  exercises          Exercise[]
  workouts           Workout[]
  programs           Program[]
  equipment          Equipment[]
  contentItems       ContentItem[]
  aiSuggestions      AISuggestion[]
  aiPromptTemplates  AIPromptTemplate[]
  aiGenerations      AIGeneration[]
  aiConfigProfiles   AIConfigProfile[]
  userPreferences    UserPreferences?

  @@index([email])
}

model Session {
  id                 String    @id @default(cuid())
  userId             String
  token              String    @unique
  expiresAt          DateTime
  createdAt          DateTime  @default(now())

  // Relations
  user               User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
  @@index([token])
  // Composite for "list this user's still-valid sessions" (Settings page,
  // ensure-other-sessions revocation). Also speeds the per-user session
  // cleanup query in deleteOtherSessions.
  @@index([userId, expiresAt])
}

model Exercise {
  id                 String    @id @default(cuid())
  userId             String
  name               String
  description        String?
  muscleGroups       String    // JSON array stored as text: ["chest", "triceps"]
  type               String    // barbell, dumbbell, machine, cable, bodyweight, cardio, kettlebell, other
  inputFields        String    @default("[\"sets\",\"reps\",\"weight\"]") // JSON array: sets, reps, weight, duration, distance, calories
  defaultWeightUnit  String?   // null = use user pref; "kg" for kettlebells, etc.
  isCustom           Boolean   @default(false)
  createdAt          DateTime  @default(now())

  // Relations
  user               User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  setLogs            SetLog[]
  programExercises   ProgramExercise[]

  @@unique([userId, name])
  @@index([userId])
  @@index([type])
}

model Workout {
  id                 String    @id @default(cuid())
  userId             String
  date               DateTime
  name               String?
  notes              String?
  durationMinutes    Int?
  difficulty         Int?      // 1-10 scale
  caloriesBurned     Int?
  /// Optional: which planned ProgramDay this workout was logged
  /// against. Set when the user clicks "Start workout from program
  /// day" so we can later compute adherence vs plan.
  programDayId       String?
  deletedAt          DateTime?
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt

  // Relations
  user               User       @relation(fields: [userId], references: [id], onDelete: Cascade)
  setLogs            SetLog[]
  programDay         ProgramDay? @relation(fields: [programDayId], references: [id], onDelete: SetNull)

  @@index([userId])
  @@index([date])
  @@index([deletedAt])
  @@index([programDayId])
  // Hot path: workout list page is `where userId AND deletedAt IS NULL
  // ORDER BY date DESC LIMIT N`. Composite index lets SQLite walk it
  // straight off the index without sorting.
  @@index([userId, deletedAt, date])
}

model SetLog {
  id                 String    @id @default(cuid())
  workoutId          String
  exerciseId         String
  setNumber          Int
  reps               Int?
  weight             Float?
  weightUnit         String    @default("lbs")
  rpe                Int?      // Rate of Perceived Exertion (1-10)
  durationSeconds    Int?      // for timed exercises (assault bike, jump rope, planks)
  distance           Float?    // for distance-based exercises
  distanceUnit       String?   // "mi", "km", "m"
  calories           Int?      // for cardio machines that report calories
  customMetrics      String?   // JSON map for dynamic custom metrics (e.g. {"watts":"157"})
  notes              String?
  createdAt          DateTime  @default(now())

  // Relations
  workout            Workout   @relation(fields: [workoutId], references: [id], onDelete: Cascade)
  exercise           Exercise  @relation(fields: [exerciseId], references: [id], onDelete: Cascade)

  @@index([workoutId])
  @@index([exerciseId])
  // Sets are always rendered in setNumber order under a workout; this
  // composite removes the sort.
  @@index([workoutId, setNumber])
}

model Program {
  id                 String    @id @default(cuid())
  userId             String
  name               String
  description        String?
  type               String    // hypertrophy, strength, power, endurance, recovery
  durationWeeks      Int
  startDate          DateTime
  endDate            DateTime?
  isActive           Boolean   @default(false)
  aiGenerated        Boolean   @default(false)
  aiNotes            String?
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt

  // Relations
  user               User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  weeks              ProgramWeek[]
  aiSuggestions      AISuggestion[]

  @@index([userId])
  @@index([isActive])
}

model ProgramWeek {
  id                 String    @id @default(cuid())
  programId          String
  weekNumber         Int
  phase              String?
  description        String?
  createdAt          DateTime  @default(now())

  // Relations
  program            Program   @relation(fields: [programId], references: [id], onDelete: Cascade)
  days               ProgramDay[]

  @@unique([programId, weekNumber])
  @@index([programId])
}

model ProgramDay {
  id                 String    @id @default(cuid())
  weekId             String
  dayOfWeek          Int       // 0-6 (Sunday-Saturday)
  name               String?
  description        String?
  createdAt          DateTime  @default(now())

  // Relations
  week               ProgramWeek @relation(fields: [weekId], references: [id], onDelete: Cascade)
  exercises          ProgramExercise[]
  workouts           Workout[]   // sessions logged against this planned day

  @@unique([weekId, dayOfWeek])
  @@index([weekId])
}

model ProgramExercise {
  id                 String    @id @default(cuid())
  dayId              String
  exerciseId         String
  order              Int
  sets               Int?
  repsMin            Int?
  repsMax            Int?
  rpe                Int?
  restSeconds        Int?
  notes              String?
  /// v1.1.0:4 — AI-suggested starting weight (or coach-prescribed
  /// for manual programs). When you "Start workout from program day"
  /// this pre-populates SetLog.weight so the user has a target. Null
  /// = no suggestion, fall back to whatever they did last time.
  suggestedWeight       Float?
  suggestedWeightUnit   String?   // "lbs" | "kg"; null = use user pref
  createdAt          DateTime  @default(now())

  // Relations
  day                ProgramDay @relation(fields: [dayId], references: [id], onDelete: Cascade)
  exercise           Exercise  @relation(fields: [exerciseId], references: [id], onDelete: Cascade)

  @@unique([dayId, order])
  @@index([dayId])
  @@index([exerciseId])
}

model Equipment {
  id                 String    @id @default(cuid())
  userId             String
  name               String
  type               String    // rack, barbell, dumbbell, plate, bench, etc.
  quantity           Int
  weight             Float?    // for plates, dumbbells, etc.
  weightUnit         String    @default("lbs")
  notes              String?
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt

  // Relations
  user               User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([userId, name])
  @@index([userId])
}

model ContentItem {
  id                 String    @id @default(cuid())
  userId             String
  title              String
  source             String    // pdf_upload, youtube, manual_entry
  sourceUrl          String?
  content            String?
  fullText           String?   // Full searchable text
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt

  // Relations
  user               User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  chunks             ContentChunk[]

  @@index([userId])
  @@index([source])
}

model ContentChunk {
  id                 String    @id @default(cuid())
  contentItemId      String
  chunkNumber        Int
  text               String
  pageNumber         Int?
  timestamp          Float?    // For video timestamps
  createdAt          DateTime  @default(now())

  // Relations
  contentItem        ContentItem @relation(fields: [contentItemId], references: [id], onDelete: Cascade)

  @@index([contentItemId])
}

model AISuggestion {
  id                 String    @id @default(cuid())
  userId             String
  programId          String?
  type               String    // exercise_recommendation, workout_adjustment, program_optimization, etc.
  suggestion         String
  priority           Int       // 1-5 scale
  accepted           Boolean?
  source             String    // rules, claude
  metadata           String?   // JSON metadata
  createdAt          DateTime  @default(now())

  // Relations
  user               User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  program            Program?  @relation(fields: [programId], references: [id], onDelete: SetNull)

  @@index([userId])
  @@index([programId])
  @@index([type])
}

/// Singleton row keyed on id=1. Holds instance-wide settings that aren't
/// tied to any single user (multi-user signup gate, feature flags, etc.).
/// Use `getInstanceSettings()` from lib/instanceSettings.ts to read; it
/// upserts the row defensively on first read so callers never see null.
model InstanceSettings {
  id            Int      @id @default(1)
  signupsOpen   Boolean  @default(false)
  updatedAt     DateTime @updatedAt
}

model UserPreferences {
  id                 String    @id @default(cuid())
  userId             String    @unique
  theme              String    @default("system") // light, dark, system
  defaultWeightUnit  String    @default("lbs")
  defaultRestSeconds Int       @default(90)
  // ─── Dead fields, retained for back-compat ────────────────────
  // enableClaudeAI / claudeApiKey were the v1.0.0:1-6 single-provider
  // toggles. v1.0.0:7 removed the photo-import feature that used them
  // and stopped reading/writing them. Kept as columns to avoid a
  // destructive ALTER on existing data; new code uses the aiProvider/
  // aiModel/aiBaseUrl/aiApiKey block below.
  enableClaudeAI     Boolean   @default(false)
  claudeApiKey       String?
  // ─── v1.1.0:2 model-agnostic AI configuration ─────────────────
  // aiProvider: 'claude' | 'openai' | 'openai-compatible' | 'gemini'
  //           | 'ollama' | null (disabled)
  // aiBaseUrl:  required for openai-compatible + ollama; ignored
  //             otherwise. Use http://ollama.embassy:11434 for the
  //             StartOS sister service if it's installed.
  // aiApiKey:   plaintext (consistent with all other secrets in
  //             /data; the host-level threat model assumes /data
  //             is owned by the operator). Null for ollama on a
  //             trusted LAN.
  aiProvider         String?
  aiModel            String?
  aiBaseUrl          String?
  aiApiKey           String?
  // ─── v1.1.0:4 multi-config: which AIConfigProfile is active ───
  // Null = fall back to the legacy single-config columns above (which
  // we keep populated as a mirror of the active profile for backwards-
  // compat with any code path that still reads them).
  activeAIConfigId   String?
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt

  // Relations
  user               User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
}

/// v1.1.0:4 — A single saved AI provider configuration. Users can
/// have many (one per provider, or several of the same provider with
/// different models/keys) and toggle one as active. The active one is
/// what /api/ai/generate and /api/ai/test use.
///
/// We mirror the active profile back into UserPreferences.aiProvider/
/// aiModel/aiBaseUrl/aiApiKey on every "set active" so any old code
/// path that reads from prefs keeps working without conditional logic.
model AIConfigProfile {
  id                 String    @id @default(cuid())
  userId             String
  /// User-chosen label, e.g. "Local Ollama", "Claude Sonnet (work)".
  /// Defaults to a generated name on create if not provided.
  name               String
  provider           String    // 'claude' | 'openai' | 'openai-compatible' | 'gemini' | 'ollama'
  model              String
  baseUrl            String?   // for openai-compatible + ollama
  apiKey             String?   // plaintext, same threat model as the rest of /data
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt

  user               User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
}

/// User-defined or shipped prompt templates for AI program generation.
/// `userId = null` means the template ships with the package (built-in,
/// reconciled per-boot from prisma/aiTemplates.seed.json). `userId =
/// <id>` means a user-created template, fully owned by them.
///
/// Built-in templates are read-only in the UI for non-admins (they
/// can clone-to-edit). Admins can edit built-ins via the same API but
/// edits won't survive the next boot's reconcile pass — to ship
/// changes, edit the JSON in the repo and bump a version.
model AIPromptTemplate {
  id                 String    @id @default(cuid())
  userId             String?
  name               String
  description        String?
  /// Role + constraints sent as the system message to the LLM.
  systemPrompt       String
  /// Body of the user message; supports {{userInput}} interpolation
  /// so the user's specifics (e.g. "4 weeks heavy leg emphasis") get
  /// stitched into the template.
  userPromptTemplate String
  isBuiltIn          Boolean   @default(false)
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt

  user               User?     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
  @@index([isBuiltIn])
}

/// One row per AI generation request. Stores the exact prompts sent
/// (so the user can see why the model produced what it did), the raw
/// model output, the parsed program structure if parsing succeeded,
/// and a status that tracks the lifecycle:
///
///   pending   → request in flight
///   completed → got a response, parsed it; preview available
///   failed    → got an error or parse failure (errorMessage set)
///   applied   → user clicked Apply, Program created (appliedProgramId set)
model AIGeneration {
  id                 String    @id @default(cuid())
  userId             String
  templateId         String?
  templateName       String?
  userInput          String
  systemPrompt       String
  userPrompt         String
  /// Streamed-so-far text. Updated periodically by the background
  /// generator so navigating-away clients can resume display via
  /// polling. Final value matches `rawResponse` once status flips
  /// to 'completed' or 'failed'.
  progressText       String?
  rawResponse        String?
  parsedProgram      String?    // JSON.stringify of the parsed structure
  provider           String
  model              String
  tokensIn           Int?
  tokensOut          Int?
  /// Wall-clock duration in milliseconds from request start to final
  /// status flip. Useful for the "this took 10 minutes" stat in the UI.
  durationMs         Int?
  status             String     @default("pending")
  errorMessage       String?
  appliedProgramId   String?
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt

  user               User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId, createdAt])
  @@index([status])
  @@index([appliedProgramId])
}
