const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:4291"

function apiError(prefix: string, status: number, text: string) {
  let detail = text
  try {
    const parsed = JSON.parse(text)
    detail = parsed?.detail || text
  } catch {
    detail = text
  }
  return new Error(`${prefix} ${status} ${String(detail).slice(0, 300)}`)
}

export type JobStatus =
  | "created"
  | "downloading"
  | "downloaded"
  | "splitting"
  | "frames_extracted"
  | "transcribing"
  | "transcribed"
  | "failed"

export interface FrameObject {
  name: string
  position?: string
  color?: string
  extract_prompt?: string
}

export interface FrameDescription {
  scene?: string
  objects?: FrameObject[]
  style?: string
  suggested_prompt?: string
  transparent_human_assessment?: TransparentHumanFrameScore
}

export interface GeneratedImage {
  id: string
  prompt: string
  model: string
  mode: string
  url: string
  selected: boolean
  created_at: number
}

export interface KeyElement {
  id: string
  name_zh: string
  name_en: string
  position?: string
  source: "auto" | "manual" | "region"
  region?: { x: number; y: number; w: number; h: number } | null
  cutouts?: string[]               // v2 多张提取图 id 列表
  cutout_id?: string | null         // v1 兼容字段
  cutout_background?: "white" | "black"
  subject_kind?: SubjectKind
  subject_assets?: SubjectAsset[]
  subject_consensus_brief?: string
  subject_consensus_brief_zh?: string
  created_at?: number
}

export interface ImageRef {
  kind: "keyframe" | "cutout" | "asset" | "library_subject" | "library_product" | "library_scene"
  frame_idx: number
  element_id?: string | null
  cutout_id?: string | null
  label?: string
  asset_meta?: {
    standard?: string
    original_width?: number
    original_height?: number
    width?: number
    height?: number
    original_bytes?: number
    work_bytes?: number
    max_side?: number
    min_long_side?: number
    min_short_side?: number
    quality?: number
    actions?: string[]
    warnings?: string[]
    normalized?: boolean
  }
}

export type PromptLibraryCategory = "scene_desc" | "video_desc" | "subject_desc" | "skg_script" | "product_angle"
export type AssetLibraryKind = "subjects" | "products" | "scenes" | "videos"

export interface PromptLibraryItem {
  id: string
  category: PromptLibraryCategory
  name: string
  tags: string[]
  prompt_en: string
  prompt_zh: string
  use_count: number
  source_job_id: string
  created_at: number
  updated_at: number
}

export interface AssetLibraryImage {
  id: string
  view: string
  label: string
  filename: string
  url: string
  width: number
  height: number
  created_at: number
}

export interface AssetLibraryItem {
  id: string
  kind: AssetLibraryKind
  name: string
  name_zh?: string
  note?: string
  tags: string[]
  source_job_id?: string
  use_count: number
  created_at: number
  updated_at: number
  is_official?: boolean
  prompt_brief?: string
  prompt_brief_zh?: string
  subject_style?: "transparent_human" | "source_actor" | "cartoon_subject"
  product_type?: string
  views?: AssetLibraryImage[]
  images?: AssetLibraryImage[]
  asset_role?: string
  aspect_ratio?: string
  image?: AssetLibraryImage | null
  duration?: number
  poster?: AssetLibraryImage | null
  video_url?: string
}

export interface ResourceLibraryRecentItem {
  type: "prompt" | "asset"
  id: string
  name: string
  category?: PromptLibraryCategory
  kind?: AssetLibraryKind
  created_at: number
  item: PromptLibraryItem | AssetLibraryItem
}

export interface ProductFusionRegion {
  x: number
  y: number
  w: number
  h: number
}

export interface ProductFusionShot {
  id: string
  first_image?: ImageRef | null
  last_image?: ImageRef | null
  product_images?: ImageRef[]
  product_image?: ImageRef | null
  character_id?: string
  character_name?: string
  subject_image?: ImageRef | null
  subject_images?: ImageRef[]
  person_image?: ImageRef | null
  product_region?: ProductFusionRegion | null
  scene_image?: ImageRef | null
  action_text?: string
  duration?: number
  image_model?: "gpt-image-2"
  video_model?: "seedance"
  guide_image?: ImageRef | null
}

export interface StoryboardScene {
  duration: number
  first_image?: ImageRef | null
  last_image?: ImageRef | null
  product_images?: ImageRef[]
  subject_images?: ImageRef[]
  product_fusion_shots?: ProductFusionShot[]
  visual_mode?: "person_only" | "person_product" | "product_only" | "environment"
  needs_product?: boolean
  needs_subject?: boolean
  storyboard_row_idx?: number | null
  subject_brief?: string
  skg_copy_en?: string
  skg_copy_zh?: string
  scene_one_line_en?: string
  scene_one_line_zh?: string
  action_one_line_en?: string
  action_one_line_zh?: string
  selected_video_id?: string
  first_frame_plan?: string
  last_frame_plan?: string
  product_placement?: string
  subject_image?: ImageRef | null
  scene_image?: ImageRef | null
  product_image?: ImageRef | null
  action_image?: ImageRef | null
  // v1 兼容
  subject?: string
  product?: string
  scene?: string
  action?: string
  reference_ids?: string[]
}

