import { NextRequest, NextResponse } from 'next/server';
import { VideoGenerationClient, Config, HeaderUtils } from 'coze-coding-dev-sdk';
import { buildCustomApiHeaders, fetchWithRetry, parseCustomApiError, parseCustomApiJsonWithProgress } from '@/lib/custom-api-fetch';
import { getAspectRatioPromptHint } from '@/lib/model-config';
import { localStorage } from '@/lib/local-storage';
import { isTrustedInternalGenerationRequest, isUuid, resolveServerApiConfig } from '@/lib/server-api-config';
import { updateGenerationJobProgress } from '@/lib/generation-job-estimates';
import {
  compressImageBufferForUpstream,
  dataUrlToImageBuffer,
  imageBufferToDataUrl,
} from '@/lib/server-image-compression';

interface CustomApiConfig {
  apiUrl: string;
  modelName: string;
  apiKey: string;
  provider: string;
  customApiKeyId?: string;
  systemApiId?: string;
}

const GENERATION_TIMEOUT = 180_000;
const MAX_UPSTREAM_REFERENCE_IMAGE_BYTES = Number(process.env.MAX_UPSTREAM_REFERENCE_IMAGE_BYTES || 1536 * 1024);

export const runtime = 'nodejs';

/**
 * Upload a media data URL to S3 storage and return a presigned URL.
 * Includes a 45s timeout to prevent blocking the response.
 */
