import { useEffect, useRef, type CSSProperties, type ReactNode } from 'react';
import type { GatewayTask, PlatformModel } from '@easyai-ai-gateway/contracts';
import Segmented from 'antd/es/segmented';
import Slider from 'antd/es/slider';
import {
  Download,
  Edit3,
  FileText,
  Image as ImageIcon,
  Images,
  Link2,
  LoaderCircle,
  Music2,
  Sparkles,
  Square,
} from 'lucide-react';
import { resolveApiAssetUrl } from '../api';
import { Button, Input, Popover, PopoverContent, PopoverTrigger } from '../components/ui';
import type { PlaygroundMode } from '../types';
import type { PlaygroundUpload, PlaygroundUploadKind, PlaygroundVideoCreateMode } from './playground-upload';

export type MediaOutputMode = 'single' | 'group';
export type MediaCountPreset = 1 | 2 | 3 | 4 | 'custom';
export type MediaResolution = string;
export type ImageQuality = 'low' | 'medium' | 'high' | 'auto';

const mediaGridGap = 2;
const mediaPreviewMaxHeight = 600;

export interface MediaGenerationSettings {
  aspectRatio: string;
  countPreset: MediaCountPreset;
  customCount: number;
  durationSeconds: number;
  height: number;
  outputMode: MediaOutputMode;
  outputAudio: boolean;
  quality: ImageQuality;
  resolution: MediaResolution;
  width: number;
}

export interface MediaGenerationRun {
  createdAt: string;
  error?: string;
  localId: string;
  mode: Exclude<PlaygroundMode, 'chat'>;
  modelLabel: string;
  modelValue?: string;
  prompt: string;
  settings: MediaGenerationSettings;
  status: GatewayTask['status'] | 'submitting';
  task?: GatewayTask;
  uploads?: PlaygroundUpload[];
  videoMode?: PlaygroundVideoCreateMode;
}

export function gatewayTaskErrorText(task: GatewayTask | undefined, fallback = '任务失败') {
  if (!task) return fallback;
  const result = recordFromUnknown(task.result);
  const resultError = recordFromUnknown(result?.error);
  const metrics = recordFromUnknown(task.metrics);
  const hasErrorState = task.status === 'failed'
    || task.status === 'cancelled'
    || Boolean(task.error || task.errorMessage || task.errorCode || resultError || metrics?.error);
  if (!hasErrorState) return '';
  const message = firstString(
    task.errorMessage,
    task.error,
    resultError?.message,
    result?.message,
    metrics?.error,
    fallback,
  );
  const meta = uniqueStrings([
    firstString(task.errorCode, resultError?.code, metrics?.errorCode) ? `错误码: ${firstString(task.errorCode, resultError?.code, metrics?.errorCode)}` : '',
    firstString(task.requestId, resultError?.requestId, resultError?.request_id, metrics?.requestId) ? `RequestID: ${firstString(task.requestId, resultError?.requestId, resultError?.request_id, metrics?.requestId)}` : '',
    task.id ? `TaskID: ${task.id}` : '',
  ]);
  if (!message && !meta.length) return '';
  return meta.length ? `${message || fallback}（${meta.join('，')}）` : message;
}

interface AspectRatioOption {
  label: string;
  value: string;
  visual: 'smart' | 'wide' | 'landscape' | 'square' | 'portrait' | 'tall';
}

interface ResolutionOption {
  label: string;
  modes: Array<Exclude<PlaygroundMode, 'chat'>>;
  value: MediaResolution;
}

export interface MediaModelCapabilities {
  aspectRatios: string[];
  durationOptions: number[];
  durationRange: [number, number];
  durationStep: number;
  maxCount: number;
  resolutions: MediaResolution[];
  supportsAudio: boolean;
  supportsGroup: boolean;
  supportsQualityControl: boolean;
}

const aspectRatioOptions: AspectRatioOption[] = [
  { value: 'auto', label: '智能', visual: 'smart' },
  { value: '8:1', label: '8:1', visual: 'wide' },
  { value: '4:1', label: '4:1', visual: 'wide' },
  { value: '21:9', label: '21:9', visual: 'wide' },
  { value: '16:9', label: '16:9', visual: 'wide' },
  { value: '3:2', label: '3:2', visual: 'landscape' },
  { value: '5:4', label: '5:4', visual: 'landscape' },
  { value: '4:3', label: '4:3', visual: 'landscape' },
  { value: '1:1', label: '1:1', visual: 'square' },
  { value: '4:5', label: '4:5', visual: 'portrait' },
  { value: '3:4', label: '3:4', visual: 'portrait' },
  { value: '2:3', label: '2:3', visual: 'portrait' },
  { value: '9:16', label: '9:16', visual: 'tall' },
  { value: '1:4', label: '1:4', visual: 'tall' },
  { value: '1:8', label: '1:8', visual: 'tall' },
];

const resolutionOptions: ResolutionOption[] = [
  { value: '1K', label: '标准 1K', modes: ['image'] },
  { value: '2K', label: '高清 2K', modes: ['image'] },
  { value: '4K', label: '超清 4K', modes: ['image'] },
  { value: '480p', label: '标清 480p', modes: ['video'] },
  { value: '720p', label: '高清 720p', modes: ['video'] },
  { value: '1080p', label: '全高清 1080p', modes: ['video'] },
  { value: '2160p', label: '超清 2160p', modes: ['video'] },
];

const countPresetOptions: Array<{ label: string; value: MediaCountPreset }> = [
  { value: 1, label: '1' },
  { value: 2, label: '2' },
  { value: 3, label: '3' },
  { value: 4, label: '4' },
  { value: 'custom', label: '自定义' },
];

const imageQualityOptions: Array<{ label: string; value: ImageQuality }> = [
  { value: 'low', label: '低' },
  { value: 'medium', label: '标准' },
  { value: 'high', label: '高' },
  { value: 'auto', label: '自动' },
];

export function defaultMediaGenerationSettings(): MediaGenerationSettings {
  return {
    aspectRatio: '1:1',
    countPreset: 1,
    customCount: 6,
    durationSeconds: 5,
    height: 2048,
    outputMode: 'single',
    outputAudio: true,
    quality: 'auto',
    resolution: '2K',
    width: 2048,
  };
}

export function mediaOutputCount(settings: MediaGenerationSettings) {
  if (settings.outputMode === 'single') return 1;
  const raw = settings.countPreset === 'custom' ? settings.customCount : settings.countPreset;
  return clampNumber(raw, 1, 20);
}