export interface QuickStoryboardPlanInput {
  skg_copy_en?: string
  skg_copy_zh?: string
  scene_one_line_en?: string
  scene_one_line_zh?: string
  action_one_line_en?: string
  action_one_line_zh?: string
  subject_brief?: string
  duration?: number
  visual_mode?: StoryboardScene["visual_mode"]
  needs_product?: boolean
  needs_subject?: boolean
}

export interface RefineStoryboardResult {
  items: {
    skg_copy_en: string
    skg_copy_zh: string
    scene_one_line_en: string
    scene_one_line_zh: string
    action_one_line_en: string
    action_one_line_zh: string
  }
  model: string
  error?: string
}

export interface GeneratedVideo {
  id: string
  provider_id?: string
  frame_idx: number
  storyboard_row_idx?: number | null
  prompt: string
  model: string
  status: "queued" | "in_progress" | "completed" | "failed"
  url?: string
  poster_url?: string
  duration: number
  progress: number
  error?: string
  created_at: number
  queue_position?: number
  queue_size?: number
  queue_message?: string
}

export interface RuntimeModelOption {
  id: string
  label: string
  model: string
  description?: string
  available?: boolean
  duration_options?: number[]
  max_duration_seconds?: number
  size_options?: RuntimeSizeOption[]
}

export interface RuntimeSizeOption {
  id: string
  label: string
  value: string
  description?: string
}

export interface RuntimeModels {
  asr?: string
  asr_language?: string
  asr_base_url?: string
  asr_remote_enabled?: boolean
  asr_local_fallback_enabled?: boolean
  asr_audio_fallback_enabled?: boolean
  faster_whisper?: string
  local_asr?: string
  asr_fallback?: string
  translate?: string
  rewrite?: string
  audio_rewrite?: string
  vision?: string
  product_view?: string
  image?: string
  image_base_url?: string
  image_options?: RuntimeModelOption[]
  image_size_options?: RuntimeSizeOption[]
  image_fallbacks?: string[]
  image_circuit?: {
    primary?: string
    fallbacks?: string[]
    failure_threshold?: number
    cooldown_seconds?: number
    primary_failures?: number
    primary_open?: boolean
    primary_open_until?: number
    primary_open_remaining_seconds?: number
  }
  subject_image?: string
  subject_image_fallbacks?: string[]
  voice_provider?: string
  voice_base_url?: string
  voice_tts?: string
  voice_id?: string
  voice_pool?: string[]
  voice_configured?: boolean
  voice_tts_paths?: string[]
  video?: string
  video_aliases?: Record<string, string>
  video_options?: RuntimeModelOption[]
  video_duration_options?: number[]
  video_max_duration_seconds?: number
  video_size_options?: RuntimeSizeOption[]
  video_provider?: string
  video_base_url?: string
  video_configured?: boolean
  video_create_paths?: string[]
}

export interface RuntimeHealth {
  ok: boolean
  llm_configured?: boolean
  auth_configured?: boolean
  base_url?: string
  models?: RuntimeModels
}

