import { useRef, useState, type CSSProperties } from 'react';
import {
  FileText,
  Image as ImageIcon,
  LoaderCircle,
  Music2,
  Paperclip,
  Plus,
  Repeat2,
  Video,
  X,
} from 'lucide-react';
import { uploadFileToStorage } from '../api';
import type { VideoGenerationContent } from '../api';
import type { PlaygroundMode } from '../types';

export type PlaygroundUploadKind = 'audio' | 'file' | 'image' | 'video';
export type PlaygroundUploadRole = 'first_frame' | 'last_frame';
export type PlaygroundVideoCreateMode = 'text_to_video' | 'first_last_frame' | 'omni_reference';

export interface PlaygroundUpload {
  contentType: string;
  id: string;
  kind: PlaygroundUploadKind;
  name: string;
  raw: Record<string, unknown>;
  role?: PlaygroundUploadRole;
  size: number;
  url: string;
}

export type OpenAIChatContentPart =
  | { type: 'text'; text: string }
  | { type: 'image_url'; image_url: { url: string } }
  | { type: 'video_url'; video_url: { mime_type?: string; url: string } }
  | { type: 'audio_url'; audio_url: { mime_type?: string; url: string } }
  | { type: 'file_url'; file_url: { filename: string; url: string } };

export const mediaUploadAccept = 'image/*,video/*,audio/*';
export const imageOnlyUploadAccept = 'image/*';
export const chatUploadAccept = [
  mediaUploadAccept,
  '.csv',
  '.doc',
  '.docx',
  '.json',
  '.jsonl',
  '.md',
  '.markdown',
  '.pdf',
  '.ppt',
  '.pptx',
  '.txt',
  '.xls',
  '.xlsx',
  '.yaml',
  '.yml',
  'application/json',
  'application/msword',
  'application/pdf',
  'application/vnd.ms-excel',
  'application/vnd.ms-powerpoint',
  'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  'text/*',
].join(',');

export function ComposerUploadButton(props: {
  accept: string;
  active?: boolean;
  disabled?: boolean;
  uploading?: boolean;
  onFiles?: (files: File[]) => void;
}) {
  const inputRef = useRef<HTMLInputElement>(null);
  const disabled = props.disabled || props.uploading;

  return (
    <>
      <button
        type="button"
        className="composerUpload"
        aria-label="上传附件"
        data-active={props.active === true}
        disabled={disabled}
        onClick={() => inputRef.current?.click()}
      >
        {props.uploading ? <LoaderCircle className="composerUploadSpinner" size={18} /> : <Paperclip size={18} />}
      </button>
      <input
        ref={inputRef}
        type="file"
        multiple
        hidden
        accept={props.accept}
        disabled={disabled}
        onChange={(event) => {
          const files = Array.from(event.currentTarget.files ?? []);
          event.currentTarget.value = '';
          props.onFiles?.(files);
        }}
      />
    </>
  );
}

export function PlaygroundReferencePicker(props: {
  accept: string;
  disabled?: boolean;
  mode: PlaygroundMode;
  uploadLabel?: string;
  uploads: PlaygroundUpload[];
  uploading?: boolean;
  videoMode?: PlaygroundVideoCreateMode;
  onFiles?: (files: File[], targetRole?: PlaygroundUploadRole) => void;
  onRemove?: (id: string) => void;
  onSwapFrames?: () => void;
}) {
  if (props.mode === 'video' && props.videoMode === 'first_last_frame') {
    return (
      <FirstLastFramePicker
        accept={props.accept}
        disabled={props.disabled}
        uploads={props.uploads}
        uploading={props.uploading}
        onFiles={props.onFiles}
        onRemove={props.onRemove}
        onSwapFrames={props.onSwapFrames}
      />
    );
  }
  return (
    <StackedReferencePicker
      accept={props.accept}
      disabled={props.disabled}
      uploadLabel={props.uploadLabel ?? (props.mode === 'chat' ? '上传附件' : '参考内容')}
      uploads={props.uploads}
      uploading={props.uploading}
      onFiles={props.onFiles}
      onRemove={props.onRemove}
    />
  );
}

function StackedReferencePicker(props: {
  accept: string;
  disabled?: boolean;
  uploadLabel: string;
  uploads: PlaygroundUpload[];
  uploading?: boolean;
  onFiles?: (files: File[]) => void;
  onRemove?: (id: string) => void;
}) {
  const inputRef = useRef<HTMLInputElement>(null);
  const [hoveredId, setHoveredId] = useState('');
  const [expanded, setExpanded] = useState(false);
  const hoveredUpload = props.uploads.find((item) => item.id === hoveredId);
  const disabled = props.disabled || props.uploading || !props.onFiles;
  const uploadCardIndex = props.uploads.length;

  return (
    <div
      className="mediaReferenceStack"
      data-expanded={expanded}
      onMouseLeave={() => {
        setExpanded(false);
        setHoveredId('');
      }}
    >
      {hoveredUpload && <div className="mediaReferenceTooltip">{hoveredUpload.name}</div>}
      <div
        className="mediaReferenceStackCards"
        data-empty={!props.uploads.length}
        style={{ '--reference-count': Math.max(1, props.uploads.length + 1) } as CSSProperties}
      >
        {props.uploads.map((item, index) => (
          <div
            className="mediaReferenceCard"
            data-hovered={hoveredId === item.id}
            data-kind={item.kind}
            key={item.id}
            style={referenceCardStyle(index)}
            title={item.name}
            onMouseEnter={() => {
              setExpanded(true);
              setHoveredId(item.id);
            }}
          >
            <ReferencePreview item={item} />
            {item.kind !== 'image' && <span className="mediaReferenceDuration">{uploadKindLabel(item.kind)}</span>}
            {props.onRemove && hoveredId === item.id && (
              <button type="button" className="mediaReferenceRemove" aria-label={`删除 ${item.name}`} onClick={() => props.onRemove?.(item.id)}>
                <X size={13} />
              </button>
            )}
          </div>
        ))}
        <button
          type="button"
          className="mediaReferenceCard mediaReferenceUploadCard"
          aria-label={props.uploadLabel}
          data-has-uploads={props.uploads.length > 0}
          data-uploading={Boolean(props.uploading)}
          disabled={disabled}
          style={referenceCardStyle(uploadCardIndex)}
          title={props.uploadLabel}
          onClick={() => inputRef.current?.click()}
          onMouseEnter={() => setHoveredId('')}
        >
          {props.uploading ? <LoaderCircle className="composerUploadSpinner" size={18} /> : <Plus size={20} />}
          <span>{props.uploadLabel}</span>
        </button>
        {props.uploads.length > 0 && (
          <button
            type="button"
            className="mediaReferenceAdd"
            aria-label={props.uploadLabel}
            data-uploading={Boolean(props.uploading)}
            disabled={disabled}
            title={props.uploadLabel}
            onClick={() => inputRef.current?.click()}
            onMouseEnter={() => {
              setExpanded(false);
              setHoveredId('');
            }}
          >
            {props.uploading ? <LoaderCircle className="composerUploadSpinner" size={15} /> : <Plus size={17} />}
          </button>
        )}
      </div>
      <input
        ref={inputRef}
        type="file"
        multiple
        hidden
        accept={props.accept}
        disabled={disabled}
        onChange={(event) => {
          const files = Array.from(event.currentTarget.files ?? []);
          event.currentTarget.value = '';
          props.onFiles?.(files);
        }}
      />
    </div>
  );
}

const referenceTiltValues = [-7, 6, -3, 8, -5, 4, -8, 5];
const referenceXValues = [0, -5, 6, -3, 4, -6, 3, -4];
const referenceYValues = [0, 3, -1, 4, 1, 5, 2, -2];

function referenceCardStyle(index: number) {
  const valueIndex = index % referenceTiltValues.length;
  return {
    '--reference-index': index,
    '--reference-tilt': `${referenceTiltValues[valueIndex]}deg`,
    '--reference-x': `${referenceXValues[valueIndex]}px`,
    '--reference-y': `${referenceYValues[valueIndex]}px`,
  } as CSSProperties;
}

function FirstLastFramePicker(props: {
  accept: string;
  disabled?: boolean;
  uploads: PlaygroundUpload[];
  uploading?: boolean;
  onFiles?: (files: File[], targetRole?: PlaygroundUploadRole) => void;
  onRemove?: (id: string) => void;
  onSwapFrames?: () => void;
}) {
  const first = frameUploadByRole(props.uploads, 'first_frame');
  const last = frameUploadByRole(props.uploads, 'last_frame');
  const canSwap = Boolean(first && last);

  return (
    <div className="firstLastFramePicker">
      <FrameSlot
        accept={props.accept}
        disabled={props.disabled}
        item={first}
        label="首帧"
        role="first_frame"
        uploading={props.uploading}
        onFiles={props.onFiles}
        onRemove={props.onRemove}
      />
      <button type="button" className="frameSwapButton" aria-label="交换首尾帧" disabled={!canSwap} onClick={props.onSwapFrames}>
        <Repeat2 size={19} />
      </button>
      <FrameSlot
        accept={props.accept}
        disabled={props.disabled}
        item={last}
        label="尾帧"
        role="last_frame"
        uploading={props.uploading}
        onFiles={props.onFiles}
        onRemove={props.onRemove}
      />
    </div>
  );
}

function FrameSlot(props: {
  accept: string;
  disabled?: boolean;
  item?: PlaygroundUpload;
  label: string;
  role: PlaygroundUploadRole;
  uploading?: boolean;
  onFiles?: (files: File[], targetRole?: PlaygroundUploadRole) => void;
  onRemove?: (id: string) => void;
}) {
  const inputRef = useRef<HTMLInputElement>(null);
  const disabled = props.disabled || props.uploading || !props.onFiles;

  return (
    <div className="frameSlot">
      <button
        type="button"
        className="frameSlotButton"
        data-filled={Boolean(props.item)}
        disabled={disabled}
        title={props.item?.name ?? props.label}
        onClick={() => inputRef.current?.click()}
      >
        {props.item ? <ReferencePreview item={props.item} /> : <Plus size={20} />}
        <span>{props.label}</span>
      </button>
      {props.item && props.onRemove && (
        <button type="button" className="frameSlotRemove" aria-label={`删除 ${props.label}`} onClick={() => props.onRemove?.(props.item!.id)}>
          <X size={13} />
        </button>
      )}
      <input
        ref={inputRef}
        type="file"
        hidden
        accept={props.accept}
        disabled={disabled}
        onChange={(event) => {
          const files = Array.from(event.currentTarget.files ?? []);
          event.currentTarget.value = '';
          props.onFiles?.(files, props.role);
        }}
      />
    </div>
  );
}

function ReferencePreview(props: { item: PlaygroundUpload }) {
  if (props.item.kind === 'image') {
    return <img src={props.item.url} alt="" draggable={false} />;
  }
  if (props.item.kind === 'video') {
    return <video src={props.item.url} muted playsInline preload="metadata" />;
  }
  if (props.item.kind === 'audio') {
    return (
      <span className="mediaReferencePlaceholder">
        <Music2 size={18} />
      </span>
    );
  }
  return (
    <span className="mediaReferencePlaceholder">
      <FileText size={18} />
    </span>
  );
}

export function UploadAttachmentList(props: {
  message?: string;
  uploads: PlaygroundUpload[];
  onRemove?: (id: string) => void;
}) {
  if (!props.uploads.length && !props.message) return null;
  return (
    <div className="composerUploadArea">
      {props.uploads.length > 0 && (
        <div className="composerUploadList">
          {props.uploads.map((item) => (
            <span className="composerUploadChip" key={item.id} title={`${item.name} · ${item.url}`}>
              {uploadKindIcon(item.kind)}
              <span>{item.name}</span>
              <small>{formatFileSize(item.size)}</small>
              {props.onRemove && (
                <button type="button" aria-label={`移除 ${item.name}`} onClick={() => props.onRemove?.(item.id)}>
                  <X size={13} />
                </button>
              )}
            </span>
          ))}
        </div>
      )}
      {props.message && <div className="composerUploadMessage">{props.message}</div>}
    </div>
  );
}

function uploadKindIcon(kind: PlaygroundUploadKind) {
  if (kind === 'image') return <ImageIcon size={14} />;
  if (kind === 'video') return <Video size={14} />;
  if (kind === 'audio') return <Music2 size={14} />;
  return <FileText size={14} />;
}

export async function uploadPlaygroundFiles(
  token: string,
  files: File[],
  options: { allowFiles: boolean; allowedKinds?: PlaygroundUploadKind[]; source: string },
): Promise<{ items: PlaygroundUpload[]; warnings: string[] }> {
  const allowedKinds = options.allowedKinds ?? (options.allowFiles ? ['audio', 'file', 'image', 'video'] : ['audio', 'image', 'video']);
  const accepted: Array<{ file: File; kind: PlaygroundUploadKind }> = [];
  const warnings: string[] = [];
  files.forEach((file) => {
    const kind = acceptedUploadKind(file, options.allowFiles);
    if (!kind || !allowedKinds.includes(kind)) {
      warnings.push(options.allowFiles
        ? `已跳过 ${file.name}，聊天仅支持图片、视频、音频和常见文档。`
        : `已跳过 ${file.name}，当前场景仅支持${allowedUploadKindLabel(allowedKinds)}。`);
      return;
    }
    accepted.push({ file, kind });
  });
  if (!accepted.length) return { items: [], warnings };
  const items = await Promise.all(accepted.map(async ({ file, kind }) => {
    const response = await uploadFileToStorage(token, file, options.source);
    const url = uploadResponseUrl(response);
    if (!url) {
      throw new Error(`${file.name} 上传成功，但网关没有返回可用文件 URL。`);
    }
    return {
      contentType: file.type || '',
      id: newLocalId(),
      kind,
      name: file.name || '未命名文件',
      raw: response,
      size: file.size,
      url,
    };
  }));
  return { items, warnings };
}

function acceptedUploadKind(file: File, allowFiles: boolean): PlaygroundUploadKind | undefined {
  const mime = file.type.toLowerCase();
  const extension = fileExtension(file.name);
  if (mime.startsWith('image/') || imageExtensions.has(extension)) return 'image';
  if (mime.startsWith('video/') || videoExtensions.has(extension)) return 'video';
  if (mime.startsWith('audio/') || audioExtensions.has(extension)) return 'audio';
  if (allowFiles && (documentExtensions.has(extension) || documentMimes.has(mime))) return 'file';
  return undefined;
}

const imageExtensions = new Set(['avif', 'bmp', 'gif', 'heic', 'heif', 'jpeg', 'jpg', 'png', 'svg', 'tif', 'tiff', 'webp']);
const videoExtensions = new Set(['avi', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'webm']);
const audioExtensions = new Set(['aac', 'flac', 'm4a', 'mp3', 'oga', 'ogg', 'opus', 'wav', 'weba']);
const documentExtensions = new Set(['csv', 'doc', 'docx', 'json', 'jsonl', 'md', 'markdown', 'pdf', 'ppt', 'pptx', 'txt', 'xls', 'xlsx', 'yaml', 'yml']);
const documentMimes = new Set([
  'application/json',
  'application/msword',
  'application/pdf',
  'application/vnd.ms-excel',
  'application/vnd.ms-powerpoint',
  'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  'text/csv',
  'text/markdown',
  'text/plain',
  'text/yaml',
]);

function fileExtension(name: string) {
  const index = name.lastIndexOf('.');
  return index >= 0 ? name.slice(index + 1).toLowerCase() : '';
}

function uploadResponseUrl(response: Record<string, unknown>) {
  const data = recordFromUnknown(response.data);
  const file = recordFromUnknown(response.file);
  const result = recordFromUnknown(response.result);
  return firstString(
    response.url,
    response.fileUrl,
    response.file_url,
    response.objectUrl,
    response.object_url,
    response.downloadUrl,
    response.download_url,
    data?.url,
    data?.fileUrl,
    data?.file_url,
    file?.url,
    file?.fileUrl,
    file?.file_url,
    result?.url,
    result?.fileUrl,
    result?.file_url,
  );
}

export function openAIContentFromPromptAndUploads(prompt: string, uploads: PlaygroundUpload[]): OpenAIChatContentPart[] {
  const content: OpenAIChatContentPart[] = [];
  const text = prompt.trim();
  if (text) {
    content.push({ type: 'text', text });
  }
  uploads.forEach((item) => {
    const part = openAIContentPartFromUpload(item);
    if (part) content.push(part);
  });
  return content.length ? content : [{ type: 'text', text: '' }];
}

function openAIContentPartFromUpload(item: PlaygroundUpload): OpenAIChatContentPart | undefined {
  if (!item.url) return undefined;
  if (item.kind === 'image') return { type: 'image_url', image_url: { url: item.url } };
  if (item.kind === 'video') return { type: 'video_url', video_url: mediaURLPayload(item) };
  if (item.kind === 'audio') return { type: 'audio_url', audio_url: mediaURLPayload(item) };
  return { type: 'file_url', file_url: { filename: item.name, url: item.url } };
}

function mediaURLPayload(item: PlaygroundUpload) {
  const payload: { mime_type?: string; url: string } = { url: item.url };
  if (item.contentType) payload.mime_type = item.contentType;
  return payload;
}

export function mediaUploadRequestPayload(uploads: PlaygroundUpload[], mode: Exclude<PlaygroundMode, 'chat'>) {
  const images = uploads.filter((item) => item.kind === 'image').map((item) => item.url);
  const payload: Record<string, string | string[]> = {};
  if (mode === 'image') {
    if (images.length) {
      payload.image = singleOrMany(images);
      payload.images = images;
    }
    return payload;
  }
  return payload;
}

export function videoGenerationContentFromPromptAndUploads(
  prompt: string,
  uploads: PlaygroundUpload[],
  videoMode: PlaygroundVideoCreateMode,
): VideoGenerationContent[] {
  const content: VideoGenerationContent[] = [];
  const text = prompt.trim();
  if (text) {
    content.push({ type: 'text', text });
  }
  if (videoMode === 'first_last_frame') {
    const first = frameUploadByRole(uploads, 'first_frame');
    const last = frameUploadByRole(uploads, 'last_frame');
    if (first?.url) {
      content.push({ type: 'image_url', role: 'first_frame', image_url: { url: first.url } });
    }
    if (last?.url) {
      content.push({ type: 'image_url', role: 'last_frame', image_url: { url: last.url } });
    }
    return content.length ? content : [{ type: 'text', text: '' }];
  }
  uploads.forEach((item) => {
    const part = videoGenerationContentFromUpload(item);
    if (part) content.push(part);
  });
  return content.length ? content : [{ type: 'text', text: '' }];
}

function videoGenerationContentFromUpload(item: PlaygroundUpload): VideoGenerationContent | undefined {
  if (!item.url) return undefined;
  if (item.kind === 'image') {
    return { type: 'image_url', role: 'reference_image', image_url: { url: item.url } };
  }
  if (item.kind === 'video') {
    return { type: 'video_url', role: 'reference_video', video_url: { ...mediaURLPayload(item), refer_type: 'feature' } };
  }
  if (item.kind === 'audio') {
    return { type: 'audio_url', role: 'reference_audio', audio_url: mediaURLPayload(item) };
  }
  return undefined;
}

function singleOrMany(values: string[]) {
  return values.length === 1 ? values[0] : values;
}

export function uploadKindLabel(kind: PlaygroundUploadKind) {
  if (kind === 'image') return '图片';
  if (kind === 'video') return '视频';
  if (kind === 'audio') return '音频';
  return '文件';
}

export function allowedUploadKindLabel(kinds: PlaygroundUploadKind[]) {
  const labels = kinds.map(uploadKindLabel);
  return labels.length ? labels.join('、') : '当前文件类型';
}

export function formatFileSize(size: number) {
  if (!Number.isFinite(size) || size <= 0) return '';
  if (size < 1024) return `${size} B`;
  if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
  return `${(size / 1024 / 1024).toFixed(1)} MB`;
}

export function mediaUploadAcceptForMode(mode: PlaygroundMode, videoMode: PlaygroundVideoCreateMode) {
  if (mode === 'image') return imageOnlyUploadAccept;
  if (mode === 'video' && videoMode === 'first_last_frame') return imageOnlyUploadAccept;
  return mediaUploadAccept;
}

export function allowedMediaUploadKinds(mode: PlaygroundMode, videoMode: PlaygroundVideoCreateMode): PlaygroundUploadKind[] {
  if (mode === 'image') return ['image'];
  if (mode === 'video' && videoMode === 'first_last_frame') return ['image'];
  if (mode === 'video') return ['audio', 'image', 'video'];
  return ['audio', 'file', 'image', 'video'];
}

export function mediaUploadSummaryMessage(uploads: PlaygroundUpload[], mode: PlaygroundMode, videoMode: PlaygroundVideoCreateMode) {
  if (!uploads.length) return '';
  const images = uploads.filter((item) => item.kind === 'image').length;
  const videos = uploads.filter((item) => item.kind === 'video').length;
  const audios = uploads.filter((item) => item.kind === 'audio').length;
  const files = uploads.filter((item) => item.kind === 'file').length;
  if (mode === 'image') {
    return `已上传 ${images} 张参考图。`;
  }
  if (mode === 'video' && videoMode === 'first_last_frame') {
    const first = frameUploadByRole(uploads, 'first_frame');
    const last = frameUploadByRole(uploads, 'last_frame');
    if (first && last) return '已上传首帧、尾帧参考图。';
    if (first) return '已上传首帧参考图。';
    if (last) return '已上传尾帧参考图。';
    return `已上传 ${images} 张首尾帧参考图。`;
  }
  const parts = [
    images ? `${images} 张图片` : '',
    videos ? `${videos} 个视频` : '',
    audios ? `${audios} 段音频` : '',
    files ? `${files} 个文件` : '',
  ].filter(Boolean);
  return parts.length ? `已上传 ${parts.join('、')}。` : '';
}

export function mergeMediaUploadsForMode(
  current: PlaygroundUpload[],
  items: PlaygroundUpload[],
  mode: PlaygroundMode,
  videoMode: PlaygroundVideoCreateMode,
  targetRole?: PlaygroundUploadRole,
) {
  if (mode === 'image') {
    return [...current.filter((item) => item.kind === 'image'), ...items.filter((item) => item.kind === 'image')];
  }
  if (mode === 'video' && videoMode === 'first_last_frame') {
    return mergeFirstLastFrameUploads(current, items, targetRole);
  }
  if (mode === 'video') {
    return [...current, ...items.filter((item) => item.kind === 'image' || item.kind === 'video' || item.kind === 'audio')];
  }
  return [...current, ...items];
}

export function normalizeFirstLastFrameUploads(uploads: PlaygroundUpload[]) {
  const images = uploads.filter((item) => item.kind === 'image');
  if (!images.length) return uploads.length ? [] : uploads;
  const first = frameUploadByRole(images, 'first_frame') ?? images[0];
  const last = frameUploadByRole(images, 'last_frame') ?? images.find((item) => item.id !== first?.id);
  const next: PlaygroundUpload[] = [];
  if (first) next.push({ ...first, role: 'first_frame' });
  if (last) next.push({ ...last, role: 'last_frame' });
  return uploadListsEqual(uploads, next) ? uploads : next;
}

function mergeFirstLastFrameUploads(current: PlaygroundUpload[], items: PlaygroundUpload[], targetRole?: PlaygroundUploadRole) {
  const incoming = items.filter((item) => item.kind === 'image');
  let next = normalizeFirstLastFrameUploads(current);
  if (!incoming.length) return next;
  const assignUpload = (item: PlaygroundUpload, role: PlaygroundUploadRole) => {
    next = next.filter((upload) => upload.role !== role);
    next.push({ ...item, role });
  };
  if (targetRole) {
    assignUpload(incoming[0]!, targetRole);
    const oppositeRole: PlaygroundUploadRole = targetRole === 'first_frame' ? 'last_frame' : 'first_frame';
    incoming.slice(1).forEach((item) => {
      if (!frameUploadByRole(next, oppositeRole)) assignUpload(item, oppositeRole);
    });
    return sortFrameUploads(next);
  }
  incoming.forEach((item) => {
    if (!frameUploadByRole(next, 'first_frame')) {
      assignUpload(item, 'first_frame');
    } else if (!frameUploadByRole(next, 'last_frame')) {
      assignUpload(item, 'last_frame');
    }
  });
  return sortFrameUploads(next);
}

export function swapFirstLastFrameUploads(uploads: PlaygroundUpload[]) {
  return sortFrameUploads(uploads.map((item) => {
    if (item.role === 'first_frame') return { ...item, role: 'last_frame' as const };
    if (item.role === 'last_frame') return { ...item, role: 'first_frame' as const };
    return item;
  }));
}

function sortFrameUploads(uploads: PlaygroundUpload[]) {
  const first = frameUploadByRole(uploads, 'first_frame');
  const last = frameUploadByRole(uploads, 'last_frame');
  return [first, last].filter((item): item is PlaygroundUpload => Boolean(item));
}

export function frameUploadByRole(uploads: PlaygroundUpload[], role: PlaygroundUploadRole) {
  return uploads.find((item) => item.role === role);
}

function uploadListsEqual(left: PlaygroundUpload[], right: PlaygroundUpload[]) {
  if (left.length !== right.length) return false;
  return left.every((item, index) => {
    const next = right[index];
    return next && item.id === next.id && item.role === next.role && item.kind === next.kind;
  });
}

function recordFromUnknown(value: unknown): Record<string, unknown> | undefined {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
  return value as Record<string, unknown>;
}

function firstString(...values: unknown[]) {
  for (const value of values) {
    const text = typeof value === 'string' ? value.trim() : '';
    if (text) return text;
  }
  return '';
}

function newLocalId() {
  return typeof crypto !== 'undefined' && 'randomUUID' in crypto
    ? crypto.randomUUID()
    : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