export function mediaRequestPayload(
  settings: MediaGenerationSettings,
  mode: Exclude<PlaygroundMode, 'chat'>,
  options?: { supportsQualityControl?: boolean },
) {
  if (mode === 'video') {
    return {
      aspect_ratio: settings.aspectRatio === 'auto' ? undefined : settings.aspectRatio,
      audio: settings.outputAudio,
      duration: settings.durationSeconds,
      resolution: settings.resolution,
    };
  }

  const count = mediaOutputCount(settings);
  const size = `${settings.width}x${settings.height}`;
  return {
    aspect_ratio: settings.aspectRatio === 'auto' ? undefined : settings.aspectRatio,
    count,
    height: settings.height,
    n: count,
    quality: options?.supportsQualityControl ? settings.quality : undefined,
    resolution: settings.resolution,
    size,
    width: settings.width,
  };
}

export function deriveMediaModelCapabilities(
  models: PlatformModel[] | PlatformModel | undefined,
  mode: Exclude<PlaygroundMode, 'chat'>,
  contextKey?: string,
  resolution?: string,
): MediaModelCapabilities {
  const modelList = (Array.isArray(models) ? models : models ? [models] : []).filter(Boolean);
  if (!modelList.length) return defaultMediaModelCapabilities(mode);
  const derived = modelList.map((model) => deriveSingleMediaModelCapabilities(model, mode, contextKey, resolution));
  if (derived.length === 1) return derived[0];
  const durationOptions = intersectNumberValues(
    derived.map((item) => durationValuesForCapabilities(item)),
    durationValuesForCapabilities(defaultMediaModelCapabilities(mode)),
  );
  return {
    aspectRatios: intersectOptionValues(derived.map((item) => item.aspectRatios), aspectRatioOptions.map((item) => item.value)),
    durationOptions,
    durationRange: durationRangeFromValues(durationOptions),
    durationStep: 1,
    maxCount: Math.max(1, Math.min(...derived.map((item) => item.maxCount))),
    resolutions: intersectOptionValues(derived.map((item) => item.resolutions), resolutionOptionsForMode(mode).map((item) => item.value)),
    supportsAudio: derived.every((item) => item.supportsAudio),
    supportsGroup: derived.every((item) => item.supportsGroup),
    supportsQualityControl: derived.every((item) => item.supportsQualityControl),
  };
}

export function normalizeMediaSettingsForCapabilities(
  settings: MediaGenerationSettings,
  capabilities: MediaModelCapabilities,
  mode: Exclude<PlaygroundMode, 'chat'>,
) {
  const aspectOptions = filterAspectRatioOptions(capabilities);
  const resolutionItems = filterResolutionOptions(capabilities, mode);
  const durationOptions = durationValuesForCapabilities(capabilities);
  const maxCount = Math.max(1, Math.min(capabilities.maxCount, 20));
  const supportsGroup = capabilities.supportsGroup && maxCount > 1;
  const next: MediaGenerationSettings = {
    ...settings,
    aspectRatio: aspectOptions.some((item) => item.value === settings.aspectRatio) ? settings.aspectRatio : aspectOptions[0]?.value ?? 'auto',
    resolution: resolutionItems.some((item) => item.value === settings.resolution) ? settings.resolution : resolutionItems[0]?.value ?? settings.resolution,
  };

  if (mode === 'video') {
    next.countPreset = 1;
    next.customCount = 1;
    next.durationSeconds = closestDurationValue(settings.durationSeconds, durationOptions);
    next.outputAudio = capabilities.supportsAudio ? settings.outputAudio : false;
    next.outputMode = 'single';
    return mediaSettingsEqual(settings, next) ? settings : next;
  }

  if (!supportsGroup) {
    next.countPreset = 1;
    next.customCount = 1;
    next.outputMode = 'single';
  } else if (next.outputMode === 'group') {
    if (next.countPreset === 'custom') {
      next.customCount = clampNumber(next.customCount, 2, maxCount);
    } else if (next.countPreset > maxCount) {
      next.countPreset = maxCount <= 4 ? (maxCount as MediaCountPreset) : 'custom';
      next.customCount = maxCount;
    } else if (next.countPreset === 1) {
      next.outputMode = 'single';
    }
  } else {
    next.countPreset = 1;
  }
  if (!capabilities.supportsQualityControl) {
    next.quality = 'auto';
  }

  return mediaSettingsEqual(settings, next) ? settings : next;
}