export async function getRuntimeHealth(): Promise<RuntimeHealth> {
  const res = await fetch(`${API_BASE}/health`, { cache: "no-store" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`health ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export interface CreativeCopyVariant {
  title: string
  hook_zh: string
  script_zh: string
  script_en: string
  image_prompt_en: string
  video_prompt_en: string
  caption_zh: string
  hashtags: string[]
}

export interface CreativeCopyResult {
  model: string
  variants: CreativeCopyVariant[]
}

export async function generateCreativeCopy(body: {
  goal: string
  product?: string
  audience?: string
  platform?: string
  tone?: string
  seconds?: number
  source_text?: string
}): Promise<CreativeCopyResult> {
  const res = await fetch(`${API_BASE}/creative/copy`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw apiError("generateCreativeCopy", res.status, txt)
  }
  return res.json()
}

export async function createCreativeImageJob(file?: File | null): Promise<Job> {
  const init: RequestInit = file
    ? (() => {
        const fd = new FormData()
        fd.append("file", file)
        return { method: "POST", body: fd }
      })()
    : {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: "{}",
      }
  const res = await fetch(`${API_BASE}/creative/jobs/image`, init)
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw apiError("createCreativeImageJob", res.status, txt)
  }
  return res.json()
}

// 把 ImageRef 解析成可显示的 src URL
export function resolveImageRefUrl(jobId: string, ref: ImageRef): string {
  if (ref.kind === "keyframe") {
    return effectiveFrameUrl(jobId, { index: ref.frame_idx, cleaned_applied: false })
  }
  if (ref.kind === "asset" && ref.element_id) {
    return `${API_BASE}/jobs/${jobId}/assets/${ref.element_id}.jpg`
  }
  if (ref.kind === "library_subject" || ref.kind === "library_product" || ref.kind === "library_scene") {
    return ""
  }
  if (ref.element_id && ref.cutout_id) {
    if (ref.cutout_id === ref.element_id) {
      // legacy v1
      return cutoutUrl(jobId, ref.frame_idx, ref.element_id)
    }
    return cutoutUrl(jobId, ref.frame_idx, ref.element_id, ref.cutout_id)
  }
  return ""
}

export async function uploadStoryboardAsset(jobId: string, file: File): Promise<ImageRef> {
  const fd = new FormData()
  fd.append("file", file)
  const res = await fetch(`${API_BASE}/jobs/${jobId}/assets`, { method: "POST", body: fd })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`uploadStoryboardAsset ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function generateProductAngleAsset(
  jobId: string,
  body: { source_ref: ImageRef; source_refs?: ImageRef[]; source_notes?: string[]; target_view: string; note?: string },
): Promise<ImageRef> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/assets/product-angle`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw apiError("generateProductAngleAsset", res.status, txt)
  }
  return res.json()
}

export interface StoryboardScriptRewriteSegment {
  index: number
  start: number
  end: number
  role: string
  source: string
  current_text: string
}

export async function rewriteStoryboardScript(
  jobId: string,
  body: {
    mode: "segment" | "all"
    author_intent?: string
    segments: StoryboardScriptRewriteSegment[]
  },
): Promise<{ items: Array<{ index: number; text: string; text_zh?: string }> }> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/script/rewrite`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`rewriteStoryboardScript ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export interface ProductViewAnalysisItem {
  index: number
  view: string
  background?: string
  use_tags?: string[]
  orientation?: {
    product_left?: string
    product_right?: string
    top?: string
    bottom?: string
    inner_side?: string
    outer_side?: string
    opening_direction?: string
  }
  landmarks?: string[]
  note: string
  risk?: string
  confidence: number
}

export async function analyzeProductViews(
  jobId: string,
  refs: ImageRef[],
): Promise<{ items: ProductViewAnalysisItem[]; missing_views: string[] }> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/assets/product-views/analyze`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refs }),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`analyzeProductViews ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function saveProductRefs(jobId: string, items: ProductRefStateItem[]): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/product-refs`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ items }),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`saveProductRefs ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function listProductLibrary(): Promise<ProductLibraryItem[]> {
  const res = await fetch(`${API_BASE}/product-library/skg`)
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`listProductLibrary ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function copyProductLibraryAsset(jobId: string, productId: string): Promise<ImageRef> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/assets/product-library`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ product_id: productId }),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`copyProductLibraryAsset ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function listCharacterLibrary(): Promise<CharacterLibraryItem[]> {
  const res = await fetch(`${API_BASE}/character-library/skg`)
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`listCharacterLibrary ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export function characterLibraryImageUrl(filename: string): string {
  return `${API_BASE}/character-library/skg/images/${filename}`
}

export async function listSubjectTemplates(): Promise<SubjectTemplateItem[]> {
  const res = await fetch(`${API_BASE}/subject-templates`)
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`listSubjectTemplates ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export function subjectTemplateImageUrl(filename: string): string {
  return `${API_BASE}/subject-templates/images/${filename}`
}

export async function saveSubjectTemplate(
  jobId: string,
  body: {
    name: string
    note?: string
    frame_idx: number
    element_id: string
    asset_ids: string[]
    subject_style?: "transparent_human" | "source_actor" | "cartoon_subject"
  },
): Promise<SubjectTemplateItem> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/subject-templates`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      name: body.name,
      note: body.note ?? "",
      frame_idx: body.frame_idx,
      element_id: body.element_id,
      asset_ids: body.asset_ids,
      subject_style: body.subject_style ?? "transparent_human",
    }),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`saveSubjectTemplate ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export function assetLibraryFileUrl(kind: AssetLibraryKind, itemId: string, filename: string): string {
  return `${API_BASE}/asset-library/${kind}/${itemId}/file/${filename}`
}

export async function listPromptLibrary(category?: PromptLibraryCategory, q = ""): Promise<PromptLibraryItem[]> {
  const qs = new URLSearchParams()
  if (category) qs.set("category", category)
  if (q.trim()) qs.set("q", q.trim())
  const res = await fetch(`${API_BASE}/prompt-library${qs.toString() ? `?${qs}` : ""}`, { cache: "no-store" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`listPromptLibrary ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function createPromptLibraryItem(body: {
  category: PromptLibraryCategory
  name: string
  tags?: string[]
  prompt_en: string
  prompt_zh?: string
  source_job_id?: string
}): Promise<PromptLibraryItem> {
  const res = await fetch(`${API_BASE}/prompt-library`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      category: body.category,
      name: body.name,
      tags: body.tags ?? [],
      prompt_en: body.prompt_en,
      prompt_zh: body.prompt_zh ?? "",
      source_job_id: body.source_job_id ?? "",
    }),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`createPromptLibraryItem ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function usePromptLibraryItem(id: string): Promise<PromptLibraryItem> {
  const res = await fetch(`${API_BASE}/prompt-library/${id}/use`, { method: "POST" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`usePromptLibraryItem ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function deletePromptLibraryItem(id: string): Promise<{ ok: boolean }> {
  const res = await fetch(`${API_BASE}/prompt-library/${id}`, { method: "DELETE" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`deletePromptLibraryItem ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function listAssetLibrary(kind: AssetLibraryKind, q = ""): Promise<AssetLibraryItem[]> {
  const qs = new URLSearchParams()
  if (q.trim()) qs.set("q", q.trim())
  const res = await fetch(`${API_BASE}/asset-library/${kind}${qs.toString() ? `?${qs}` : ""}`, { cache: "no-store" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`listAssetLibrary ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function createAssetLibraryItem(
  kind: AssetLibraryKind,
  metadata: Record<string, unknown>,
  files: File[],
): Promise<AssetLibraryItem> {
  const fd = new FormData()
  fd.append("metadata", JSON.stringify(metadata))
  for (const file of files) fd.append("files", file)
  const res = await fetch(`${API_BASE}/asset-library/${kind}`, { method: "POST", body: fd })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`createAssetLibraryItem ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function getAssetLibraryRefs(kind: AssetLibraryKind, id: string): Promise<{ count: number; jobs: string[] }> {
  const res = await fetch(`${API_BASE}/asset-library/${kind}/${id}/refs`, { cache: "no-store" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`getAssetLibraryRefs ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function deleteAssetLibraryItem(kind: AssetLibraryKind, id: string, force = false): Promise<{ ok: boolean }> {
  const res = await fetch(`${API_BASE}/asset-library/${kind}/${id}${force ? "?force=true" : ""}`, { method: "DELETE" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`deleteAssetLibraryItem ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function copyAssetLibraryToJob(kind: AssetLibraryKind, id: string, jobId: string): Promise<ImageRef | { kind: "video"; video_id: string; url: string; label?: string }> {
  const res = await fetch(`${API_BASE}/asset-library/${kind}/${id}/copy-to-job/${jobId}`, { method: "POST" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`copyAssetLibraryToJob ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function getResourceLibraryRecent(hours = 24): Promise<{ items: ResourceLibraryRecentItem[] }> {
  const res = await fetch(`${API_BASE}/resource-library/recent?hours=${encodeURIComponent(String(hours))}`, { cache: "no-store" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`getResourceLibraryRecent ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function copyCharacterLibraryAssets(jobId: string, characterId: string): Promise<{ character_id: string; character_name: string; images: ImageRef[] }> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/assets/character-library`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ character_id: characterId }),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`copyCharacterLibraryAssets ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function createProductFusionGuide(
  jobId: string,
  body: ProductFusionShot,
): Promise<ImageRef> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/product-fusion/guide`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`createProductFusionGuide ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function generateProductFusionDescriptions(
  jobId: string,
  shots: ProductFusionShot[],
): Promise<{ descriptions: string[]; mode: "llm" | "fallback" }> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/product-fusion/descriptions`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ shots }),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`generateProductFusionDescriptions ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export interface KeyFrame {
  index: number
  timestamp: number
  url: string
  description?: FrameDescription | null
  transparent_human_score?: TransparentHumanFrameScore | null
  cleaned_url?: string | null
  cleaned_applied?: boolean
  quality_report?: QualityReport | null
  scene_assets?: SceneAsset[]
  elements?: KeyElement[]
  storyboard?: StoryboardScene | null
  generated_images?: GeneratedImage[]
}

export type FrameExtractTarget = "transparent_human" | "balanced" | "subject" | "transition" | "expression" | "motion"
export type FrameExtractMode = "replace" | "append"
export type FrameExtractQuality = "auto" | "fast" | "accurate" | "ultra"
export type AssetBackground = "white" | "black"
export type AssetSize = "source" | "1024" | "1536" | "2048"
export type SubjectKind = "object" | "living"
export type SubjectView = string
export type SubjectAssetStatus = "queued" | "in_progress" | "completed" | "failed"
export type SubjectImageModelPreference = "auto" | "gpt-image-2" | "gemini-3-pro-image-preview"
export type SubjectModelBundle = "gpt" | "gemini"
export type SceneMode = "remove_subject" | "similar" | "style"
export type SceneStyle = "source" | "premium_product" | "clean_studio" | "warm_lifestyle" | "cinematic"
export type SceneAssetRole = "scene" | "first_frame" | "last_frame"

export interface QualityReport {
  width: number
  height: number
  short_side: number
  sharpness: number
  risk: "ok" | "warn" | "bad"
  warnings: string[]
}

export interface TransparentHumanFrameScore {
  transparent_body_score: number
  skeleton_visible_score: number
  human_prominence_score: number
  clarity_score: number
  commercial_style_score: number
  product_usefulness_score: number
  total_score?: number
  qualified?: boolean
  reject_reason?: string
  notes?: string
}

export interface SceneAsset {
  id: string
  label: string
  url: string
  width: number
  height: number
  quality: "hd"
  size: AssetSize
  scene_mode?: SceneMode
  scene_style?: SceneStyle
  asset_role?: SceneAssetRole
  quality_report?: QualityReport | null
  created_at: number
}

export interface SubjectAsset {
  id: string
  view: SubjectView
  label: string
  url: string
  width: number
  height: number
  background: AssetBackground
  quality: "hd"
  size: AssetSize
  source_frame_indices?: number[]
  ai_completed?: boolean
  status?: SubjectAssetStatus
  progress?: number
  error?: string
  pack_id?: string
  pack_label?: string
  pack_mode?: string
  pack_created_at?: number
  created_at: number
}

export interface SubjectProfilePreference {
  mode?: "random" | "manual"
  gender?: string
  age?: string
  wardrobe?: string
  region_ethnicity?: string
  skin_tone?: string
  body?: string
  hair?: string
  mood?: string
  resolved_summary?: string
  prompt_summary?: string
}

export interface SubjectAgentAnalysis {
  model_bundle: SubjectModelBundle
  model: string
  source_frame_indices: number[]
  summary_zh: string
  summary_en: string
  generation_brief_en: string
  trait_chips: string[]
  mode_options: string[]
  questions: string[]
  warnings: string[]
  created_at: number
}

export interface SubjectAgentMessage {
  role: "user" | "assistant"
  content: string
  created_at: number
}

export interface SubjectAgentState {
  model_bundle: SubjectModelBundle
  source_frame_indices: number[]
  analysis?: SubjectAgentAnalysis | null
  messages: SubjectAgentMessage[]
  selected_mode: "realistic" | "cartoon" | "elements" | "custom"
  selected_traits: string[]
  requirements_zh: string
  generation_prompt_en: string
  quantity: number
  updated_at: number
}

export interface ProductLibraryItem {
  id: string
  handle: string
  title: string
  product_type: string
  image_type: string
  image_index: number
  filename: string
  url: string
  width: number
  height: number
  source_path: string
  white_score: number
  near_white_score: number
  has_people: boolean
  tags: string[]
}

export interface CharacterLibraryImage {
  id: string
  view: string
  label: string
  filename: string
  url: string
  width: number
  height: number
  source_path: string
}

export interface CharacterLibraryItem {
  id: string
  name: string
  folder: string
  description: string
  prompt_brief?: string
  prompt_brief_zh?: string
  primary_image: string
  images: CharacterLibraryImage[]
}

export interface SubjectTemplateImage {
  id: string
  view: string
  label: string
  filename: string
  url: string
  width: number
  height: number
  background: "white" | "black"
  quality: "hd"
  size: "source" | "1024" | "1536" | "2048"
  source_asset_id: string
  source_frame_indices: number[]
  created_at: number
}

export interface SubjectTemplateItem {
  id: string
  name: string
  description: string
  note: string
  prompt_brief?: string
  prompt_brief_zh?: string
  source: "database"
  source_job_id: string
  source_frame_idx: number
  source_element_id: string
  subject_style: "transparent_human" | "source_actor" | "cartoon_subject"
  primary_image: string
  images: SubjectTemplateImage[]
  created_at: number
  updated_at: number
}

export interface TranscriptSegment {
  index: number
  start: number
  end: number
  en: string
  zh: string
}

export interface AudioScript {
  status: "idle" | "rewriting" | "completed" | "failed"
  source_text: string
  source_zh: string
  rewritten_text: string
  rewritten_text_zh?: string
  speaker_profile: string
  rhythm_profile: string
  background_audio_profile: string
  product_brief: string
  rewrite_model: string
  voice_provider: string
  voice_model: string
  voice_id: string
  voice_url: string
  error: string
  created_at: number
}

export interface StoryboardImage {
  ref_id: string
  kind: "keyframe" | "cutout" | "asset"
  frame_idx: number
  element_id?: string | null
  cutout_id?: string | null
  label?: string
  created_at?: number
}

export interface ProductRefStateItem {
  id: string
  ref: ImageRef
  view: string
  background: string
  useTags: string[]
  orientation?: ProductViewAnalysisItem["orientation"]
  landmarks: string[]
  note: string
  risk: string
  source: "upload" | "ai" | "library"
  assetMeta?: ImageRef["asset_meta"]
  confidence?: number
}

export interface Job {
  id: string
  url: string
  owner_id?: string
  owner_name?: string
  owner_email?: string
  owner_provider?: string
  tenant_key?: string
  status: JobStatus
  progress: number
  message?: string
  video_url?: string
  duration?: number
  width?: number
  height?: number
  source_audio_url?: string
  frames: KeyFrame[]
  transcript: TranscriptSegment[]
  audio_script?: AudioScript
  storyboard_images?: StoryboardImage[]
  generated_videos?: GeneratedVideo[]
  product_refs?: ProductRefStateItem[]
  subject_agent?: SubjectAgentState
  error?: string
}

export interface BackendHealth {
  ok: boolean
  llm_configured: boolean
  auth_configured?: boolean
  auth_modes?: {
    password?: boolean
    feishu?: boolean
    data_isolation?: boolean
  }
  base_url: string
  models?: {
    asr?: string
    translate?: string
    rewrite?: string
    audio_rewrite?: string
    voice_tts_paths?: string[]
    video?: string
    video_aliases?: Record<string, string>
    video_base_url?: string
    video_configured?: boolean
  }
}

export function apiAssetUrl(path?: string | null): string {
  if (!path) return ""
  if (/^https?:\/\//i.test(path)) return path
  return `${API_BASE}${path.startsWith("/") ? "" : "/"}${path}`
}

export function isRestrictedDownloadError(error?: string | null): boolean {
  const text = (error ?? "").toLowerCase()
  return (
    text.includes("tiktok 下载需要登录态") ||
    text.includes("log in for access") ||
    text.includes("cookies-from-browser") ||
    text.includes("ytdlp_cookies_file") ||
    (text.includes("tiktok") && text.includes("cookies"))
  )
}

export function formatJobError(error?: string | null): string {
  if (!error) return ""
  if (isRestrictedDownloadError(error)) {
    return "这个 TikTok 视频需要登录态。请上传 MP4，或让后端配置 YTDLP_COOKIES_FROM_BROWSER / YTDLP_COOKIES_FILE 后重试。"
  }
  return error
}

export async function getHealth(): Promise<BackendHealth> {
  const res = await fetch(`${API_BASE}/health`)
  if (!res.ok) throw new Error(`health ${res.status}`)
  return res.json()
}

export async function createJob(tkUrl: string): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ url: tkUrl }),
  })
  if (!res.ok) throw new Error(`createJob ${res.status}`)
  return res.json()
}

export async function retryJobDownload(id: string): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${id}/download/retry`, { method: "POST" })
  if (!res.ok) {
    const text = await res.text().catch(() => "")
    throw apiError("retryJobDownload", res.status, text)
  }
  return res.json()
}

export async function uploadJob(file: File): Promise<Job> {
  const fd = new FormData()
  fd.append("file", file)
  const res = await fetch(`${API_BASE}/jobs/upload`, {
    method: "POST",
    body: fd,
  })
  if (!res.ok) {
    const text = await res.text().catch(() => "")
    throw new Error(`uploadJob ${res.status} ${text.slice(0, 200)}`)
  }
  return res.json()
}

export async function getJob(id: string): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${id}`)
  if (!res.ok) throw new Error(`getJob ${res.status}`)
  return res.json()
}

export async function deleteJob(id: string): Promise<{ ok: boolean; id: string }> {
  const res = await fetch(`${API_BASE}/jobs/${id}`, { method: "DELETE" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`deleteJob ${res.status} ${txt.slice(0, 200)}`)
  }
  return res.json()
}

export interface JobSummary {
  id: string
  url: string
  owner_name?: string
  owner_email?: string
  owner_provider?: string
  status: JobStatus
  progress: number
  message: string
  duration: number
  width: number
  height: number
  video_url: string
  frame_count: number
  video_count: number
  thumbnail: string
  error: string
  mtime: number
}

export async function listJobs(limit?: number): Promise<JobSummary[]> {
  const qs = limit && limit > 0 ? `?limit=${limit}` : ""
  const res = await fetch(`${API_BASE}/jobs${qs}`)
  if (!res.ok) throw new Error(`listJobs ${res.status}`)
  return res.json()
}

export async function triggerTranscribe(id: string): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${id}/transcribe`, { method: "POST" })
  if (!res.ok) throw new Error(`transcribe ${res.status}`)
  return res.json()
}

export async function analyzeJob(
  id: string,
  frames = 12,
  target: FrameExtractTarget = "balanced",
  mode: FrameExtractMode = "replace",
  quality: FrameExtractQuality = "auto",
): Promise<Job> {
  const qs = new URLSearchParams({ frames: String(frames), target, mode, quality })
  const res = await fetch(`${API_BASE}/jobs/${id}/analyze?${qs.toString()}`, { method: "POST" })
  if (!res.ok) {
    const t = await res.text().catch(() => "")
    throw new Error(`analyze ${res.status} ${t.slice(0, 200)}`)
  }
  return res.json()
}

export async function addManualFrame(id: string, t: number): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${id}/frames?t=${encodeURIComponent(t.toFixed(2))}`, { method: "POST" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`addFrame ${res.status} ${txt.slice(0, 200)}`)
  }
  return res.json()
}

export async function uploadReferenceFrame(jobId: string, file: File): Promise<Job> {
  const fd = new FormData()
  fd.append("file", file)
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/upload`, { method: "POST", body: fd })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`uploadReferenceFrame ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function describeFrame(jobId: string, frameIdx: number): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/describe`, { method: "POST" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`describe ${res.status} ${txt.slice(0, 200)}`)
  }
  return res.json()
}

export async function translateText(text: string, target: "en" | "zh" = "en"): Promise<string> {
  const res = await fetch(`${API_BASE}/translate`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text, target }),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`translate ${res.status} ${txt.slice(0, 200)}`)
  }
  const j = await res.json()
  return (j.text || "").toString()
}

export async function generateImage(
  jobId: string,
  frameIdx: number,
  body: { prompt: string; extra_prompt?: string; negative_prompt?: string; model?: string; size?: string; mode?: "edit" | "text"; from_selected?: boolean },
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/generate`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`generate ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function selectGenerated(
  jobId: string,
  frameIdx: number,
  genId: string,
  selected: boolean,
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/gen/${genId}/select`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ selected }),
  })
  if (!res.ok) throw new Error(`select ${res.status}`)
  return res.json()
}

export function generatedImageUrl(jobId: string, frameIdx: number, genId: string): string {
  return `${API_BASE}/jobs/${jobId}/frames/${frameIdx}/gen/${genId}.jpg`
}

export function frameUrl(jobId: string, frameIndex: number): string {
  return `${API_BASE}/jobs/${jobId}/frames/${frameIndex}.jpg`
}

// 接 frame 对象时返回正确版本 URL（已应用清洗版时加 cache-bust）
export function effectiveFrameUrl(jobId: string, frame: { index: number; cleaned_applied?: boolean | null }): string {
  const base = `${API_BASE}/jobs/${jobId}/frames/${frame.index}.jpg`
  return frame.cleaned_applied ? `${base}?v=applied` : base
}

export function videoUrl(jobId: string): string {
  return `${API_BASE}/jobs/${jobId}/video.mp4`
}

export function sourceAudioUrl(jobId: string): string {
  return `${API_BASE}/jobs/${jobId}/audio.wav`
}

export function cleanedFrameUrl(jobId: string, frameIndex: number, bust?: string | number): string {
  const u = `${API_BASE}/jobs/${jobId}/frames/${frameIndex}/cleaned.jpg`
  return bust ? `${u}?t=${bust}` : u
}

export function cutoutUrl(jobId: string, frameIndex: number, elementId: string, cutoutId?: string): string {
  if (cutoutId) {
    return `${API_BASE}/jobs/${jobId}/frames/${frameIndex}/elements/${elementId}/cutouts/${cutoutId}.jpg`
  }
  return `${API_BASE}/jobs/${jobId}/frames/${frameIndex}/elements/${elementId}/cutout.jpg`
}

export function jobAssetUrl(jobId: string, assetId: string): string {
  return `${API_BASE}/jobs/${jobId}/assets/${assetId}.jpg`
}

// 兼容 v1 (cutout_id) / v2 (cutouts 数组) — 返回"有没有提取图"
export function hasCutout(e: KeyElement): boolean {
  return (Array.isArray(e.cutouts) && e.cutouts.length > 0) || !!e.cutout_id
}

// 返回代表性 cutout 的 URL（v2 取最新一张，v1 用旧路径）
export function representativeCutoutUrl(
  jobId: string,
  frameIndex: number,
  e: KeyElement,
): string | null {
  if (Array.isArray(e.cutouts) && e.cutouts.length > 0) {
    return cutoutUrl(jobId, frameIndex, e.id, e.cutouts[e.cutouts.length - 1])
  }
  if (e.cutout_id) return cutoutUrl(jobId, frameIndex, e.id)
  return null
}

export async function pushStoryboardImage(
  jobId: string,
  body: { kind: "keyframe" | "cutout" | "asset"; frame_idx: number; element_id?: string | null; cutout_id?: string | null; label?: string },
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/storyboard-images`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`pushStoryboardImage ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function removeStoryboardImage(jobId: string, refId: string): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/storyboard-images/${refId}`, { method: "DELETE" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`removeStoryboardImage ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function updateStoryboard(
  jobId: string,
  frameIdx: number,
  body: StoryboardScene,
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/storyboard`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`updateStoryboard ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function quickPlanStoryboard(
  jobId: string,
  frameIdx: number,
  body: QuickStoryboardPlanInput,
): Promise<StoryboardScene> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/storyboard/quick-plan`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`quickPlanStoryboard ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function refineStoryboard(
  jobId: string,
  frameIdx: number,
  body: { current_plan: QuickStoryboardPlanInput; user_feedback: string },
): Promise<RefineStoryboardResult> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/storyboard/refine`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`refineStoryboard ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function batchGenerateAll(
  jobId: string,
  body: { count_per_row?: number; concurrency?: number; model?: string; size?: string },
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/storyboard/batch-generate-all`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`batchGenerateAll ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function generateStoryboardVideo(
  jobId: string,
  frameIdx: number,
  body: {
    prompt: string
    duration?: number
    count?: number
    seed?: number | null
    storyboard_row_idx?: number | null
    first_image?: ImageRef | null
    last_image?: ImageRef | null
    product_images?: ImageRef[]
    subject_images?: ImageRef[]
    subject_image?: ImageRef | null
    scene_image?: ImageRef | null
    product_image?: ImageRef | null
    action_image?: ImageRef | null
    source_ref?: { kind: "image" | "source_video"; url: string } | null
    model?: string
    size?: string
  },
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/storyboard/video`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    let detail = txt
    try {
      const parsed = JSON.parse(txt)
      detail = parsed?.detail || txt
    } catch {}
    throw new Error(detail || `generateStoryboardVideo ${res.status}`)
  }
  return res.json()
}

export async function deleteGeneratedVideo(jobId: string, videoId: string): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/storyboard-videos/${videoId}`, { method: "DELETE" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`deleteGeneratedVideo ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function deleteCutout(jobId: string, frameIdx: number, elementId: string, cutoutId: string): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements/${elementId}/cutouts/${cutoutId}`, {
    method: "DELETE",
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`deleteCutout ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function cleanupFrame(
  jobId: string,
  frameIdx: number,
  regions?: Array<{ x: number; y: number; w: number; h: number }> | null,
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/cleanup`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ regions: regions ?? null }),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`cleanup ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function applyCleanedFrame(jobId: string, frameIdx: number): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/cleanup/apply`, { method: "POST" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`applyCleaned ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function discardCleanedFrame(jobId: string, frameIdx: number): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/cleanup`, { method: "DELETE" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`discardCleaned ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function addElement(
  jobId: string,
  frameIdx: number,
  body: {
    name_zh: string
    name_en?: string
    position?: string
    source?: "auto" | "manual" | "region"
    region?: { x: number; y: number; w: number; h: number } | null
  },
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ source: "manual", ...body }),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`addElement ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function deleteElement(jobId: string, frameIdx: number, elementId: string): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements/${elementId}`, { method: "DELETE" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`deleteElement ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function updateElement(
  jobId: string,
  frameIdx: number,
  elementId: string,
  body: { name_zh?: string; name_en?: string; position?: string; subject_consensus_brief?: string },
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements/${elementId}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`updateElement ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function deleteFrame(jobId: string, frameIdx: number): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}`, { method: "DELETE" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`deleteFrame ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function deleteGeneratedImage(jobId: string, frameIdx: number, genId: string): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/gen/${genId}`, { method: "DELETE" })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`deleteGen ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function cutoutElement(jobId: string, frameIdx: number, elementId: string): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements/${elementId}/cutout`, {
    method: "POST",
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`cutout ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function generateSceneAsset(
  jobId: string,
  frameIdx: number,
  body: {
    size?: AssetSize
    scene_mode?: SceneMode
    scene_style?: SceneStyle
    asset_role?: SceneAssetRole
    prompt?: string
    subject_brief?: string
    source_frame_indices?: number[]
    subject_images?: ImageRef[]
    product_images?: ImageRef[]
  } = {},
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/scene-asset`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      quality: "hd",
      size: body.size ?? "source",
      scene_mode: body.scene_mode ?? "remove_subject",
      scene_style: body.scene_style ?? "source",
      asset_role: body.asset_role ?? "scene",
      prompt: body.prompt ?? "",
      subject_brief: body.subject_brief ?? "",
      source_frame_indices: body.source_frame_indices ?? null,
      subject_images: body.subject_images ?? [],
      product_images: body.product_images ?? [],
    }),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`sceneAsset ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}

export async function generateSubjectAssets(
  jobId: string,
  frameIdx: number,
  elementId: string,
  body: {
    subject_kind?: SubjectKind
    background?: AssetBackground
    size?: AssetSize
    source_frame_indices?: number[]
    views?: string[]
    character_id?: string
    subject_template_id?: string
    subject_style?: "transparent_human" | "source_actor" | "cartoon_subject"
    reconstruction_mode?: "same" | "similar"
    subject_profile?: SubjectProfilePreference | null
    prompt?: string
    image_model_preference?: SubjectImageModelPreference
    replace_views?: boolean
    pack_id?: string
    pack_label?: string
    pack_mode?: string
    pack_created_at?: number
  } = {},
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements/${elementId}/subject-assets`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      quality: "hd",
      subject_kind: body.subject_kind ?? "object",
      background: body.background ?? "white",
      size: body.size ?? "source",
      source_frame_indices: body.source_frame_indices ?? null,
      views: body.views ?? null,
      character_id: body.character_id ?? "",
      subject_template_id: body.subject_template_id ?? "",
      subject_style: body.subject_style ?? "transparent_human",
      reconstruction_mode: body.reconstruction_mode ?? "same",
      subject_profile: body.subject_profile ?? null,
      prompt: body.prompt ?? "",
      image_model_preference: body.image_model_preference ?? "auto",
      replace_views: body.replace_views ?? false,
      pack_id: body.pack_id ?? "",
      pack_label: body.pack_label ?? "",
      pack_mode: body.pack_mode ?? "",
      pack_created_at: body.pack_created_at ?? 0,
    }),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw apiError("subjectAssets", res.status, txt)
  }
  return res.json()
}

export async function analyzeSubjectAgent(
  jobId: string,
  body: {
    model_bundle: SubjectModelBundle
    source_frame_indices: number[]
  },
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/subject-agent/analyze`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw apiError("subjectAgentAnalyze", res.status, txt)
  }
  return res.json()
}

export async function sendSubjectAgentMessage(
  jobId: string,
  body: {
    model_bundle: SubjectModelBundle
    source_frame_indices: number[]
    selected_mode: SubjectAgentState["selected_mode"]
    selected_traits: string[]
    requirements_zh: string
    message: string
    quantity: number
  },
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/subject-agent/message`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw apiError("subjectAgentMessage", res.status, txt)
  }
  return res.json()
}

export async function deleteSubjectAsset(
  jobId: string,
  frameIdx: number,
  elementId: string,
  assetId: string,
): Promise<Job> {
  const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/elements/${elementId}/subject-assets/${assetId}`, {
    method: "DELETE",
  })
  if (!res.ok) {
    const txt = await res.text().catch(() => "")
    throw new Error(`deleteSubjectAsset ${res.status} ${txt.slice(0, 300)}`)
  }
  return res.json()
}