async function persistMediaToStorage(dataUrl: string, prefix: string): Promise<string> {
  if (!dataUrl.startsWith('data:')) return dataUrl;

  try {
    const match = dataUrl.match(/^data:((?:image|video)\/[^;]+);base64,(.+)$/);
    if (!match) return dataUrl;
    const [, mimeType, base64Data] = match;
    const ext = mimeType.split('/')[1] || 'mp4';
    const buffer = Buffer.from(base64Data, 'base64');
    const fileName = `${prefix}/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;

    const fileKey = await withTimeout(
      localStorage.uploadFile({ fileContent: buffer, fileName, contentType: mimeType }),
      45_000,
      'Local uploadFile (video)',
    );

    if (!fileKey) {
      console.error('[Persist Video Media] uploadFile returned empty key');
      return dataUrl;
    }

    const presignedUrl = await withTimeout(
      localStorage.generatePresignedUrl({ key: fileKey, expireTime: 2592000 }),
      10_000,
      'Local generatePresignedUrl (video)',
    );

    if (presignedUrl) {
      console.log('[Persist Video Media] Success, key:', fileKey, 'size:', buffer.length, 'bytes');
      return presignedUrl;
    }

    return dataUrl;
  } catch (err) {
    console.error('[Persist Video Media Error]', err instanceof Error ? err.message : err);
    return dataUrl;
  }
}

async function persistRemoteUrlToStorage(url: string, prefix: string): Promise<string> {
  if (!url.startsWith('http')) return url;

  try {
    const fileKey = await withTimeout(
      localStorage.uploadFromUrl({ url, timeout: 60000 }),
      60_000,
      'Local uploadFromUrl (video)',
    );
    if (!fileKey) return url;

    const presignedUrl = await withTimeout(
      localStorage.generatePresignedUrl({ key: fileKey, expireTime: 2592000 }),
      10_000,
      'Local generatePresignedUrl (video remote)',
    );

    if (presignedUrl) {
      console.log('[Persist Remote Video URL] Success, key:', fileKey);
      return presignedUrl;
    }
    return url;
  } catch (err) {
    console.warn('[Persist Remote Video URL] Failed, using original URL:', err instanceof Error ? err.message : err);
    return url;
  }
}

/** Helper: wrap a promise with a timeout that rejects with a descriptive message */
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
    promise.then(
      (v) => { clearTimeout(timer); resolve(v); },
      (e) => { clearTimeout(timer); reject(e); },
    );
  });
}

async function persistAllMediaUrls(urls: string[], prefix: string): Promise<string[]> {
  const MAX_DATA_URL_SIZE = 10 * 1024 * 1024; // 10MB limit for video data URLs
  const results = await Promise.all(
    urls.map(async (url) => {
      try {
        if (url.startsWith('data:')) {
          const result = await persistMediaToStorage(url, prefix);
          if (result.startsWith('data:') && result.length > MAX_DATA_URL_SIZE) {
            console.warn('[Persist Video] Data URL too large (' + Math.round(result.length / 1024 / 1024) + 'MB), skipping');
            return null;
          }
          return result;
        }
        if (url.startsWith('http')) return persistRemoteUrlToStorage(url, prefix);
        return url;
      } catch (err) {
        console.error('[persistAllMediaUrls video] Error:', err instanceof Error ? err.message : err);
        if (url.startsWith('data:') && url.length > MAX_DATA_URL_SIZE) return null;
        return url;
      }
    }),
  );
  return results.filter((u): u is string => u !== null);
}

async function uploadDataUrlAndGetPublicUrl(dataUrl: string): Promise<string | null> {
  try {
    const match = dataUrl.match(/^data:(image\/[^;]+);base64,(.+)$/);
    if (!match) return null;
    const [, mimeType, base64Data] = match;
    const ext = mimeType.split('/')[1] || 'png';
    const buffer = Buffer.from(base64Data, 'base64');
    const fileName = `img2vid-ref/${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;

    const fileKey = await localStorage.uploadFile({ fileContent: buffer, fileName, contentType: mimeType });
    if (!fileKey) return null;

    const presignedUrl = await localStorage.generatePresignedUrl({ key: fileKey, expireTime: 3600 });
    console.log('[Upload Ref Video Image] Success, key:', fileKey);
    return presignedUrl || null;
  } catch (err) {
    console.error('[Upload Ref Video Image Error]', err instanceof Error ? err.message : err);
    return null;
  }
}

async function toPublicImageUrl(image: string): Promise<string> {
  if (!image.startsWith('data:')) return image;
  const uploadedUrl = await uploadDataUrlAndGetPublicUrl(image);
  return uploadedUrl || image;
}

async function normalizeReferenceImageForUpstream(image: string): Promise<string> {
  const parsedImage = dataUrlToImageBuffer(image);
  if (!parsedImage) return image;

  try {
    const compressed = await compressImageBufferForUpstream(parsedImage, {
      maxBytes: MAX_UPSTREAM_REFERENCE_IMAGE_BYTES,
    });
    if (compressed.changed) {
      console.log('[Custom API img2vid] Compressed reference image:', compressed.originalBytes, '→', compressed.buffer.length);
    }
    return imageBufferToDataUrl({ buffer: compressed.buffer, mimeType: compressed.mimeType });
  } catch (err) {
    console.warn('[Custom API img2vid] Reference image compression failed, using original:', err instanceof Error ? err.message : err);
    return image;
  }
}

function normalizeReferenceImages(image?: string, images?: unknown, extraImages?: unknown): string[] {
  const refs: string[] = [];
  if (image) refs.push(image);
  if (Array.isArray(images)) {
    for (const item of images) {
      if (typeof item === 'string' && item.trim()) refs.push(item);
    }
  }
  if (Array.isArray(extraImages)) {
    for (const item of extraImages) {
      if (typeof item === 'string' && item.trim()) refs.push(item);
    }
  }
  return Array.from(new Set(refs));
}

function uniqueStrings(values: string[]): string[] {
  return Array.from(new Set(values));
}

function deriveChatCompletionsUrl(originalUrl: string): string {
  if (originalUrl.includes('/chat/completions')) return originalUrl;
  return originalUrl
    .replace(/\/(videos|images)\/(generations|edits).*/i, '/chat/completions')
    .replace(/\/+$/, '');
}

function deriveImagesEditsUrl(originalUrl: string): string {
  if (originalUrl.includes('/images/edits')) return originalUrl;
  return originalUrl
    .replace(/\/(videos|images)\/generations.*/i, '/images/edits')
    .replace(/\/+$/, '');
}

function extractVideosFromChatResponse(data: Record<string, unknown>): string[] {
  const videos: string[] = [];
  const choices = data.choices as Array<Record<string, unknown>> | undefined;
  if (Array.isArray(choices)) {
    for (const choice of choices) {
      const message = choice.message as Record<string, unknown> | undefined;
      if (!message) continue;
      const content = message.content;
      if (typeof content === 'string') {
        if (content.startsWith('http') || content.startsWith('data:video/')) videos.push(content);
        const urlMatch = content.match(/(https?:\/\/[^\s"']+\.(mp4|mov|webm)[^\s"']*)/i);
        if (urlMatch) videos.push(urlMatch[1]);
      } else if (Array.isArray(content)) {
        for (const item of content as Array<Record<string, unknown>>) {
          if (item.type === 'video_url' && item.video_url) {
            const url = (item.video_url as Record<string, unknown>).url;
            if (typeof url === 'string') videos.push(url);
          }
          if (item.type === 'text' && typeof item.text === 'string') {
            const text = item.text as string;
            if (text.startsWith('http') || text.startsWith('data:video/')) videos.push(text);
            const urlMatch = text.match(/(https?:\/\/[^\s"']+\.(mp4|mov|webm)[^\s"']*)/i);
            if (urlMatch) videos.push(urlMatch[1]);
          }
        }
      }
    }
  }
  return videos;
}

function extractVideosFromGenerationsResponse(data: Record<string, unknown>): string[] {
  const videos: string[] = [];
  if (Array.isArray(data.data)) {
    for (const item of data.data as Array<Record<string, unknown>>) {
      if (typeof item === 'string') { videos.push(item); continue; }
      if (item.url && typeof item.url === 'string') videos.push(item.url);
      if (item.video_url && typeof item.video_url === 'string') videos.push(item.video_url);
      if (item.b64_json && typeof item.b64_json === 'string') {
        videos.push(`data:video/mp4;base64,${item.b64_json}`);
      }
    }
  } else if (typeof data.url === 'string') {
    videos.push(data.url);
  } else if (typeof data.video_url === 'string') {
    videos.push(data.video_url);
  }
  return videos;
}

async function customApiImageToVideo(
  customApiConfig: CustomApiConfig,
  prompt: string | undefined,
  negativePrompt: string | undefined,
  image: string,
  referenceImages: string[] = [],
  aspectRatio?: string,
  duration?: number,
  fps?: number,
  onProgress?: (progress: Record<string, unknown>) => void | Promise<void>,
): Promise<NextResponse> {
  const endpoint = customApiConfig.apiUrl;
  if (!endpoint) {
    return NextResponse.json({ error: '自定义API未配置请求地址' }, { status: 400 });
  }
  if (!customApiConfig.modelName) {
    return NextResponse.json({ error: '自定义API未配置模型名称' }, { status: 400 });
  }

  const normalizedImage = await normalizeReferenceImageForUpstream(image);
  const normalizedReferenceImages = uniqueStrings(await Promise.all(
    normalizeReferenceImages(normalizedImage, referenceImages).map(normalizeReferenceImageForUpstream),
  ));

  // Prepare image buffer for FormData upload
  let imageBuffer: Buffer | null = null;
  let imageMimeType = 'image/png';
  if (normalizedImage.startsWith('data:')) {
    const parsedImage = dataUrlToImageBuffer(normalizedImage);
    if (parsedImage) {
      imageMimeType = parsedImage.mimeType;
      imageBuffer = parsedImage.buffer;
    }
  }

  // Upload reference image to S3
  const imageUrl = await toPublicImageUrl(normalizedImage);
  const imageUrls = await Promise.all(normalizedReferenceImages.map(toPublicImageUrl));

  let promptText = prompt || '根据参考图生成视频';
  if (negativePrompt) promptText += `\n\n负面提示词: ${negativePrompt}`;
  // Augment prompt with aspect ratio hint
  if (aspectRatio) {
    const hint = getAspectRatioPromptHint(aspectRatio);
    if (hint) promptText += `\n\n[${hint}]`;
  }

  const headers = buildCustomApiHeaders(customApiConfig.apiKey);

  // Get raw base64 for strategies that need it
  let rawBase64 = normalizedImage;
  if (normalizedImage.startsWith('data:')) {
    const commaIndex = normalizedImage.indexOf(',');
    if (commaIndex !== -1) rawBase64 = normalizedImage.substring(commaIndex + 1);
  }

  const strategyResults: string[] = [];
  let firstUpstreamError: { error: string; status: number } | null = null;

  // --- Strategy 1: images/edits with multipart/form-data ---
  // Same as img2img - Cherry Studio uses multipart/form-data for image-based requests
  if (imageBuffer) {
    const editsUrl = deriveImagesEditsUrl(endpoint);
    console.log('[Custom API img2vid → 策略1: images/edits (FormData)] URL:', editsUrl, '| model:', customApiConfig.modelName);
    try {
      const boundary = `----FormBoundary${Date.now()}${Math.random().toString(36).slice(2)}`;
      const parts: Buffer[] = [];

      const textFields: Record<string, string> = {
        model: customApiConfig.modelName,
        prompt: promptText,
      };
      if (aspectRatio) textFields.aspect_ratio = aspectRatio;
      if (duration) textFields.duration = String(duration);
      if (fps) textFields.fps = String(fps);

      for (const [key, value] of Object.entries(textFields)) {
        parts.push(Buffer.from(
          `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`
        ));
      }

      const ext = imageMimeType.split('/')[1] || 'png';
      const imageBuffers: Array<{ mimeType: string; buffer: Buffer }> = [];
      for (const ref of normalizedReferenceImages) {
        if (!ref.startsWith('data:')) continue;
        const parsedImage = dataUrlToImageBuffer(ref);
        if (!parsedImage) continue;
        imageBuffers.push(parsedImage);
      }

      imageBuffers.forEach((item, index) => {
        const fieldName = index === 0 ? 'image' : 'images[]';
        const itemExt = item.mimeType.split('/')[1] || ext;
        parts.push(Buffer.from(
          `--${boundary}\r\nContent-Disposition: form-data; name="${fieldName}"; filename="image-${index + 1}.${itemExt}"\r\nContent-Type: ${item.mimeType}\r\n\r\n`
        ));
        parts.push(item.buffer);
        parts.push(Buffer.from(`\r\n`));
      });
      parts.push(Buffer.from(`--${boundary}--\r\n`));

      const bodyBuffer = Buffer.concat(parts);

      const editsResponse = await fetchWithRetry(
        editsUrl,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${customApiConfig.apiKey}`,
            'Content-Type': `multipart/form-data; boundary=${boundary}`,
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
          },
          body: bodyBuffer,
        },
        GENERATION_TIMEOUT,
        1,
      );
      if (editsResponse.ok) {
        const editsData = await parseCustomApiJsonWithProgress(editsResponse, onProgress);
        let videos = extractVideosFromGenerationsResponse(editsData as Record<string, unknown>);
        if (videos.length === 0) videos = extractVideosFromChatResponse(editsData as Record<string, unknown>);
        if (videos.length > 0) {
          const persistedVideos = await persistAllMediaUrls(videos, 'generated/videos');
          return NextResponse.json({ videos: persistedVideos });
        }
        strategyResults.push('策略1(images/edits FormData): 响应中无视频数据');
      } else {
        const errorText = await editsResponse.text();
        const parsedError = parseCustomApiError(editsResponse.status, errorText);
        if (!firstUpstreamError) firstUpstreamError = { error: parsedError, status: editsResponse.status };
        strategyResults.push(parsedError);
      }
    } catch (err) {
      strategyResults.push(`策略1(images/edits FormData): ${err instanceof Error ? err.message : '异常'}`);
    }
  }

  // --- Strategy 2: chat/completions with image_url ---
  const chatUrl = deriveChatCompletionsUrl(endpoint);
  const chatBody: Record<string, unknown> = {
    model: customApiConfig.modelName,
    stream: false,
    messages: [
      {
        role: 'user',
        content: [
          ...imageUrls.map(url => ({ type: 'image_url', image_url: { url } })),
          { type: 'text', text: promptText },
        ],
      },
    ],
  };
  if (aspectRatio) chatBody.aspect_ratio = aspectRatio;
  if (duration) chatBody.duration = duration;
  if (fps) chatBody.fps = fps;

  console.log('[Custom API img2vid → 策略2: chat/completions] URL:', chatUrl, '| model:', customApiConfig.modelName);
  try {
    const chatResponse = await fetchWithRetry(chatUrl, { method: 'POST', headers, body: JSON.stringify(chatBody) }, GENERATION_TIMEOUT, 1);
    if (chatResponse.ok) {
      const chatData = await parseCustomApiJsonWithProgress(chatResponse, onProgress);
      let videos = extractVideosFromChatResponse(chatData as Record<string, unknown>);
      if (videos.length === 0) videos = extractVideosFromGenerationsResponse(chatData as Record<string, unknown>);
      if (videos.length > 0) {
        const persistedVideos = await persistAllMediaUrls(videos, 'generated/videos');
        return NextResponse.json({ videos: persistedVideos });
      }
    } else {
      const errorText = await chatResponse.text();
      const parsedError = parseCustomApiError(chatResponse.status, errorText);
      if (!firstUpstreamError) firstUpstreamError = { error: parsedError, status: chatResponse.status };
      strategyResults.push(parsedError);
    }
  } catch (err) {
    strategyResults.push(`策略2(chat/completions): ${err instanceof Error ? err.message : '异常'}`);
  }

  // --- Strategy 3: images/generations with init_image ---
  const imgBody: Record<string, unknown> = {
    model: customApiConfig.modelName,
    prompt: promptText,
    n: 1,
    size: '1024x1024',
    response_format: 'b64_json',
    init_image: rawBase64,
    images: imageUrls,
  };
  if (aspectRatio) imgBody.aspect_ratio = aspectRatio;
  if (duration) imgBody.duration = duration;
  if (fps) imgBody.fps = fps;

  console.log('[Custom API img2vid → 策略3: images/generations] URL:', endpoint, '| model:', customApiConfig.modelName);
  try {
    const imgResponse = await fetchWithRetry(endpoint, { method: 'POST', headers, body: JSON.stringify(imgBody) }, GENERATION_TIMEOUT, 1);
    if (!imgResponse.ok) {
      const errorText = await imgResponse.text();
      const parsedError = parseCustomApiError(imgResponse.status, errorText);
      if (!firstUpstreamError) firstUpstreamError = { error: parsedError, status: imgResponse.status };
      strategyResults.push(parsedError);
    } else {
      const imgData = await parseCustomApiJsonWithProgress(imgResponse, onProgress);
      const videos = extractVideosFromGenerationsResponse(imgData as Record<string, unknown>);
      if (videos.length > 0) {
        const persistedVideos = await persistAllMediaUrls(videos, 'generated/videos');
        return NextResponse.json({ videos: persistedVideos });
      }
      strategyResults.push('策略3(images/generations): 响应中无视频数据');
    }
  } catch (err) {
    strategyResults.push(`策略3(images/generations): ${err instanceof Error ? err.message : '异常'}`);
  }

  return NextResponse.json(
    {
      error: firstUpstreamError?.error || strategyResults.find(Boolean) || '图生视频失败',
    },
    { status: firstUpstreamError && firstUpstreamError.status < 500 ? firstUpstreamError.status : 502 }
  );
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const {
      prompt,
      negativePrompt,
      model = 'doubao-seedance-1-5-pro-251215',
      aspectRatio = '16:9',
      duration = 5,
      fps = 30,
      image,
      images,
      extraImages,
      customApiConfig,
    } = body as {
      prompt?: string;
      negativePrompt?: string;
      model?: string;
      aspectRatio?: string;
      duration?: number;
      fps?: number;
      image?: string;
      images?: string[];
      extraImages?: string[];
      customApiConfig?: CustomApiConfig;
    };
    const referenceImages = normalizeReferenceImages(image, images, extraImages);

    if (!prompt && referenceImages.length === 0) {
      return NextResponse.json({ error: '请提供视频描述或上传图片' }, { status: 400 });
    }
    const trustedInternalRequest = isTrustedInternalGenerationRequest(request);
    const trustedUserId = trustedInternalRequest
      ? request.headers.get('x-miaojing-generation-user-id')
      : null;
    const generationJobId = trustedInternalRequest
      ? request.headers.get('x-miaojing-generation-job-id')
      : null;
    const resolvedCustomApiConfig = await resolveServerApiConfig(
      request,
      customApiConfig,
      isUuid(trustedUserId) ? trustedUserId : null,
    );
    const handleUpstreamProgress = (progress: Record<string, unknown>) => updateGenerationJobProgress(
      isUuid(generationJobId) ? generationJobId : null,
      progress,
    );

    // ---- Custom API mode ----
    if (resolvedCustomApiConfig && resolvedCustomApiConfig.apiKey) {
      const resolvedApiKey = resolvedCustomApiConfig.apiKey;
      try {
        if (referenceImages.length > 0) {
          return await customApiImageToVideo(
            resolvedCustomApiConfig as CustomApiConfig,
            prompt,
            negativePrompt,
            referenceImages[0],
            referenceImages,
            aspectRatio,
            duration,
            fps,
            handleUpstreamProgress,
          );
        }

        // Text-to-video
        const endpoint = resolvedCustomApiConfig.apiUrl;
        if (!endpoint) return NextResponse.json({ error: '自定义API未配置请求地址' }, { status: 400 });
        if (!resolvedCustomApiConfig.modelName) return NextResponse.json({ error: '自定义API未配置模型名称' }, { status: 400 });

        // Augment prompt with aspect ratio hint as fallback
        const ratioHint = aspectRatio ? getAspectRatioPromptHint(aspectRatio) : '';
        const augmentedPrompt = ratioHint ? `${prompt || ''}\n\n[${ratioHint}]` : (prompt || '');

        const requestBody: Record<string, unknown> = {
          model: resolvedCustomApiConfig.modelName,
          prompt: augmentedPrompt,
          n: 1,
          size: '1024x1024',
          response_format: 'b64_json',
        };
        if (negativePrompt) requestBody.negative_prompt = negativePrompt;
        // Pass creation parameters for APIs that support them
        if (aspectRatio) requestBody.aspect_ratio = aspectRatio;
        if (duration) requestBody.duration = duration;
        if (fps) requestBody.fps = fps;

        console.log('[Custom API Video] Text-to-video, sending to:', endpoint, '| model:', requestBody.model);

        let customResponse: Response;
        try {
          customResponse = await fetchWithRetry(
            endpoint,
            { method: 'POST', headers: buildCustomApiHeaders(resolvedApiKey), body: JSON.stringify(requestBody) },
            GENERATION_TIMEOUT, 1,
          );
        } catch (fetchError: unknown) {
          if (fetchError instanceof DOMException && fetchError.name === 'AbortError') {
            return NextResponse.json({ error: '自定义API请求超时（180秒）' }, { status: 504 });
          }
          const msg = fetchError instanceof Error ? fetchError.message : '请求失败';
          return NextResponse.json({ error: `自定义API网络错误: ${msg}` }, { status: 502 });
        }

        if (!customResponse.ok) {
          const errorText = await customResponse.text();
          return NextResponse.json(
            { error: parseCustomApiError(customResponse.status, errorText) },
            { status: customResponse.status >= 500 ? 502 : customResponse.status }
          );
        }

        const customData = await parseCustomApiJsonWithProgress(customResponse, handleUpstreamProgress);
        const videos = extractVideosFromGenerationsResponse(customData as Record<string, unknown>);
        if (videos.length === 0) {
          return NextResponse.json({ error: '自定义API未返回有效视频数据', raw: customData }, { status: 502 });
        }
        // Persist all data URLs and remote URLs to S3
        const persistedVideos = await persistAllMediaUrls(videos, 'generated/videos');
        return NextResponse.json({ videos: persistedVideos });
      } catch (customError: unknown) {
        const msg = customError instanceof Error ? customError.message : '自定义API请求异常';
        console.error('[Custom API Video Exception]', msg);
        return NextResponse.json({ error: `自定义API异常: ${msg}` }, { status: 502 });
      }
    }

    // ---- Default mode: use coze-coding-dev-sdk ----
    const customHeaders = HeaderUtils.extractForwardHeaders(request.headers);
    const config = new Config();
    const client = new VideoGenerationClient(config, customHeaders);

    const contentItems: Array<{ type: string; text?: string; image_url?: { url: string }; role?: string }> = [];
    referenceImages.forEach((url, index) => {
      contentItems.push({ type: 'image_url', image_url: { url }, role: index === 0 ? 'first_frame' : 'reference' });
    });
    if (prompt) {
      contentItems.push({ type: 'text', text: prompt });
    }

    const ratioMap: Record<string, '16:9' | '9:16' | '1:1' | '4:3' | '3:4'> = {
      '16:9': '16:9', '9:16': '9:16', '1:1': '1:1', '4:3': '4:3', '3:4': '3:4',
    };

    const response = await client.videoGeneration(contentItems as Parameters<typeof client.videoGeneration>[0], {
      model,
      duration: Math.min(Math.max(duration, 4), 12),
      ratio: ratioMap[aspectRatio] || '16:9',
      resolution: '720p',
      generateAudio: true,
    });

    const videos: string[] = [];
    if (response.videoUrl) videos.push(response.videoUrl);
    if (videos.length === 0) return NextResponse.json({ error: '视频生成失败，请稍后重试' }, { status: 500 });

    // Persist SDK video URLs to S3 for reliable browser access
    const persistedVideos = await persistAllMediaUrls(videos, 'generated/videos');
    return NextResponse.json({ videos: persistedVideos });
  } catch (error: unknown) {
    const message = error instanceof Error ? error.message : '视频生成失败';
    console.error('[Video Generation Error]', message);
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