export function MediaSettingsPopover(props: {
  capabilities?: MediaModelCapabilities;
  mode: Exclude<PlaygroundMode, 'chat'>;
  settings: MediaGenerationSettings;
  onChange: (settings: MediaGenerationSettings) => void;
}) {
  const capabilities = props.capabilities ?? defaultMediaModelCapabilities(props.mode);
  const aspectOptions = filterAspectRatioOptions(capabilities);
  const resolutionItems = filterResolutionOptions(capabilities, props.mode);
  const isImageMode = props.mode === 'image';
  const isVideoMode = props.mode === 'video';
  const durationOptions = durationValuesForCapabilities(capabilities);
  const selectedDuration = closestDurationValue(props.settings.durationSeconds, durationOptions);
  const durationUsesExplicitOptions = capabilities.durationOptions.length > 0;
  const durationMarks = durationSliderMarks(durationOptions, durationUsesExplicitOptions);
  const durationMin = durationOptions[0] ?? props.settings.durationSeconds;
  const durationMax = durationOptions[durationOptions.length - 1] ?? props.settings.durationSeconds;
  const durationStep = durationUsesExplicitOptions ? null : capabilities.durationStep;
  const maxCount = Math.max(1, Math.min(capabilities.maxCount, 20));
  const supportsGroup = capabilities.supportsGroup && maxCount > 1;
  const countOptions = countPresetOptions.filter((item) => item.value === 'custom' ? maxCount > 4 : item.value <= Math.min(4, maxCount));
  const count = isImageMode ? mediaOutputCount(props.settings) : 1;
  const unit = '张';

  function patch(next: Partial<MediaGenerationSettings>) {
    props.onChange(normalizeMediaSettingsForCapabilities({ ...props.settings, ...next }, capabilities, props.mode));
  }

  function selectCountPreset(value: MediaCountPreset) {
    if (value !== 1 && !supportsGroup) return;
    patch({
      countPreset: value,
      outputMode: value === 1 ? 'single' : 'group',
    });
  }

  return (
    <Popover>
      <PopoverTrigger asChild>
        <button type="button" className="mediaSettingsTrigger">
          <Square size={15} />
          <span>{mediaSettingsSummary(props.settings, props.mode)}</span>
        </button>
      </PopoverTrigger>
      <PopoverContent align="start" className="mediaSettingsPanel" side="top" sideOffset={10}>
        <section className="mediaSettingsSection">
          <span className="mediaSettingsLabel">选择比例</span>
          <div className="mediaAspectGrid">
            {aspectOptions.map((item) => (
              <button
                type="button"
                key={item.value}
                data-active={props.settings.aspectRatio === item.value}
                onClick={() => patch({ aspectRatio: item.value })}
              >
                <span className="mediaAspectIcon" data-visual={item.visual}>
                  {item.visual === 'smart' && <Sparkles size={16} />}
                </span>
                <strong>{item.label}</strong>
              </button>
            ))}
          </div>
        </section>

        <section className="mediaSettingsSection">
          <span className="mediaSettingsLabel">选择分辨率</span>
          <div className="mediaResolutionSegment">
            {resolutionItems.map((item) => (
              <button
                type="button"
                key={item.value}
                data-active={props.settings.resolution === item.value}
                onClick={() => patch({ resolution: item.value })}
              >
                {item.label}
                {item.value === '4K' && <Sparkles size={15} />}
              </button>
            ))}
          </div>
        </section>

        {isImageMode && capabilities.supportsQualityControl && (
          <section className="mediaSettingsSection">
            <span className="mediaSettingsLabel">生成质量</span>
            <Segmented
              block
              className="mediaAudioSegment"
              options={imageQualityOptions}
              value={props.settings.quality}
              onChange={(value) => patch({ quality: value as ImageQuality })}
            />
          </section>
        )}

        {isVideoMode && (
          <>
            <section className="mediaSettingsSection">
              <span className="mediaSettingsLabel">同时生成声音</span>
              <Segmented
                block
                className="mediaAudioSegment"
                options={[
                  { label: '开启', value: 'on', disabled: !capabilities.supportsAudio },
                  { label: '关闭', value: 'off' },
                ]}
                value={props.settings.outputAudio && capabilities.supportsAudio ? 'on' : 'off'}
                onChange={(value) => patch({ outputAudio: value === 'on' })}
              />
              {!capabilities.supportsAudio && (
                <p className="mediaUnsupportedNote">当前模型不支持输出音频，已自动关闭。</p>
              )}
            </section>

            <section className="mediaSettingsSection">
              <div className="mediaSettingsTitleRow">
                <span className="mediaSettingsLabel">视频时长</span>
                <strong>{selectedDuration}s</strong>
              </div>
              <Slider
                className="mediaDurationSlider"
                disabled={durationOptions.length <= 1}
                marks={durationMarks}
                max={durationMax}
                min={durationMin}
                step={durationStep}
                tooltip={{ formatter: (value) => `${value ?? selectedDuration}s` }}
                value={selectedDuration}
                onChange={(value) => {
                  if (typeof value === 'number') patch({ durationSeconds: value });
                }}
              />
            </section>
          </>
        )}

        {isImageMode && (
          <>
            <section className="mediaSettingsSection">
              <span className="mediaSettingsLabel">尺寸</span>
              <div className="mediaSizeRow">
                <label>
                  <span>W</span>
                  <Input
                    inputMode="numeric"
                    min={128}
                    size="sm"
                    type="number"
                    value={props.settings.width}
                    onChange={(event) => patch({ width: clampNumber(Number(event.target.value), 128, 8192) })}
                  />
                </label>
                <Link2 size={18} />
                <label>
                  <span>H</span>
                  <Input
                    inputMode="numeric"
                    min={128}
                    size="sm"
                    type="number"
                    value={props.settings.height}
                    onChange={(event) => patch({ height: clampNumber(Number(event.target.value), 128, 8192) })}
                  />
                </label>
                <strong>PX</strong>
              </div>
            </section>

            <section className="mediaSettingsSection">
              <span className="mediaSettingsLabel">生成数量</span>
              <div className="mediaOutputModeSegment">
                <button type="button" data-active={props.settings.outputMode === 'single'} onClick={() => patch({ countPreset: 1, outputMode: 'single' })}>
                  <ImageIcon size={15} />
                  单图
                </button>
                <button type="button" disabled={!supportsGroup} data-active={props.settings.outputMode === 'group'} onClick={() => patch({ countPreset: 2, outputMode: 'group' })}>
                  <Images size={15} />
                  {supportsGroup ? '组图' : '不支持组图'}
                </button>
              </div>
              {supportsGroup ? (
                <>
                  <div className="mediaCountGrid">
                    {countOptions.map((item) => (
                      <button
                        type="button"
                        key={String(item.value)}
                        data-active={props.settings.countPreset === item.value}
                        onClick={() => selectCountPreset(item.value)}
                      >
                        {item.value === 'custom' ? item.label : `${item.label}${unit}`}
                      </button>
                    ))}
                  </div>
                  {props.settings.countPreset === 'custom' && props.settings.outputMode === 'group' && (
                    <label className="mediaCustomCount">
                      <span>自定义数量</span>
                      <Input
                        inputMode="numeric"
                        min={2}
                        max={maxCount}
                        size="sm"
                        type="number"
                        value={props.settings.customCount}
                        onChange={(event) => patch({ customCount: clampNumber(Number(event.target.value), 2, maxCount) })}
                      />
                    </label>
                  )}
                </>
              ) : (
                <p className="mediaUnsupportedNote">当前模型不支持组图输出。</p>
              )}
              <p className="mediaSettingsHint">
                <Sparkles size={14} />
                {count} / {unit}
              </p>
            </section>
          </>
        )}
      </PopoverContent>
    </Popover>
  );
}

export function MediaTaskBoard(props: {
  composer: ReactNode;
  message?: string;
  onEditRun?: (run: MediaGenerationRun) => void;
  onRerun?: (run: MediaGenerationRun) => void;
  runs: MediaGenerationRun[];
}) {
  const timelineRef = useRef<HTMLElement>(null);

  useEffect(() => {
    const timeline = timelineRef.current;
    if (!timeline) return;
    timeline.scrollTo({ behavior: 'smooth', top: timeline.scrollHeight });
  }, [props.runs.length]);

  return (
    <div className="mediaTaskPage">
      <section ref={timelineRef} className="mediaTaskTimeline" aria-label="生成任务列表">
        <h1>今天</h1>
        {props.message && <p className="playgroundError">{props.message}</p>}
        {props.runs.map((run) => (
          <MediaTaskCard
            key={run.localId}
            run={run}
            onEditRun={props.onEditRun}
            onRerun={props.onRerun}
          />
        ))}
      </section>
      <div className="mediaComposerDock">
        {props.composer}
      </div>
    </div>
  );
}

