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

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

model Account {
  id                String  @id @default(uuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String? @db.Text
  access_token      String? @db.Text
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String? @db.Text
  session_state     String?
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
  @@index([userId])
  @@map("account")
}

model CharacterAppearance {
  id                   String                  @id @default(uuid())
  characterId          String
  appearanceIndex      Int
  changeReason         String
  description          String?                 @db.Text
  descriptions         String?                 @db.Text
  imageUrl             String?                 @db.Text
  imageUrls            String?                 @db.Text
  selectedIndex        Int?
  createdAt            DateTime                @default(now())
  updatedAt            DateTime                @default(now()) @updatedAt
  previousImageUrl     String?                 @db.Text
  previousImageUrls    String?                 @db.Text
  previousDescription  String?                 @db.Text // 上一次的描述词（用于撤回）
  previousDescriptions String?                 @db.Text // 上一次的描述词数组（用于撤回）
  imageMediaId         String?
  imageMedia           MediaObject?            @relation("CharacterAppearanceImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
  character            NovelPromotionCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade)

  @@unique([characterId, appearanceIndex])
  @@index([characterId])
  @@index([imageMediaId])
  @@map("character_appearances")
}

model LocationImage {
  id                  String                   @id @default(uuid())
  locationId          String
  imageIndex          Int
  description         String?                  @db.Text
  imageUrl            String?                  @db.Text
  isSelected          Boolean                  @default(false)
  createdAt           DateTime                 @default(now())
  updatedAt           DateTime                 @default(now()) @updatedAt
  previousImageUrl    String?                  @db.Text
  previousDescription String?                  @db.Text // 上一次的描述词（用于撤回）
  imageMediaId        String?
  imageMedia          MediaObject?             @relation("LocationImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
  location            NovelPromotionLocation   @relation("LocationImages", fields: [locationId], references: [id], onDelete: Cascade)
  selectedByLocations NovelPromotionLocation[] @relation("SelectedLocationImage")

  @@unique([locationId, imageIndex])
  @@index([locationId])
  @@index([imageMediaId])
  @@map("location_images")
}

model NovelPromotionCharacter {
  id                      String                @id @default(uuid())
  novelPromotionProjectId String
  name                    String
  aliases                 String?               @db.Text
  createdAt               DateTime              @default(now())
  updatedAt               DateTime              @default(now()) @updatedAt
  customVoiceUrl          String?               @db.Text
  customVoiceMediaId      String?
  customVoiceMedia        MediaObject?          @relation("NovelPromotionCharacterVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull)
  voiceId                 String?
  voiceType               String?
  profileData             String?               @db.Text
  profileConfirmed        Boolean               @default(false)
  introduction            String?               @db.Text // 角色介绍（身份、关系、称呼映射，如"我"对应此角色）
  sourceGlobalCharacterId String? // 🆕 来源全局角色ID（复制时记录）
  appearances             CharacterAppearance[]
  novelPromotionProject   NovelPromotionProject @relation(fields: [novelPromotionProjectId], references: [id], onDelete: Cascade)

  @@index([novelPromotionProjectId])
  @@index([customVoiceMediaId])
  @@map("novel_promotion_characters")
}

model NovelPromotionLocation {
  id                      String                @id @default(uuid())
  novelPromotionProjectId String
  name                    String
  summary                 String?               @db.Text // 场景简要描述（用途/人物关联）
  createdAt               DateTime              @default(now())
  updatedAt               DateTime              @default(now()) @updatedAt
  sourceGlobalLocationId  String? // 🆕 来源全局场景ID（复制时记录）
  selectedImageId         String?
  selectedImage           LocationImage?        @relation("SelectedLocationImage", fields: [selectedImageId], references: [id], onDelete: SetNull)
  images                  LocationImage[]       @relation("LocationImages")
  novelPromotionProject   NovelPromotionProject @relation(fields: [novelPromotionProjectId], references: [id], onDelete: Cascade)

  @@index([novelPromotionProjectId])
  @@map("novel_promotion_locations")
}

model NovelPromotionEpisode {
  id                      String                     @id @default(uuid())
  novelPromotionProjectId String
  episodeNumber           Int
  name                    String
  description             String?                    @db.Text
  novelText               String?                    @db.Text
  audioUrl                String?                    @db.Text
  audioMediaId            String?
  audioMedia              MediaObject?               @relation("NovelPromotionEpisodeAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull)
  srtContent              String?                    @db.Text
  createdAt               DateTime                   @default(now())
  updatedAt               DateTime                   @default(now()) @updatedAt
  speakerVoices           String?                    @db.Text
  clips                   NovelPromotionClip[]
  novelPromotionProject   NovelPromotionProject      @relation(fields: [novelPromotionProjectId], references: [id], onDelete: Cascade)
  shots                   NovelPromotionShot[]
  storyboards             NovelPromotionStoryboard[]
  voiceLines              NovelPromotionVoiceLine[]
  editorProject           VideoEditorProject?

  @@unique([novelPromotionProjectId, episodeNumber])
  @@index([novelPromotionProjectId])
  @@index([audioMediaId])
  @@map("novel_promotion_episodes")
}

// 视频编辑器项目 - 存储剪辑数据
model VideoEditorProject {
  id           String                @id @default(uuid())
  episodeId    String                @unique
  projectData  String                @db.Text // JSON 存储编辑项目数据
  renderStatus String? // pending | rendering | completed | failed
  renderTaskId String?
  outputUrl    String?               @db.Text
  createdAt    DateTime              @default(now())
  updatedAt    DateTime              @default(now()) @updatedAt
  episode      NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade)

  @@map("video_editor_projects")
}

model NovelPromotionClip {
  id         String                    @id @default(uuid())
  episodeId  String
  start      Int?
  end        Int?
  duration   Int?
  summary    String                    @db.Text
  location   String?                   @db.Text
  content    String                    @db.Text
  createdAt  DateTime                  @default(now())
  updatedAt  DateTime                  @default(now()) @updatedAt
  characters String?                   @db.Text
  endText    String?                   @db.Text
  shotCount  Int?
  startText  String?                   @db.Text
  screenplay String?                   @db.Text
  episode    NovelPromotionEpisode     @relation(fields: [episodeId], references: [id], onDelete: Cascade)
  shots      NovelPromotionShot[]
  storyboard NovelPromotionStoryboard?

  @@index([episodeId])
  @@map("novel_promotion_clips")
}

model NovelPromotionPanel {
  id                String                    @id @default(uuid())
  storyboardId      String
  panelIndex        Int
  panelNumber       Int?
  shotType          String?                   @db.Text
  cameraMove        String?                   @db.Text
  description       String?                   @db.Text
  location          String?                   @db.Text
  characters        String?                   @db.Text
  srtSegment        String?                   @db.Text
  srtStart          Float?
  srtEnd            Float?
  duration          Float?
  imagePrompt       String?                   @db.Text
  imageUrl          String?                   @db.Text
  imageMediaId      String?
  imageMedia        MediaObject?              @relation("NovelPromotionPanelImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
  imageHistory      String?                   @db.Text
  videoPrompt       String?                   @db.Text
  firstLastFramePrompt String?                @db.Text
  videoUrl          String?                   @db.Text
  videoGenerationMode String?                 @db.Text // 视频生成方式：normal | firstlastframe
  videoMediaId      String?
  videoMedia        MediaObject?              @relation("NovelPromotionPanelVideoMedia", fields: [videoMediaId], references: [id], onDelete: SetNull)
  createdAt         DateTime                  @default(now())
  updatedAt         DateTime                  @default(now()) @updatedAt
  sceneType         String?
  candidateImages   String?                   @db.Text
  linkedToNextPanel Boolean                   @default(false)
  lipSyncTaskId     String?
  lipSyncVideoUrl   String?
  lipSyncVideoMediaId String?
  lipSyncVideoMedia MediaObject?              @relation("NovelPromotionPanelLipSyncVideoMedia", fields: [lipSyncVideoMediaId], references: [id], onDelete: SetNull)
  sketchImageUrl    String?                   @db.Text
  sketchImageMediaId String?
  sketchImageMedia  MediaObject?              @relation("NovelPromotionPanelSketchMedia", fields: [sketchImageMediaId], references: [id], onDelete: SetNull)
  photographyRules  String?                   @db.Text
  actingNotes       String?                   @db.Text // 演技指导数据 JSON
  previousImageUrl  String?                   @db.Text
  previousImageMediaId String?
  previousImageMedia MediaObject?             @relation("NovelPromotionPanelPreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull)
  storyboard        NovelPromotionStoryboard  @relation(fields: [storyboardId], references: [id], onDelete: Cascade)
  matchedVoiceLines NovelPromotionVoiceLine[]

  @@unique([storyboardId, panelIndex])
  @@index([storyboardId])
  @@index([imageMediaId])
  @@index([videoMediaId])
  @@index([lipSyncVideoMediaId])
  @@index([sketchImageMediaId])
  @@index([previousImageMediaId])
  @@map("novel_promotion_panels")
}

model NovelPromotionProject {
  id              String                    @id @default(uuid())
  projectId       String                    @unique
  createdAt       DateTime                  @default(now())
  updatedAt       DateTime                  @default(now()) @updatedAt
  analysisModel   String? // 用户配置的分析模型（nullable，必须配置后才能使用）
  imageModel      String? // 用户配置的图片模型
  videoModel      String? // 用户配置的视频模型
  audioModel      String? // 用户配置的语音模型
  videoRatio      String                    @default("9:16")
  ttsRate         String                    @default("+50%")
  globalAssetText String?                   @db.Text
  artStyle        String                    @default("american-comic")
  artStylePrompt  String?                   @db.Text
  characterModel  String? // 用户配置的角色图片模型
  locationModel   String? // 用户配置的场景图片模型
  storyboardModel String? // 用户配置的分镜图片模型
  editModel       String? // 用户配置的修图/编辑模型
  videoResolution String                    @default("720p")
  capabilityOverrides String?              @db.Text
  workflowMode    String                    @default("srt")
  lastEpisodeId   String?
  imageResolution String                    @default("2K")
  importStatus    String?
  characters      NovelPromotionCharacter[]
  episodes        NovelPromotionEpisode[]
  locations       NovelPromotionLocation[]
  project         Project                   @relation(fields: [projectId], references: [id], onDelete: Cascade)

  @@map("novel_promotion_projects")
}

model NovelPromotionShot {
  id              String                @id @default(uuid())
  episodeId       String
  clipId          String?
  shotId          String
  srtStart        Int
  srtEnd          Int
  srtDuration     Float
  sequence        String?               @db.Text
  locations       String?               @db.Text
  characters      String?               @db.Text
  plot            String?               @db.Text
  imagePrompt     String?               @db.Text
  scale           String?               @db.Text
  module          String?               @db.Text
  focus           String?               @db.Text
  zhSummarize     String?               @db.Text
  imageUrl        String?               @db.Text
  imageMediaId    String?
  imageMedia      MediaObject?          @relation("NovelPromotionShotImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
  createdAt       DateTime              @default(now())
  updatedAt       DateTime              @default(now()) @updatedAt
  pov             String?               @db.Text
  clip            NovelPromotionClip?   @relation(fields: [clipId], references: [id], onDelete: Cascade)
  episode         NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade)

  @@index([clipId])
  @@index([episodeId])
  @@index([shotId])
  @@index([imageMediaId])
  @@map("novel_promotion_shots")
}

model NovelPromotionStoryboard {
  id                  String                @id @default(uuid())
  episodeId           String
  clipId              String                @unique
  storyboardImageUrl  String?               @db.Text
  createdAt           DateTime              @default(now())
  updatedAt           DateTime              @default(now()) @updatedAt
  panelCount          Int                   @default(9)
  storyboardTextJson  String?               @db.Text
  imageHistory        String?               @db.Text
  candidateImages     String?               @db.Text
  lastError           String?
  photographyPlan     String?               @db.Text
  panels              NovelPromotionPanel[]
  clip                NovelPromotionClip    @relation(fields: [clipId], references: [id], onDelete: Cascade)
  episode             NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
  supplementaryPanels SupplementaryPanel[]

  @@index([clipId])
  @@index([episodeId])
  @@map("novel_promotion_storyboards")
}

model SupplementaryPanel {
  id            String                   @id @default(uuid())
  storyboardId  String
  sourceType    String
  sourcePanelId String?
  description   String?                  @db.Text
  imagePrompt   String?                  @db.Text
  imageUrl      String?                  @db.Text
  imageMediaId  String?
  imageMedia    MediaObject?             @relation("SupplementaryPanelImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
  characters    String?                  @db.Text
  location      String?                  @db.Text
  createdAt     DateTime                 @default(now())
  updatedAt     DateTime                 @default(now()) @updatedAt
  storyboard    NovelPromotionStoryboard @relation(fields: [storyboardId], references: [id], onDelete: Cascade)

  @@index([storyboardId])
  @@index([imageMediaId])
  @@map("supplementary_panels")
}

model Project {
  id                 String                 @id @default(uuid())
  name               String
  description        String?                @db.Text
  mode               String                 @default("novel-promotion")
  userId             String
  createdAt          DateTime               @default(now())
  updatedAt          DateTime               @default(now()) @updatedAt
  lastAccessedAt     DateTime?
  novelPromotionData NovelPromotionProject?
  user               User                   @relation(fields: [userId], references: [id], onDelete: Cascade)
  usageCosts         UsageCost[]

  @@index([userId])
  @@map("projects")
}

model Session {
  id           String   @id @default(uuid())
  sessionToken String   @unique(map: "Session_sessionToken_key")
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([userId])
  @@map("session")
}

model UsageCost {
  id        String   @id @default(uuid())
  projectId String
  userId    String
  apiType   String
  model     String
  action    String
  quantity  Int
  unit      String
  cost      Decimal  @db.Decimal(18, 6)
  metadata  String?  @db.Text
  createdAt DateTime @default(now())
  project   Project  @relation(fields: [projectId], references: [id], onDelete: Cascade)
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@index([apiType])
  @@index([createdAt])
  @@index([projectId])
  @@index([userId])
  @@map("usage_costs")
}

model User {
  id            String          @id @default(uuid())
  name          String          @unique(map: "User_name_key")
  email         String?
  emailVerified DateTime?
  image         String?
  password      String?
  createdAt     DateTime        @default(now())
  updatedAt     DateTime        @default(now()) @updatedAt
  accounts      Account[]
  projects      Project[]
  sessions      Session[]
  usageCosts    UsageCost[]
  balance       UserBalance?
  preferences   UserPreference?

  // 资产中心
  globalAssetFolders GlobalAssetFolder[]
  globalCharacters   GlobalCharacter[]
  globalLocations    GlobalLocation[]
  globalVoices       GlobalVoice[]
  tasks              Task[]
  taskEvents         TaskEvent[]
  graphRuns          GraphRun[]
  graphEvents        GraphEvent[]

  @@map("user")
}

model UserPreference {
  id              String   @id @default(uuid())
  userId          String   @unique
  analysisModel   String? // 用户配置的分析模型（nullable，必须配置后才能使用）
  characterModel  String? // 用户配置的角色图片模型
  locationModel   String? // 用户配置的场景图片模型
  storyboardModel String? // 用户配置的分镜图片模型
  editModel       String? // 用户配置的修图模型
  videoModel      String? // 用户配置的视频模型
  audioModel      String? // 用户配置的语音模型
  lipSyncModel    String? // 用户配置的口型同步模型
  voiceDesignModel String? // 用户配置的音色设计模型
  analysisConcurrency Int? // 分析流程并发上限
  imageConcurrency Int? // 图像流程并发上限
  videoConcurrency Int? // 视频流程并发上限
  videoRatio      String   @default("9:16")
  videoResolution String   @default("720p")
  artStyle        String   @default("american-comic")
  ttsRate         String   @default("+50%")
  createdAt       DateTime @default(now())
  updatedAt       DateTime @default(now()) @updatedAt
  imageResolution String   @default("2K")
  capabilityDefaults String? @db.Text

  // API Key 配置（极简版）
  llmBaseUrl  String? @default("https://openrouter.ai/api/v1")
  llmApiKey   String? @db.Text // 加密存储
  falApiKey   String? @db.Text // FAL（图片+视频+语音）
  googleAiKey String? @db.Text // Google AI（Gemini 图片）
  arkApiKey   String? @db.Text // 火山引擎（Seedream+Seedance）
  qwenApiKey  String? @db.Text // 阿里百炼（声音设计）

  // 自定义模型列表 + 价格（JSON）
  customModels String? @db.Text

  // 自定义 OpenAI 兼容提供商列表（JSON，包含加密的 API Key）
  customProviders String? @db.Text

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

  @@map("user_preferences")
}

model VerificationToken {
  identifier String
  token      String   @unique(map: "VerificationToken_token_key")
  expires    DateTime

  @@unique([identifier, token])
  @@map("verificationtoken")
}

model NovelPromotionVoiceLine {
  id                  String                @id @default(uuid())
  episodeId           String
  lineIndex           Int
  speaker             String
  content             String                @db.Text
  voicePresetId       String?
  audioUrl            String?               @db.Text
  audioMediaId        String?
  audioMedia          MediaObject?          @relation("NovelPromotionVoiceLineAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull)
  createdAt           DateTime              @default(now())
  updatedAt           DateTime              @default(now()) @updatedAt
  emotionPrompt       String?               @db.Text
  emotionStrength     Float?                @default(0.4)
  matchedPanelIndex   Int?
  matchedStoryboardId String?
  audioDuration       Int?
  matchedPanelId      String?
  episode             NovelPromotionEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade)
  matchedPanel        NovelPromotionPanel?  @relation(fields: [matchedPanelId], references: [id])

  @@unique([episodeId, lineIndex])
  @@index([episodeId])
  @@index([matchedPanelId])
  @@index([audioMediaId])
  @@map("novel_promotion_voice_lines")
}

model VoicePreset {
  id          String   @id @default(uuid())
  name        String
  audioUrl    String   @db.Text
  audioMediaId String?
  audioMedia  MediaObject? @relation("VoicePresetAudioMedia", fields: [audioMediaId], references: [id], onDelete: SetNull)
  description String?  @db.Text
  gender      String?
  isSystem    Boolean  @default(true)
  createdAt   DateTime @default(now())

  @@index([audioMediaId])
  @@map("voice_presets")
}

model UserBalance {
  id           String   @id @default(uuid())
  userId       String   @unique
  balance      Decimal  @default(0) @db.Decimal(18, 6)
  frozenAmount Decimal  @default(0) @db.Decimal(18, 6)
  totalSpent   Decimal  @default(0) @db.Decimal(18, 6)
  createdAt    DateTime @default(now())
  updatedAt    DateTime @default(now()) @updatedAt
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("user_balances")
}

model BalanceFreeze {
  id        String   @id @default(uuid())
  userId    String
  amount    Decimal  @db.Decimal(18, 6)
  status    String   @default("pending")
  source    String?  @db.VarChar(64)
  taskId    String?
  requestId String?
  idempotencyKey String? @unique
  metadata  String?  @db.Text
  expiresAt DateTime?
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt

  @@index([userId])
  @@index([status])
  @@index([taskId])
  @@map("balance_freezes")
}

model BalanceTransaction {
  id           String   @id @default(uuid())
  userId       String
  type         String
  amount       Decimal  @db.Decimal(18, 6)
  balanceAfter Decimal  @db.Decimal(18, 6)
  description  String?  @db.Text
  relatedId    String?
  freezeId     String?
  operatorId   String?  @db.VarChar(64)
  externalOrderId String? @db.VarChar(128)
  idempotencyKey String? @db.VarChar(128)
  projectId    String?  @db.VarChar(128) // 关联项目 ID，用于流水展示项目名
  episodeId    String?  @db.VarChar(128) // 关联集数 ID，用于流水展示集数
  taskType     String?  @db.VarChar(64)  // 任务类型 key（与 action 一致），用于前端 i18n
  billingMeta  String?  @db.Text         // 计费详情 JSON: { quantity, unit, model, resolution, duration, tokens... }
  createdAt    DateTime @default(now())

  @@index([userId])
  @@index([type])
  @@index([createdAt])
  @@index([freezeId])
  @@index([externalOrderId])
  @@index([projectId])
  @@unique([userId, type, idempotencyKey])
  @@map("balance_transactions")
}

model Task {
  id               String    @id @default(uuid())
  userId           String
  projectId        String
  episodeId        String?
  type             String
  targetType       String
  targetId         String
  status           String    @default("queued")
  progress         Int       @default(0)
  attempt          Int       @default(0)
  maxAttempts      Int       @default(5)
  priority         Int       @default(0)
  dedupeKey        String?   @unique
  externalId       String?
  payload          Json?
  result           Json?
  errorCode        String?
  errorMessage     String?   @db.Text
  billingInfo      Json?
  billedAt         DateTime?
  queuedAt         DateTime  @default(now())
  startedAt        DateTime?
  finishedAt       DateTime?
  heartbeatAt      DateTime?
  enqueuedAt       DateTime?
  enqueueAttempts  Int       @default(0)
  lastEnqueueError String?   @db.Text
  createdAt        DateTime  @default(now())
  updatedAt        DateTime  @updatedAt

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

  @@index([status])
  @@index([type])
  @@index([targetType, targetId])
  @@index([projectId])
  @@index([userId])
  @@index([heartbeatAt])
  @@map("tasks")
}

model TaskEvent {
  id        Int      @id @default(autoincrement())
  taskId    String
  projectId String
  userId    String
  eventType String
  payload   Json?
  createdAt DateTime @default(now())

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

  @@index([projectId, id])
  @@index([taskId])
  @@index([userId])
  @@map("task_events")
}

model GraphRun {
  id                String             @id @default(uuid())
  userId            String
  projectId         String
  episodeId         String?
  workflowType      String
  taskType          String?
  taskId            String?            @unique
  targetType        String
  targetId          String
  status            String             @default("queued")
  input             Json?
  output            Json?
  errorCode         String?
  errorMessage      String?            @db.Text
  cancelRequestedAt DateTime?
  queuedAt          DateTime           @default(now())
  startedAt         DateTime?
  finishedAt        DateTime?
  lastSeq           Int                @default(0)
  createdAt         DateTime           @default(now())
  updatedAt         DateTime           @updatedAt
  user              User               @relation(fields: [userId], references: [id], onDelete: Cascade)
  steps             GraphStep[]
  attempts          GraphStepAttempt[]
  events            GraphEvent[]
  checkpoints       GraphCheckpoint[]
  artifacts         GraphArtifact[]

  @@index([projectId, status])
  @@index([userId, createdAt])
  @@index([taskId])
  @@index([targetType, targetId])
  @@map("graph_runs")
}

model GraphStep {
  id               String             @id @default(uuid())
  runId            String
  stepKey          String
  stepTitle        String
  status           String             @default("pending")
  currentAttempt   Int                @default(0)
  stepIndex        Int
  stepTotal        Int
  startedAt        DateTime?
  finishedAt       DateTime?
  lastErrorCode    String?
  lastErrorMessage String?            @db.Text
  createdAt        DateTime           @default(now())
  updatedAt        DateTime           @updatedAt
  run              GraphRun           @relation(fields: [runId], references: [id], onDelete: Cascade)
  attempts         GraphStepAttempt[]

  @@unique([runId, stepKey])
  @@index([runId, status])
  @@index([runId, stepIndex])
  @@map("graph_steps")
}

model GraphStepAttempt {
  id              String     @id @default(uuid())
  runId           String
  stepKey         String
  attempt         Int
  status          String     @default("pending")
  provider        String?
  modelKey        String?
  inputHash       String?
  input           Json?
  outputText      String?    @db.Text
  outputReasoning String?    @db.Text
  usageJson       Json?
  errorCode       String?
  errorMessage    String?    @db.Text
  startedAt       DateTime?
  finishedAt      DateTime?
  createdAt       DateTime   @default(now())
  updatedAt       DateTime   @updatedAt
  run             GraphRun   @relation(fields: [runId], references: [id], onDelete: Cascade)
  step            GraphStep  @relation(fields: [runId, stepKey], references: [runId, stepKey], onDelete: Cascade)

  @@unique([runId, stepKey, attempt])
  @@index([runId, stepKey])
  @@index([runId, createdAt])
  @@map("graph_step_attempts")
}

model GraphEvent {
  id        BigInt   @id @default(autoincrement())
  runId     String
  projectId String
  userId    String
  seq       Int
  eventType String
  stepKey   String?
  attempt   Int?
  lane      String?
  payload   Json?
  createdAt DateTime @default(now())
  run       GraphRun @relation(fields: [runId], references: [id], onDelete: Cascade)
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([runId, seq])
  @@index([projectId, id])
  @@index([runId, id])
  @@index([userId, id])
  @@map("graph_events")
}

model GraphCheckpoint {
  id         String   @id @default(uuid())
  runId      String
  nodeKey    String
  version    Int
  stateJson  Json
  stateBytes Int
  createdAt  DateTime @default(now())
  run        GraphRun @relation(fields: [runId], references: [id], onDelete: Cascade)

  @@unique([runId, nodeKey, version])
  @@index([runId, createdAt])
  @@map("graph_checkpoints")
}

model GraphArtifact {
  id          String   @id @default(uuid())
  runId       String
  stepKey     String?
  artifactType String
  refId       String
  versionHash String?
  payload     Json?
  createdAt   DateTime @default(now())
  run         GraphRun @relation(fields: [runId], references: [id], onDelete: Cascade)

  @@unique([runId, stepKey, artifactType, refId])
  @@index([runId])
  @@index([runId, stepKey])
  @@index([artifactType, refId])
  @@map("graph_artifacts")
}

// ==================== 资产中心 ====================

// 资产文件夹（一层，不支持嵌套）
model GlobalAssetFolder {
  id        String   @id @default(uuid())
  userId    String
  name      String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  user       User              @relation(fields: [userId], references: [id], onDelete: Cascade)
  characters GlobalCharacter[]
  locations  GlobalLocation[]
  voices     GlobalVoice[]

  @@index([userId])
  @@map("global_asset_folders")
}

// 全局角色（结构与 NovelPromotionCharacter 一致）
model GlobalCharacter {
  id               String   @id @default(uuid())
  userId           String
  folderId         String?
  name             String
  aliases          String?  @db.Text
  profileData      String?  @db.Text
  profileConfirmed Boolean  @default(false)
  voiceId          String?
  voiceType        String?
  customVoiceUrl   String?  @db.Text
  customVoiceMediaId String?
  customVoiceMedia MediaObject? @relation("GlobalCharacterVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull)
  globalVoiceId    String? // 绑定的全局音色 ID
  createdAt        DateTime @default(now())
  updatedAt        DateTime @updatedAt

  user        User                        @relation(fields: [userId], references: [id], onDelete: Cascade)
  folder      GlobalAssetFolder?          @relation(fields: [folderId], references: [id], onDelete: SetNull)
  appearances GlobalCharacterAppearance[]

  @@index([userId])
  @@index([folderId])
  @@index([customVoiceMediaId])
  @@map("global_characters")
}

// 全局角色形象（结构与 CharacterAppearance 一致）
model GlobalCharacterAppearance {
  id                   String   @id @default(uuid())
  characterId          String
  appearanceIndex      Int
  changeReason         String   @default("default")
  artStyle             String?
  description          String?  @db.Text
  descriptions         String?  @db.Text
  imageUrl             String?  @db.Text
  imageMediaId         String?
  imageMedia           MediaObject? @relation("GlobalCharacterAppearanceImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
  imageUrls            String?  @db.Text
  selectedIndex        Int?
  previousImageUrl     String?  @db.Text
  previousImageMediaId String?
  previousImageMedia   MediaObject? @relation("GlobalCharacterAppearancePreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull)
  previousImageUrls    String?  @db.Text
  previousDescription  String?  @db.Text // 上一次的描述词（用于撤回）
  previousDescriptions String?  @db.Text // 上一次的描述词数组（用于撤回）
  createdAt            DateTime @default(now())
  updatedAt            DateTime @updatedAt

  character GlobalCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade)

  @@unique([characterId, appearanceIndex])
  @@index([characterId])
  @@index([imageMediaId])
  @@index([previousImageMediaId])
  @@map("global_character_appearances")
}

// 全局场景（结构与 NovelPromotionLocation 一致）
model GlobalLocation {
  id        String   @id @default(uuid())
  userId    String
  folderId  String?
  name      String
  artStyle  String?
  summary   String?  @db.Text
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  user   User                  @relation(fields: [userId], references: [id], onDelete: Cascade)
  folder GlobalAssetFolder?    @relation(fields: [folderId], references: [id], onDelete: SetNull)
  images GlobalLocationImage[]

  @@index([userId])
  @@index([folderId])
  @@map("global_locations")
}

// 全局场景图片（结构与 LocationImage 一致）
model GlobalLocationImage {
  id                  String   @id @default(uuid())
  locationId          String
  imageIndex          Int
  description         String?  @db.Text
  imageUrl            String?  @db.Text
  imageMediaId        String?
  imageMedia          MediaObject? @relation("GlobalLocationImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull)
  isSelected          Boolean  @default(false)
  previousImageUrl    String?  @db.Text
  previousImageMediaId String?
  previousImageMedia  MediaObject? @relation("GlobalLocationImagePreviousImageMedia", fields: [previousImageMediaId], references: [id], onDelete: SetNull)
  previousDescription String?  @db.Text // 上一次的描述词（用于撤回）
  createdAt           DateTime @default(now())
  updatedAt           DateTime @updatedAt

  location GlobalLocation @relation(fields: [locationId], references: [id], onDelete: Cascade)

  @@unique([locationId, imageIndex])
  @@index([locationId])
  @@index([imageMediaId])
  @@index([previousImageMediaId])
  @@map("global_location_images")
}

// 全局音色库
model GlobalVoice {
  id             String   @id @default(uuid())
  userId         String
  folderId       String?
  name           String // 音色名称
  description    String?  @db.Text // 详细描述
  voiceId        String? // qwen-tts-vd 的 voice ID
  voiceType      String   @default("qwen-designed") // qwen-designed | custom
  customVoiceUrl String?  @db.Text // 上传的音频 URL（预览用）
  customVoiceMediaId String?
  customVoiceMedia MediaObject? @relation("GlobalVoiceCustomVoiceMedia", fields: [customVoiceMediaId], references: [id], onDelete: SetNull)
  voicePrompt    String?  @db.Text // AI 设计时的提示词
  gender         String? // male | female | neutral
  language       String   @default("zh")
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt

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

  @@index([userId])
  @@index([folderId])
  @@index([customVoiceMediaId])
  @@map("global_voices")
}

model MediaObject {
  id         String   @id @default(uuid())
  publicId   String   @unique
  storageKey String   @unique @db.VarChar(512)
  sha256     String?
  mimeType   String?
  sizeBytes  BigInt?
  width      Int?
  height     Int?
  durationMs Int?
  createdAt  DateTime @default(now())
  updatedAt  DateTime @default(now()) @updatedAt

  characterAppearanceImages             CharacterAppearance[]       @relation("CharacterAppearanceImageMedia")
  locationImages                        LocationImage[]             @relation("LocationImageMedia")
  novelPromotionCharacterVoices         NovelPromotionCharacter[]   @relation("NovelPromotionCharacterVoiceMedia")
  novelPromotionEpisodeAudios           NovelPromotionEpisode[]     @relation("NovelPromotionEpisodeAudioMedia")
  novelPromotionPanelImages             NovelPromotionPanel[]       @relation("NovelPromotionPanelImageMedia")
  novelPromotionPanelVideos             NovelPromotionPanel[]       @relation("NovelPromotionPanelVideoMedia")
  novelPromotionPanelLipSyncVideos      NovelPromotionPanel[]       @relation("NovelPromotionPanelLipSyncVideoMedia")
  novelPromotionPanelSketchImages       NovelPromotionPanel[]       @relation("NovelPromotionPanelSketchMedia")
  novelPromotionPanelPreviousImages     NovelPromotionPanel[]       @relation("NovelPromotionPanelPreviousImageMedia")
  novelPromotionShotImages              NovelPromotionShot[]        @relation("NovelPromotionShotImageMedia")
  supplementaryPanelImages              SupplementaryPanel[]        @relation("SupplementaryPanelImageMedia")
  novelPromotionVoiceLineAudios         NovelPromotionVoiceLine[]   @relation("NovelPromotionVoiceLineAudioMedia")
  voicePresetAudios                     VoicePreset[]               @relation("VoicePresetAudioMedia")
  globalCharacterVoices                 GlobalCharacter[]           @relation("GlobalCharacterVoiceMedia")
  globalCharacterAppearanceImages       GlobalCharacterAppearance[] @relation("GlobalCharacterAppearanceImageMedia")
  globalCharacterAppearancePreviousImgs GlobalCharacterAppearance[] @relation("GlobalCharacterAppearancePreviousImageMedia")
  globalLocationImageImages             GlobalLocationImage[]       @relation("GlobalLocationImageMedia")
  globalLocationImagePreviousImages     GlobalLocationImage[]       @relation("GlobalLocationImagePreviousImageMedia")
  globalVoiceCustomVoices               GlobalVoice[]               @relation("GlobalVoiceCustomVoiceMedia")

  @@index([createdAt])
  @@map("media_objects")
}

model LegacyMediaRefBackup {
  id          String   @id @default(uuid())
  runId       String
  tableName   String
  rowId       String
  fieldName   String
  legacyValue String   @db.Text
  checksum    String
  createdAt   DateTime @default(now())

  @@index([runId])
  @@index([tableName, fieldName])
  @@map("legacy_media_refs_backup")
}