function MediaTaskCard(props: {
  run: MediaGenerationRun;
  onEditRun?: (run: MediaGenerationRun) => void;
  onRerun?: (run: MediaGenerationRun) => void;
}) {
  const items = mediaResultItems(props.run);
  const expectedCount = Math.max(mediaOutputCount(props.run.settings), items.length, 1);
  const columns = expectedCount === 1 ? 1 : expectedCount === 2 ? 2 : Math.min(5, expectedCount);
  const style = {
    '--media-grid-columns': columns,
    '--media-grid-max-width': mediaGridMaxWidth(props.run.settings, columns),
    '--media-result-aspect': cssAspectRatio(props.run.settings),
  } as CSSProperties;
  const status = mediaStatusText(props.run);
  const unit = props.run.mode === 'video' ? '条' : '张';
  const isPending = props.run.status === 'submitting' || props.run.status === 'queued' || props.run.status === 'running';
  const backdropItem = expectedCount === 1 && items[0]?.type === 'image' ? items[0] : undefined;
  const errorText = mediaRunErrorText(props.run);
  const references = mediaReferenceItems(props.run);
  const promptParts = promptDisplayParts(props.run.prompt, references);
  const taskMeta = mediaTaskMetaText(props.run);
  const previewState = items.length > 0 ? 'filled' : isPending ? 'loading' : 'empty';

  return (
    <article className="mediaTaskItem" data-status={props.run.status}>
      <header className="mediaTaskHeader">
        <div className="mediaTaskHeaderMain">
          {references.length > 0 && <MediaTaskReferenceStack references={references} />}
          <div className="mediaTaskHeaderContent">
            <div className="mediaTaskMetaLine">
              <Sparkles size={16} />
              <time dateTime={props.run.createdAt}>{formatRunDateTime(props.run.createdAt)}</time>
              <span aria-hidden="true">|</span>
              <strong>{props.run.modelLabel}</strong>
              <span>{props.run.mode === 'video' ? '视频生成' : references.length ? '图像编辑' : '图像生成'}</span>
              <span aria-hidden="true">|</span>
              <span>{taskMeta}</span>
            </div>
            <div className="mediaTaskPromptLine">
              <span className="mediaTaskPromptText">{promptParts}</span>
            </div>
          </div>
        </div>
        <span className="mediaTaskStatus" data-status={props.run.status}>{status}</span>
      </header>

      {errorText && (
        <div className="mediaTaskError">
          <strong>错误详情</strong>
          <span>{errorText}</span>
        </div>
      )}

      <div className="mediaPreviewStage" data-count={expectedCount} data-preview-state={previewState}>
        {backdropItem && <img aria-hidden="true" className="mediaPreviewBackdrop" src={backdropItem.src} alt="" />}
        <div className="mediaGrid" data-count={expectedCount} data-preview-state={previewState} style={style}>
          {Array.from({ length: expectedCount }).map((_, index) => (
            <MediaTile
              expectedCount={expectedCount}
              index={index}
              item={items[index]}
              key={`${props.run.localId}-${index}`}
              mode={props.run.mode}
              status={props.run.status}
            />
          ))}
        </div>
      </div>

      <footer className="mediaTaskActions">
        {items[0] ? (
          <Button asChild size="sm" variant="secondary">
            <a href={items[0].src} download target="_blank" rel="noreferrer">
              <Download size={14} />
              下载
            </a>
          </Button>
        ) : (
          <Button type="button" size="sm" variant="secondary" disabled>
            <Download size={14} />
            下载
          </Button>
        )}
        <Button type="button" size="sm" variant="secondary" onClick={() => props.onEditRun?.(props.run)}>
          <Edit3 size={14} />
          重新编辑
        </Button>
        <Button type="button" size="sm" variant="secondary" disabled={isPending} onClick={() => props.onRerun?.(props.run)}>
          <Sparkles size={14} />
          再次生成
        </Button>
        <span>
          <Sparkles size={14} />
          {expectedCount} / {unit}
        </span>
      </footer>
    </article>
  );
}

function MediaTaskReferenceStack(props: { references: PlaygroundUpload[] }) {
  const visibleReferences = props.references.slice(0, 8);
  const overflowCount = props.references.length - visibleReferences.length;
  return (
    <span
      className="mediaTaskReferenceStack"
      style={{ '--reference-count': Math.max(1, visibleReferences.length) } as CSSProperties}
      title={props.references.map((item) => item.name).join('\n')}
    >
      {visibleReferences.map((item, index) => (
        <span
          className="mediaTaskReferenceCard"
          data-kind={item.kind}
          key={`${item.id}-${index}`}
          style={taskReferenceCardStyle(index)}
        >
          <MediaTaskReferencePreview item={item} />
          <small>{referenceKindLabel(item.kind)}</small>
        </span>
      ))}
      {overflowCount > 0 && (
        <span className="mediaTaskReferenceOverflow">+{overflowCount}</span>
      )}
    </span>
  );
}

function MediaTaskReferencePreview(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 <Music2 size={16} />;
  }
  return <FileText size={16} />;
}

function PromptResourceTag(props: { item: PlaygroundUpload; references: PlaygroundUpload[] }) {
  return (
    <span className="mediaPromptResourceTag" contentEditable={false}>
      <span className="mediaPromptResourceThumb">
        <MediaTaskReferencePreview item={props.item} />
      </span>
      <span>{mediaReferenceMentionLabel(props.item, props.references)}</span>
    </span>
  );
}

function MediaTile(props: {
  expectedCount: number;
  index: number;
  item?: MediaResultItem;
  mode: Exclude<PlaygroundMode, 'chat'>;
  status: MediaGenerationRun['status'];
}) {
  const isLoading = props.status === 'submitting' || props.status === 'queued' || props.status === 'running';
  const isFailed = props.status === 'failed' || props.status === 'cancelled';
  return (
    <div className="mediaTile" data-count={props.expectedCount} data-empty={!props.item && !isLoading} data-kind={props.mode} data-placeholder={!props.item}>
      {props.item?.type === 'video' && (
        <video controls muted playsInline poster={props.item.poster}>
          <source src={props.item.src} />
        </video>
      )}
      {props.item?.type === 'image' && <img src={props.item.src} alt={`生成结果 ${props.index + 1}`} />}
      {isLoading && !props.item && (
        <div className="mediaLoading">
          <LoaderCircle size={24} />
          <span>{props.mode === 'video' ? '视频生成中' : '图片生成中'}</span>
        </div>
      )}
      {isFailed && !props.item && (
        <div className="mediaEmptyTile">
          <span>生成失败</span>
        </div>
      )}
      {!isLoading && !isFailed && !props.item && (
        <div className="mediaEmptyTile">
          <span>暂无结果</span>
        </div>
      )}
    </div>
  );
}

function mediaResultItems(run: MediaGenerationRun): MediaResultItem[] {
  const data = arrayFromUnknown(run.task?.result?.data);
  return data
    .map((entry) => mediaResultItemFromEntry(entry, run.mode))
    .filter((item): item is MediaResultItem => Boolean(item));
}

function mediaRunErrorText(run: MediaGenerationRun) {
  return gatewayTaskErrorText(run.task, '') || run.error || '';
}

const mediaResourceTokenPattern = /<<<playground-resource:([^>]+)>>>/g;
const taskReferenceTiltValues = [-10, 8, -5, 9, -7, 5, -8, 6];
const taskReferenceYValues = [0, 3, -1, 2, -2, 4, 1, -3];

function promptDisplayParts(raw: string, references: PlaygroundUpload[]): ReactNode[] {
  if (!raw.includes('<<<playground-resource:')) return [raw];
  const parts: ReactNode[] = [];
  const byId = new Map(references.map((item) => [item.id, item]));
  const usedIds = new Set<string>();
  let fallbackIndex = 0;
  let lastIndex = 0;
  let match: RegExpExecArray | null;
  const re = new RegExp(mediaResourceTokenPattern);
  while ((match = re.exec(raw)) !== null) {
    if (match.index > lastIndex) {
      parts.push(raw.slice(lastIndex, match.index));
    }
    const id = match[1] ?? '';
    let item = byId.get(id);
    if (item) {
      usedIds.add(item.id);
    } else {
      while (fallbackIndex < references.length && usedIds.has(references[fallbackIndex]!.id)) {
        fallbackIndex += 1;
      }
      item = references[fallbackIndex];
      fallbackIndex += 1;
      if (item) usedIds.add(item.id);
    }
    if (item) {
      parts.push(<PromptResourceTag item={item} references={references} key={`${match.index}-${item.id}`} />);
    } else {
      parts.push('@资产');
    }
    lastIndex = match.index + (match[0]?.length ?? 0);
  }
  if (lastIndex < raw.length) {
    parts.push(raw.slice(lastIndex));
  }
  return parts;
}

function taskReferenceCardStyle(index: number) {
  const valueIndex = index % taskReferenceTiltValues.length;
  return {
    '--reference-index': index,
    '--reference-tilt': `${taskReferenceTiltValues[valueIndex]}deg`,
    '--reference-y': `${taskReferenceYValues[valueIndex]}px`,
  } as CSSProperties;
}

function mediaReferenceMentionLabel(item: PlaygroundUpload, references: PlaygroundUpload[]) {
  return `@${promptReferenceKindLabel(item.kind)}${referenceKindIndex(item, references)}`;
}

function referenceKindIndex(item: PlaygroundUpload, references: PlaygroundUpload[]) {
  const sameKind = references.filter((reference) => reference.kind === item.kind);
  return Math.max(1, sameKind.findIndex((reference) => reference.id === item.id) + 1);
}

function mediaReferenceItems(run: MediaGenerationRun): PlaygroundUpload[] {
  const uploads = normalizeReferenceUploads(run.uploads);
  if (uploads.length) return uploads;
  return referencesFromTaskRequest(run.task?.request, run.mode);
}

function normalizeReferenceUploads(value: unknown): PlaygroundUpload[] {
  if (!Array.isArray(value)) return [];
  return value
    .map((item, index) => normalizeReferenceUpload(item, index))
    .filter((item): item is PlaygroundUpload => Boolean(item));
}

function normalizeReferenceUpload(value: unknown, index: number): PlaygroundUpload | undefined {
  const record = recordFromUnknown(value);
  if (!record) return undefined;
  const url = stringFromUnknown(record.url);
  const kind = referenceKindFromUnknown(record.kind, url);
  if (!url || !kind) return undefined;
  const size = numberFromUnknown(record.size);
  return {
    contentType: stringFromUnknown(record.contentType),
    id: stringFromUnknown(record.id) || `${kind}-${index}-${url}`,
    kind,
    name: stringFromUnknown(record.name) || `${referenceKindLabel(kind)} ${index + 1}`,
    raw: recordFromUnknown(record.raw) ?? {},
    role: record.role === 'first_frame' || record.role === 'last_frame' ? record.role : undefined,
    size: size && size > 0 ? Math.round(size) : 0,
    url,
  };
}

function referencesFromTaskRequest(request: unknown, mode: Exclude<PlaygroundMode, 'chat'>): PlaygroundUpload[] {
  const record = recordFromUnknown(request);
  if (!record) return [];
  const references: PlaygroundUpload[] = [];
  if (Array.isArray(record.content)) {
    record.content.forEach((item) => {
      const content = recordFromUnknown(item);
      if (!content) return;
      appendReferenceFromContentPart(references, content);
    });
  }
  if (mode === 'image') {
    appendImageReferencesFromValue(references, record.image);
    appendImageReferencesFromValue(references, record.images);
    appendImageReferencesFromValue(references, record.input_image);
    appendImageReferencesFromValue(references, record.input_images);
  }
  return dedupeReferenceUploads(references);
}

function appendReferenceFromContentPart(references: PlaygroundUpload[], part: Record<string, unknown>) {
  const type = stringFromUnknown(part.type);
  if (type === 'image_url') {
    appendReferenceUrl(references, 'image', firstString(nestedString(part.image_url, 'url'), part.url));
    return;
  }
  if (type === 'video_url') {
    appendReferenceUrl(references, 'video', firstString(nestedString(part.video_url, 'url'), part.url));
    return;
  }
  if (type === 'audio_url') {
    appendReferenceUrl(references, 'audio', firstString(nestedString(part.audio_url, 'url'), part.url));
  }
}

function appendImageReferencesFromValue(references: PlaygroundUpload[], value: unknown) {
  if (!value) return;
  if (typeof value === 'string') {
    appendReferenceUrl(references, 'image', value);
    return;
  }
  if (Array.isArray(value)) {
    value.forEach((item) => appendImageReferencesFromValue(references, item));
    return;
  }
  const record = recordFromUnknown(value);
  if (!record) return;
  appendReferenceUrl(references, 'image', firstString(record.url, nestedString(record.image_url, 'url'), record.image_url, record.imageUrl, record.path));
}

function appendReferenceUrl(references: PlaygroundUpload[], kind: PlaygroundUploadKind, rawUrl: unknown) {
  const url = stringFromUnknown(rawUrl);
  if (!url) return;
  const resolvedUrl = resolveApiAssetUrl(url);
  references.push({
    contentType: '',
    id: `${kind}-${references.length}-${resolvedUrl}`,
    kind,
    name: `${referenceKindLabel(kind)} ${references.filter((item) => item.kind === kind).length + 1}`,
    raw: {},
    size: 0,
    url: resolvedUrl,
  });
}

function dedupeReferenceUploads(references: PlaygroundUpload[]) {
  const seen = new Set<string>();
  return references.filter((item) => {
    const key = `${item.kind}:${item.url}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

function referenceKindFromUnknown(value: unknown, url: string): PlaygroundUploadKind | undefined {
  if (value === 'image' || value === 'video' || value === 'audio' || value === 'file') return value;
  if (/\.(png|jpe?g|webp|gif|bmp|avif|svg)(\?|#|$)/i.test(url)) return 'image';
  if (/\.(mp4|mov|webm|m4v|avi|mkv)(\?|#|$)/i.test(url)) return 'video';
  if (/\.(mp3|m4a|wav|aac|flac|ogg|opus)(\?|#|$)/i.test(url)) return 'audio';
  return undefined;
}

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

function promptReferenceKindLabel(kind: PlaygroundUploadKind) {
  if (kind === 'image') return '图片';
  return referenceKindLabel(kind);
}

function mediaResultItemFromEntry(entry: unknown, mode: Exclude<PlaygroundMode, 'chat'>): MediaResultItem | undefined {
  const record = recordFromUnknown(entry);
  if (!record) return undefined;
  const b64 = stringFromUnknown(record.b64_json);
  if (b64) return { src: `data:image/png;base64,${b64}`, type: 'image' };
  const videoUrl = firstString(record.video_url, nestedString(record.video_url, 'url'), record.videoUrl, record.output_video_url);
  if (videoUrl) return { src: resolveApiAssetUrl(videoUrl), type: 'video' };
  const imageUrl = firstString(record.url, record.image_url, nestedString(record.image_url, 'url'), record.output_url);
  if (!imageUrl) return undefined;
  const normalized = resolveApiAssetUrl(imageUrl);
  const type = mode === 'video' || /\.(mp4|mov|webm|m4v)(\?|#|$)/i.test(normalized) ? 'video' : 'image';
  return { src: normalized, type };
}

function mediaSettingsSummary(settings: MediaGenerationSettings, mode: Exclude<PlaygroundMode, 'chat'>) {
  const resolutionLabel = resolutionOptionsForMode(mode).find((item) => item.value === settings.resolution)?.label ?? settings.resolution;
  if (mode === 'video') return `${settings.aspectRatio} | ${resolutionLabel} | ${settings.durationSeconds}s | ${settings.outputAudio ? '有声音' : '无声音'}`;
  const count = mediaOutputCount(settings);
  const modeLabel = settings.outputMode === 'single' ? '单图' : '组图';
  return `${settings.aspectRatio} | ${resolutionLabel} | ${settings.width}x${settings.height} | ${modeLabel} ${count}张`;
}

function mediaStatusText(run: MediaGenerationRun) {
  if (run.status === 'submitting') return '提交中';
  if (run.status === 'queued') return '排队中';
  if (run.status === 'running') return '生成中';
  if (run.status === 'succeeded') return '已完成';
  if (run.status === 'failed') return '失败';
  if (run.status === 'cancelled') return '已取消';
  return run.status;
}

function formatRunDateTime(value: string) {
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return value;
  const pad = (item: number) => String(item).padStart(2, '0');
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}

function mediaTaskMetaText(run: MediaGenerationRun) {
  const items = [run.settings.aspectRatio, run.settings.resolution];
  if (run.mode === 'video') {
    items.push(`${run.settings.durationSeconds}s`);
    if (run.settings.outputAudio) items.push('有声音');
  } else {
    const count = mediaOutputCount(run.settings);
    if (count > 1) items.push(`${count}张`);
  }
  return items.filter(Boolean).join(' | ');
}

function cssAspectRatio(settings: MediaGenerationSettings) {
  const [aspectWidth, aspectHeight] = settings.aspectRatio.split(':').map((item) => Number(item));
  if (Number.isFinite(aspectWidth) && Number.isFinite(aspectHeight) && aspectWidth > 0 && aspectHeight > 0) {
    return `${aspectWidth} / ${aspectHeight}`;
  }
  return `${settings.width || 1} / ${settings.height || 1}`;
}

function mediaGridMaxWidth(settings: MediaGenerationSettings, columns: number) {
  const ratio = numericAspectRatio(settings);
  const safeColumns = Math.max(1, columns);
  const maxWidth = (mediaPreviewMaxHeight * ratio * safeColumns) + (mediaGridGap * (safeColumns - 1));
  return `${Math.max(120, Math.ceil(maxWidth))}px`;
}

function numericAspectRatio(settings: MediaGenerationSettings) {
  const [aspectWidth, aspectHeight] = settings.aspectRatio.split(':').map((item) => Number(item));
  if (Number.isFinite(aspectWidth) && Number.isFinite(aspectHeight) && aspectWidth > 0 && aspectHeight > 0) {
    return aspectWidth / aspectHeight;
  }
  const width = Number(settings.width);
  const height = Number(settings.height);
  if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
    return width / height;
  }
  return 1;
}

function deriveSingleMediaModelCapabilities(
  model: PlatformModel,
  mode: Exclude<PlaygroundMode, 'chat'>,
  contextKey?: string,
  resolution?: string,
): MediaModelCapabilities {
  const source = mergeCapabilityRecords(model.capabilities, model.capabilityOverride);
  const typeKeys = capabilityTypeKeys(model, source, mode, contextKey);
  const defaultCapabilities = defaultMediaModelCapabilities(mode);
  const durationScopes = [resolution, contextKey];
  const resolutionValues = normalizeResolutionValues(stringListFromCapability(firstCapabilityValue(source, typeKeys, ['output_resolutions']), [contextKey]));
  const allowedAspectValues = normalizeAspectRatioValues(stringListFromCapability(firstCapabilityValue(source, typeKeys, ['aspect_ratio_allowed']), [contextKey]));
  const ratioRange = ratioRangeFromValue(firstCapabilityValue(source, typeKeys, ['aspect_ratio_range']));
  const rangedAspectValues = ratioRange ? aspectRatioOptions
    .filter((item) => item.value === 'auto' || aspectRatioWithinRange(item.value, ratioRange))
    .map((item) => item.value) : [];
  const aspectRatios = allowedAspectValues.length && rangedAspectValues.length
    ? allowedAspectValues.filter((item) => rangedAspectValues.includes(item))
    : allowedAspectValues.length ? allowedAspectValues : rangedAspectValues;
  const maxCountValue = numberFromUnknown(firstCapabilityValue(source, typeKeys, countCapabilityKeys(mode)));
  const explicitGroupSupport = boolFromUnknown(firstCapabilityValue(source, typeKeys, groupCapabilityKeys(mode)));
  const durationRange = durationRangeFromValue(scopedCapabilityValue(firstCapabilityValue(source, typeKeys, ['duration_range']), durationScopes)) ?? defaultCapabilities.durationRange;
  const durationStep = durationStepFromValue(scopedCapabilityValue(firstCapabilityValue(source, typeKeys, ['duration_step']), durationScopes), defaultCapabilities.durationStep);
  const durationOptions = normalizeDurationValues(numberListFromCapability(scopedCapabilityValue(firstCapabilityValue(source, typeKeys, ['duration_options']), durationScopes)));
  const explicitAudioSupport = boolFromUnknown(firstCapabilityValue(source, typeKeys, ['output_audio']));
  const explicitQualitySupport = boolFromUnknown(firstCapabilityValue(source, typeKeys, ['support_quality_control', 'supportQualityControl', 'quality_control', 'qualityControl', 'quality']));
  const maxCount = explicitGroupSupport === false ? 1 : clampNumber(maxCountValue ?? defaultCapabilities.maxCount, 1, 20);
  const supportsGroup = explicitGroupSupport === false ? false : maxCount > 1;

  return {
    aspectRatios: aspectRatios.length ? aspectRatios : defaultCapabilities.aspectRatios,
    durationOptions,
    durationRange,
    durationStep,
    maxCount,
    resolutions: resolutionValues.length ? resolutionValues : defaultCapabilities.resolutions,
    supportsAudio: explicitAudioSupport ?? defaultCapabilities.supportsAudio,
    supportsGroup,
    supportsQualityControl: explicitQualitySupport ?? defaultCapabilities.supportsQualityControl,
  };
}

function defaultMediaModelCapabilities(mode: Exclude<PlaygroundMode, 'chat'>): MediaModelCapabilities {
  const isVideo = mode === 'video';
  return {
    aspectRatios: aspectRatioOptions.map((item) => item.value),
    durationOptions: [],
    durationRange: isVideo ? [5, 10] : [1, 1],
    durationStep: 1,
    maxCount: 20,
    resolutions: resolutionOptionsForMode(mode).map((item) => item.value),
    supportsAudio: false,
    supportsGroup: mode === 'image',
    supportsQualityControl: false,
  };
}

function filterAspectRatioOptions(capabilities: MediaModelCapabilities) {
  const allowed = new Set(capabilities.aspectRatios);
  const items = aspectRatioOptions.filter((item) => allowed.has(item.value));
  return items.length ? items : aspectRatioOptions;
}

function filterResolutionOptions(capabilities: MediaModelCapabilities, mode: Exclude<PlaygroundMode, 'chat'>) {
  const allowed = new Set(capabilities.resolutions);
  const modeOptions = resolutionOptionsForMode(mode);
  const items = modeOptions.filter((item) => allowed.has(item.value));
  return items.length ? items : modeOptions;
}

function durationValuesForCapabilities(capabilities: MediaModelCapabilities) {
  const [min, max] = capabilities.durationRange;
  const step = Math.max(1, capabilities.durationStep);
  const options = capabilities.durationOptions.length
    ? capabilities.durationOptions
    : expandDurationRange(min, max, step);
  const filtered = options.filter((value) => (
    value >= min
    && value <= max
    && durationMatchesStep(value, min, step)
  ));
  return filtered.length ? filtered : [min];
}

function expandDurationRange(min: number, max: number, step: number) {
  const values: number[] = [];
  for (let value = min; value <= max; value += step) {
    values.push(value);
  }
  if (!values.includes(max)) values.push(max);
  return normalizeDurationValues(values);
}

function durationMatchesStep(value: number, min: number, step: number) {
  if (step <= 1) return true;
  const ratio = (value - min) / step;
  return Math.abs(ratio - Math.round(ratio)) < 0.000001;
}

function closestDurationValue(value: number, values: number[]) {
  if (!values.length) return value;
  return values.reduce((closest, item) => (
    Math.abs(item - value) < Math.abs(closest - value) ? item : closest
  ), values[0]);
}

function durationSliderMarks(values: number[], showAll: boolean) {
  const markValues = showAll || values.length <= 16
    ? values
    : uniqueNumberValues([values[0], values[values.length - 1]]);
  return Object.fromEntries(markValues.map((value) => [value, `${value}s`]));
}

function durationRangeFromValues(values: number[]): [number, number] {
  const normalized = normalizeDurationValues(values);
  if (!normalized.length) return [5, 10];
  return [normalized[0], normalized[normalized.length - 1]];
}

function resolutionOptionsForMode(mode: Exclude<PlaygroundMode, 'chat'>) {
  return resolutionOptions.filter((item) => item.modes.includes(mode));
}

function intersectOptionValues(values: string[][], fallback: string[]) {
  const nonEmptyValues = values.filter((items) => items.length > 0);
  if (!nonEmptyValues.length) return fallback;
  const intersection = fallback.filter((item) => nonEmptyValues.every((items) => items.includes(item)));
  return intersection.length ? intersection : nonEmptyValues[0];
}

function intersectNumberValues(values: number[][], fallback: number[]) {
  const nonEmptyValues = values.filter((items) => items.length > 0);
  if (!nonEmptyValues.length) return fallback;
  const intersection = fallback.filter((item) => nonEmptyValues.every((items) => items.includes(item)));
  return intersection.length ? intersection : nonEmptyValues[0];
}

function capabilityTypeKeys(
  model: PlatformModel,
  source: Record<string, unknown>,
  mode: Exclude<PlaygroundMode, 'chat'>,
  contextKey?: string,
) {
  const modeTypeHints = mode === 'image'
    ? ['image_generate', 'image_edit', 'images.generations', 'images.edits', 'image']
    : [contextKey, 'text_to_video', 'image_to_video', 'omni_video', 'video_generate', 'video'];
  return uniqueStrings([
    ...stringListFromCapability(source.originalTypes),
    ...model.modelType,
    ...modeTypeHints.filter((item): item is string => Boolean(item)),
  ]);
}

function firstCapabilityValue(source: Record<string, unknown>, typeKeys: string[], keys: string[]) {
  for (const key of keys) {
    const value = nestedCapabilityValue(source, typeKeys, key);
    if (hasCapabilityValue(value)) return value;
  }
  return undefined;
}

function nestedCapabilityValue(source: Record<string, unknown>, typeKeys: string[], key: string) {
  for (const type of typeKeys) {
    const config = recordFromUnknown(source[type]);
    if (config && key in config) return config[key];
  }
  if (key in source) return source[key];
  for (const value of Object.values(source)) {
    const config = recordFromUnknown(value);
    if (config && key in config) return config[key];
  }
  return undefined;
}

function scopedCapabilityValue(value: unknown, scopes: Array<string | undefined>): unknown {
  const record = recordFromUnknown(value);
  if (!record) return value;
  const scopeKeys = uniqueStrings(scopes.filter((item): item is string => Boolean(item)));
  for (const scope of scopeKeys) {
    const scoped = record[scope];
    if (hasCapabilityValue(scoped)) {
      return scopedCapabilityValue(scoped, scopeKeys.filter((item) => item !== scope));
    }
  }
  for (const scope of ['default', 'all', '*']) {
    const scoped = record[scope];
    if (hasCapabilityValue(scoped)) return scoped;
  }
  return value;
}

function groupCapabilityKeys(mode: Exclude<PlaygroundMode, 'chat'>) {
  return mode === 'image'
    ? ['output_multiple_images', 'multiple_images', 'support_multiple_images', 'supports_group']
    : ['output_multiple_videos', 'multiple_videos', 'support_multiple_videos', 'supports_group'];
}

function countCapabilityKeys(mode: Exclude<PlaygroundMode, 'chat'>) {
  return mode === 'image'
    ? ['output_max_images_count', 'max_images', 'max_outputs', 'output_max_count']
    : ['output_max_videos_count', 'max_videos', 'max_outputs', 'output_max_count'];
}

function mergeCapabilityRecords(...records: Array<Record<string, unknown> | undefined>) {
  return records.reduce<Record<string, unknown>>((acc, record) => {
    if (!record) return acc;
    Object.entries(record).forEach(([key, value]) => {
      const current = recordFromUnknown(acc[key]);
      const next = recordFromUnknown(value);
      acc[key] = current && next ? { ...current, ...next } : value;
    });
    return acc;
  }, {});
}

function stringListFromCapability(value: unknown, preferredKeys: Array<string | undefined> = []): string[] {
  if (Array.isArray(value)) return value.map(String).map((item) => item.trim()).filter(Boolean);
  if (typeof value === 'string') return value.split(/[,，\s]+/).map((item) => item.trim()).filter(Boolean);
  const record = recordFromUnknown(value);
  if (!record) return [];
  const preferred = preferredKeys
    .filter((key): key is string => Boolean(key))
    .flatMap((key) => stringListFromCapability(record[key]));
  if (preferred.length) return preferred;
  return Object.values(record).flatMap((item) => stringListFromCapability(item));
}

function numberListFromCapability(value: unknown): number[] {
  if (Array.isArray(value)) return value.flatMap((item) => numberListFromCapability(item));
  if (typeof value === 'number') return [value];
  if (typeof value === 'string') return value.match(/-?\d+(?:\.\d+)?/g)?.map(Number).filter(Number.isFinite) ?? [];
  const record = recordFromUnknown(value);
  if (!record) return [];
  return Object.values(record).flatMap((item) => numberListFromCapability(item));
}

function normalizeAspectRatioValues(values: string[]) {
  const allowedValues = new Set(aspectRatioOptions.map((item) => item.value));
  return uniqueStrings(values.map((value) => {
    const normalized = value.toLowerCase().replace(/[×x]/g, ':').replace(/\s+/g, '');
    if (['auto', 'smart', 'adaptive', '智能'].includes(normalized)) return 'auto';
    return normalized;
  })).filter((value) => allowedValues.has(value));
}

function normalizeResolutionValues(values: string[]) {
  const allowedValues = new Set(resolutionOptions.map((item) => item.value));
  return uniqueStrings(values.map((value) => {
    const normalized = value.trim().toLowerCase();
    if (normalized.includes('2160')) return '2160p';
    if (normalized.includes('1080')) return '1080p';
    if (normalized.includes('720')) return '720p';
    if (normalized.includes('480')) return '480p';
    if (normalized.includes('4k') || normalized.includes('4096')) return '4K';
    if (normalized.includes('2k') || normalized.includes('2048')) return '2K';
    if (normalized.includes('1k') || normalized.includes('1024')) return '1K';
    return value.trim();
  })).filter((value) => allowedValues.has(value));
}

function ratioRangeFromValue(value: unknown): [number, number] | undefined {
  if (Array.isArray(value) && value.length >= 2) {
    const min = Number(value[0]);
    const max = Number(value[1]);
    if (Number.isFinite(min) && Number.isFinite(max)) return [Math.min(min, max), Math.max(min, max)];
  }
  if (typeof value === 'string') {
    const numbers = value.match(/-?\d+(?:\.\d+)?/g)?.map(Number) ?? [];
    if (numbers.length >= 2 && numbers.every(Number.isFinite)) return [Math.min(numbers[0], numbers[1]), Math.max(numbers[0], numbers[1])];
  }
  const record = recordFromUnknown(value);
  if (!record) return undefined;
  for (const item of Object.values(record)) {
    const nested = ratioRangeFromValue(item);
    if (nested) return nested;
  }
  return undefined;
}

function durationRangeFromValue(value: unknown): [number, number] | undefined {
  const range = ratioRangeFromValue(value);
  if (!range) return undefined;
  const min = Math.max(1, Math.round(range[0]));
  const max = Math.max(1, Math.round(range[1]));
  return [Math.min(min, max), Math.max(min, max)];
}

function aspectRatioWithinRange(value: string, range: [number, number]) {
  const [width, height] = value.split(':').map(Number);
  if (!Number.isFinite(width) || !Number.isFinite(height) || height <= 0) return false;
  const ratio = width / height;
  return ratio >= range[0] && ratio <= range[1];
}

function hasCapabilityValue(value: unknown) {
  if (value === undefined || value === null || value === '') return false;
  if (Array.isArray(value)) return value.length > 0;
  return true;
}

function boolFromUnknown(value: unknown) {
  if (value === true || value === 'true') return true;
  if (value === false || value === 'false') return false;
  return undefined;
}

function numberFromUnknown(value: unknown) {
  if (typeof value === 'number' && Number.isFinite(value)) return value;
  if (typeof value === 'string' && value.trim()) {
    const parsed = Number(value);
    if (Number.isFinite(parsed)) return parsed;
  }
  return undefined;
}

function durationStepFromValue(value: unknown, fallback: number) {
  const parsed = numberFromUnknown(value);
  if (!parsed || parsed <= 0) return fallback;
  return Math.max(1, Math.round(parsed));
}

function normalizeDurationValues(values: number[]) {
  return uniqueNumberValues(values
    .filter((item) => Number.isFinite(item) && item > 0)
    .map((item) => Math.round(item)))
    .sort((left, right) => left - right);
}

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

function uniqueStrings(values: string[]) {
  return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean)));
}

function mediaSettingsEqual(left: MediaGenerationSettings, right: MediaGenerationSettings) {
  return left.aspectRatio === right.aspectRatio
    && left.countPreset === right.countPreset
    && left.customCount === right.customCount
    && left.durationSeconds === right.durationSeconds
    && left.height === right.height
    && left.outputMode === right.outputMode
    && left.outputAudio === right.outputAudio
    && left.quality === right.quality
    && left.resolution === right.resolution
    && left.width === right.width;
}

function firstString(...values: unknown[]) {
  return values.map(stringFromUnknown).find(Boolean) ?? '';
}

function nestedString(value: unknown, key: string) {
  return stringFromUnknown(recordFromUnknown(value)?.[key]);
}

function stringFromUnknown(value: unknown) {
  return typeof value === 'string' ? value.trim() : '';
}

function arrayFromUnknown(value: unknown) {
  return Array.isArray(value) ? value : [];
}

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 clampNumber(value: number, min: number, max: number) {
  if (!Number.isFinite(value)) return min;
  return Math.min(max, Math.max(min, Math.round(value)));
}

interface MediaResultItem {
  poster?: string;
  src: string;
  type: 'image' | 'video';
}
