import {
  useId,
  useRef,
  useState,
  type ChangeEvent,
  type KeyboardEvent,
} from 'react';
import {
  EDITABLE_STREAM_DISPOSITIONS,
  LOUDNESS_PROFILES,
  classifySubtitleCodec,
  createChaptersAtIntervals,
  createChaptersFromMarkers,
  parseChapterJson,
  parseMetadataJson,
  rangesForSplit,
  serializeChapterJson,
  serializeMetadataJson,
  validateSplitMarkers,
  type EditableChapter,
  type ImageFormat,
  type LoudnessMeasurement,
  type LoudnessTargets,
  type MetadataEdits,
  type StreamDisposition,
  type SplitDefinition,
  type SubtitleFormat,
  type SubtitleStreamDescription,
  type TimeRange,
} from '../commands';
import {
  GENERATED_OUTPUT_LIMIT_EXPLANATION,
  MAX_GENERATED_OUTPUT_FILES,
} from '../limits';
import {
  MAX_SCENE_CHANGE_FRAMES,
  MAX_SCENE_CHANGE_THRESHOLD,
  MIN_SCENE_CHANGE_THRESHOLD,
} from '../commands/thumbnails';
import {
  parseUserPresetJson,
  serializeUserPreset,
  validateUserPreset,
  type AudioCodec,
  type ChapterPolicy,
  type MetadataPolicy,
  type PresetKind,
  type QualityMode,
  type SubtitlePolicy,
  type UserExportPreset,
  type VideoCodec,
} from '../presets';
import './AdvancedOperations.css';

export type AdvancedCapabilityKey =
  | 'split-fast'
  | 'split-accurate'
  | 'waveform-static'
  | 'waveform-peaks'
  | 'loudness-measure'
  | 'peak-normalize'
  | 'loudness-normalize'
  | 'thumbnail-series'
  | 'contact-sheet'
  | 'contact-sheet-labels'
  | 'scene-change-analysis'
  | 'metadata-write'
  | 'chapter-write'
  | 'subtitle-extract'
  | 'subtitle-convert'
  | 'subtitle-mux'
  | 'subtitle-burn'
  | 'preset-storage';

export interface AdvancedCapabilityStatus {
  readonly available: boolean;
  readonly reason?: string;
}

export interface AdvancedSourceContext {
  readonly sourceId?: string;
  readonly durationSeconds?: number;
  readonly sourceExtension: string;
  readonly hasAudio: boolean;
  readonly hasVideo: boolean;
  readonly selectedAudioStreamIndex?: number;
}

export interface MetadataStreamDescription {
  readonly index: number;
  readonly type: string;
  readonly codec: string;
  readonly language?: string;
  readonly title?: string;
  readonly disposition: Readonly<Record<string, boolean>>;
}

export interface AdvancedExportPresetOption {
  readonly id: string;
  readonly name: string;
  readonly fileExtension: string;
  readonly supportsAudio: boolean;
  readonly supportsVideo: boolean;
  readonly disabledReason?: string;
}

export type AdvancedSplitRequest =
  | {
      readonly mode: 'fast';
      readonly definition: SplitDefinition;
      readonly targetExtension: string;
    }
  | {
      readonly mode: 'accurate';
      readonly definition: SplitDefinition;
      readonly targetExtension: string;
      readonly presetId: string;
    };

export type AdvancedSplitSegmentState =
  | 'queued'
  | 'running'
  | 'completed'
  | 'verified'
  | 'warning'
  | 'failed'
  | 'cancelled';

export interface AdvancedSplitSegmentStatus {
  readonly outputId: string;
  readonly fileName: string;
  readonly startSeconds: number;
  readonly endSeconds: number;
  readonly state: AdvancedSplitSegmentState;
  readonly message?: string;
}

export interface AdvancedSplitBatchStatus {
  readonly planId: string;
  readonly segments: readonly AdvancedSplitSegmentStatus[];
}

export interface StaticWaveformRequest {
  readonly type: 'static-waveform';
  readonly audioStreamIndex?: number;
  readonly width: number;
  readonly height: number;
  readonly foreground: string;
  readonly background: string;
}

export interface WaveformPeaksRequest {
  readonly type: 'waveform-peaks';
  readonly audioStreamIndex?: number;
  readonly sampleRate: number;
  readonly bucketCount: number;
}

export interface LoudnessMeasurementRequest {
  readonly type: 'loudness-measurement';
  readonly audioStreamIndex?: number;
  readonly targets: LoudnessTargets;
}

export interface PeakNormalizationRequest {
  readonly type: 'peak-normalization';
  readonly audioStreamIndex?: number;
  readonly targetPeakDb: number;
  readonly presetId: string;
}

export interface LoudnessNormalizationRequest {
  readonly type: 'loudness-normalization';
  readonly audioStreamIndex?: number;
  readonly targets: LoudnessTargets;
  readonly measurement: LoudnessMeasurement;
  readonly presetId: string;
}

export type AdvancedAudioRequest =
  | StaticWaveformRequest
  | WaveformPeaksRequest
  | LoudnessMeasurementRequest
  | PeakNormalizationRequest
  | LoudnessNormalizationRequest;

export type ThumbnailDistribution =
  | { readonly type: 'evenly-spaced'; readonly count: number }
  | { readonly type: 'fixed-interval'; readonly intervalSeconds: number }
  | {
      readonly type: 'scene-changes';
      readonly count: number;
      readonly threshold: number;
    }
  | {
      readonly type: 'chapter-starts' | 'split-markers';
      readonly timesSeconds: readonly number[];
    };

export interface ThumbnailSeriesRequest {
  readonly distribution: ThumbnailDistribution;
  readonly width?: number;
  readonly height?: number;
  readonly format: ImageFormat;
  readonly quality: number;
}

export interface ContactSheetRequest {
  readonly frameCount: number;
  readonly rows: number;
  readonly columns: number;
  readonly cellWidth: number;
  readonly spacing: number;
  readonly background: string;
  readonly maintainAspect: boolean;
  readonly timestampLabels: boolean;
  readonly sourceLabel: boolean;
  readonly format: ImageFormat;
  readonly quality: number;
  readonly selection:
    | { readonly type: 'evenly-spaced' }
    | { readonly type: 'scene-changes'; readonly threshold: number };
}

export type MetadataExecutionRequest =
  | {
      readonly type: 'apply';
      readonly edits: MetadataEdits;
      readonly sourceMetadataPolicy: 'copy' | 'remove';
      readonly chapterPolicy: 'keep' | 'remove';
      readonly coverArtPolicy: 'keep' | 'remove';
    }
  | {
      readonly type: 'import';
      readonly edits: MetadataEdits;
    }
  | {
      readonly type: 'export';
      readonly edits: MetadataEdits;
      readonly json: string;
    };

export type ChapterExecutionRequest =
  | {
      readonly type: 'replace';
      readonly chapters: readonly EditableChapter[];
    }
  | { readonly type: 'remove' }
  | {
      readonly type: 'import';
      readonly chapters: readonly EditableChapter[];
    }
  | {
      readonly type: 'export';
      readonly chapters: readonly EditableChapter[];
      readonly json: string;
    };

export type SubtitleExecutionRequest =
  | {
      readonly type: 'extract';
      readonly stream: SubtitleStreamDescription;
      readonly outputFormat: SubtitleFormat;
    }
  | {
      readonly type: 'convert';
      readonly file: File;
      readonly inputFormat: SubtitleFormat;
      readonly outputFormat: SubtitleFormat;
      readonly offsetSeconds: number;
    }
  | {
      readonly type: 'soft-mux';
      readonly file: File;
      readonly inputFormat: SubtitleFormat;
      readonly subtitleCodec: string;
      readonly targetContainer: 'mp4' | 'webm' | 'matroska';
      readonly language?: string;
      readonly title?: string;
      readonly default: boolean;
      readonly forced: boolean;
    }
  | {
      readonly type: 'burn-in';
      readonly file: File;
      readonly inputFormat: SubtitleFormat;
      readonly presetId: string;
    };

export type UserPresetAction =
  | { readonly type: 'create'; readonly preset: UserExportPreset }
  | { readonly type: 'update'; readonly preset: UserExportPreset }
  | {
      readonly type: 'clone';
      readonly presetId: string;
      readonly newId: string;
      readonly newName: string;
    }
  | {
      readonly type: 'rename';
      readonly presetId: string;
      readonly name: string;
    }
  | { readonly type: 'delete'; readonly presetId: string }
  | { readonly type: 'reset-all' }
  | {
      readonly type: 'export';
      readonly preset: UserExportPreset;
      readonly json: string;
    }
  | { readonly type: 'import'; readonly preset: UserExportPreset };

type RequestHandler<T> = (request: T) => void | Promise<void>;

export interface AdvancedOperationsProps {
  readonly source?: AdvancedSourceContext;
  readonly splitMarkers?: readonly number[];
  readonly splitBatch?: AdvancedSplitBatchStatus;
  readonly chapters?: readonly EditableChapter[];
  readonly timelineClips?: readonly TimeRange[];
  readonly metadataStreams?: readonly MetadataStreamDescription[];
  readonly metadataFormatTags?: Readonly<Record<string, string>>;
  readonly subtitleStreams?: readonly SubtitleStreamDescription[];
  readonly loudnessMeasurement?: LoudnessMeasurement;
  readonly exportPresets?: readonly AdvancedExportPresetOption[];
  readonly userPresets?: readonly UserExportPreset[];
  /**
   * Missing entries are intentionally treated as unavailable. The integration
   * layer must opt in only after checking the loaded FFmpeg core and browser.
   */
  readonly availability?: Partial<
    Readonly<Record<AdvancedCapabilityKey, AdvancedCapabilityStatus>>
  >;
  readonly imageFormatAvailability?: Partial<
    Readonly<Record<ImageFormat, AdvancedCapabilityStatus>>
  >;
  readonly subtitleFormatAvailability?: Partial<
    Readonly<Record<SubtitleFormat, AdvancedCapabilityStatus>>
  >;
  readonly subtitleContainerAvailability?: Partial<
    Readonly<Record<'mp4' | 'webm' | 'matroska', AdvancedCapabilityStatus>>
  >;
  readonly busy?: boolean;
  readonly onSplit?: RequestHandler<AdvancedSplitRequest>;
  readonly onAudioAction?: RequestHandler<AdvancedAudioRequest>;
  readonly onThumbnailSeries?: RequestHandler<ThumbnailSeriesRequest>;
  readonly onContactSheet?: RequestHandler<ContactSheetRequest>;
  readonly onMetadataAction?: RequestHandler<MetadataExecutionRequest>;
  readonly onChapterAction?: RequestHandler<ChapterExecutionRequest>;
  readonly onSubtitleAction?: RequestHandler<SubtitleExecutionRequest>;
  readonly onUserPresetAction?: RequestHandler<UserPresetAction>;
}

type OperationTab =
  | 'split'
  | 'audio'
  | 'images'
  | 'metadata'
  | 'chapters'
  | 'subtitles'
  | 'presets';

const OPERATION_TABS: readonly {
  readonly id: OperationTab;
  readonly label: string;
  readonly shortLabel: string;
}[] = [
  { id: 'split', label: 'Split media', shortLabel: 'Split' },
  { id: 'audio', label: 'Waveform and loudness', shortLabel: 'Audio' },
  {
    id: 'images',
    label: 'Thumbnails and contact sheets',
    shortLabel: 'Images',
  },
  { id: 'metadata', label: 'Metadata policies', shortLabel: 'Metadata' },
  { id: 'chapters', label: 'Chapter execution', shortLabel: 'Chapters' },
  { id: 'subtitles', label: 'Subtitle operations', shortLabel: 'Subtitles' },
  { id: 'presets', label: 'User export presets', shortLabel: 'Presets' },
];

interface ActionContext {
  readonly reasonFor: (
    capability: AdvancedCapabilityKey,
    localReason: string | undefined,
    connected: boolean
  ) => string | undefined;
  readonly run: <T>(
    handler: RequestHandler<T> | undefined,
    request: T,
    successMessage: string
  ) => void;
}

export function AdvancedOperations({
  source,
  splitMarkers = [],
  splitBatch,
  chapters = [],
  timelineClips = [],
  metadataStreams = [],
  metadataFormatTags,
  subtitleStreams = [],
  loudnessMeasurement,
  exportPresets = [],
  userPresets = [],
  availability = {},
  imageFormatAvailability = {},
  subtitleFormatAvailability = {},
  subtitleContainerAvailability = {},
  busy = false,
  onSplit,
  onAudioAction,
  onThumbnailSeries,
  onContactSheet,
  onMetadataAction,
  onChapterAction,
  onSubtitleAction,
  onUserPresetAction,
}: AdvancedOperationsProps) {
  const componentId = useId();
  const [activeTab, setActiveTab] = useState<OperationTab>('split');
  const [pending, setPending] = useState(false);
  const [status, setStatus] = useState<string>();
  const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);

  const reasonFor: ActionContext['reasonFor'] = (
    capability,
    localReason,
    connected
  ) => {
    if (busy || pending) {
      return 'Another local operation is currently running.';
    }
    if (localReason) {
      return localReason;
    }
    if (!connected) {
      return 'This action has not been connected to the job runner.';
    }
    const capabilityStatus = availability[capability];
    if (!capabilityStatus?.available) {
      return (
        capabilityStatus?.reason ??
        'The required browser and FFmpeg capabilities have not been verified.'
      );
    }
    return undefined;
  };

  const run: ActionContext['run'] = <T,>(
    handler: RequestHandler<T> | undefined,
    request: T,
    successMessage: string
  ) => {
    if (!handler) {
      return;
    }
    setPending(true);
    setStatus(undefined);
    void Promise.resolve(handler(request))
      .then(() => setStatus(successMessage))
      .catch((error: unknown) =>
        setStatus(
          error instanceof Error
            ? error.message
            : 'The requested local operation failed.'
        )
      )
      .finally(() => setPending(false));
  };

  const actionContext: ActionContext = { reasonFor, run };

  const handleTabKeyDown = (
    event: KeyboardEvent<HTMLButtonElement>,
    index: number
  ) => {
    let nextIndex: number | undefined;
    if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
      nextIndex = (index + 1) % OPERATION_TABS.length;
    } else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
      nextIndex = (index - 1 + OPERATION_TABS.length) % OPERATION_TABS.length;
    } else if (event.key === 'Home') {
      nextIndex = 0;
    } else if (event.key === 'End') {
      nextIndex = OPERATION_TABS.length - 1;
    }
    if (nextIndex === undefined) {
      return;
    }
    event.preventDefault();
    const next = OPERATION_TABS[nextIndex];
    if (!next) {
      return;
    }
    setActiveTab(next.id);
    tabRefs.current[nextIndex]?.focus();
  };

  return (
    <section
      className="advanced-operations"
      aria-labelledby={`${componentId}-title`}
    >
      <header className="advanced-operations__header">
        <div>
          <p className="advanced-operations__eyebrow">Typed local workflows</p>
          <h2 id={`${componentId}-title`}>Advanced operations</h2>
          <p>
            Configure reviewed operations only. This surface never accepts raw
            FFmpeg arguments, filter graphs, remote URLs, or filesystem paths.
          </p>
        </div>
        <span className="advanced-operations__local">Runs in this browser</span>
      </header>

      <div
        className="advanced-operations__tabs"
        role="tablist"
        aria-label="Advanced operation"
      >
        {OPERATION_TABS.map((tab, index) => (
          <button
            key={tab.id}
            ref={(element) => {
              tabRefs.current[index] = element;
            }}
            id={`${componentId}-tab-${tab.id}`}
            type="button"
            role="tab"
            aria-selected={activeTab === tab.id}
            aria-controls={`${componentId}-panel-${tab.id}`}
            tabIndex={activeTab === tab.id ? 0 : -1}
            onClick={() => setActiveTab(tab.id)}
            onKeyDown={(event) => handleTabKeyDown(event, index)}
          >
            <span className="advanced-operations__tab-long">{tab.label}</span>
            <span className="advanced-operations__tab-short">
              {tab.shortLabel}
            </span>
          </button>
        ))}
      </div>

      <OperationPanel id={componentId} tab="split" activeTab={activeTab}>
        <SplitPanel
          source={source}
          initialMarkers={splitMarkers}
          splitBatch={splitBatch}
          chapters={chapters}
          timelineClips={timelineClips}
          exportPresets={exportPresets}
          onSplit={onSplit}
          action={actionContext}
        />
      </OperationPanel>
      <OperationPanel id={componentId} tab="audio" activeTab={activeTab}>
        <AudioPanel
          source={source}
          measurement={loudnessMeasurement}
          exportPresets={exportPresets}
          onAction={onAudioAction}
          action={actionContext}
        />
      </OperationPanel>
      <OperationPanel id={componentId} tab="images" activeTab={activeTab}>
        <ImagePanel
          source={source}
          markers={splitMarkers}
          chapters={chapters}
          formatAvailability={imageFormatAvailability}
          labelAvailability={availability['contact-sheet-labels']}
          sceneAvailability={availability['scene-change-analysis']}
          onThumbnailSeries={onThumbnailSeries}
          onContactSheet={onContactSheet}
          action={actionContext}
        />
      </OperationPanel>
      <OperationPanel id={componentId} tab="metadata" activeTab={activeTab}>
        <MetadataPanel
          key={source?.sourceId ?? 'no-source'}
          source={source}
          streams={metadataStreams}
          formatTags={metadataFormatTags}
          onAction={onMetadataAction}
          action={actionContext}
        />
      </OperationPanel>
      <OperationPanel id={componentId} tab="chapters" activeTab={activeTab}>
        <ChapterPanel
          source={source}
          markers={splitMarkers}
          chapters={chapters}
          onAction={onChapterAction}
          action={actionContext}
        />
      </OperationPanel>
      <OperationPanel id={componentId} tab="subtitles" activeTab={activeTab}>
        <SubtitlePanel
          source={source}
          streams={subtitleStreams}
          exportPresets={exportPresets}
          formatAvailability={subtitleFormatAvailability}
          containerAvailability={subtitleContainerAvailability}
          onAction={onSubtitleAction}
          action={actionContext}
        />
      </OperationPanel>
      <OperationPanel id={componentId} tab="presets" activeTab={activeTab}>
        <PresetPanel
          presets={userPresets}
          onAction={onUserPresetAction}
          action={actionContext}
        />
      </OperationPanel>

      <div
        className="advanced-operations__status"
        role="status"
        aria-live="polite"
      >
        {status}
      </div>
    </section>
  );
}

function OperationPanel({
  id,
  tab,
  activeTab,
  children,
}: {
  readonly id: string;
  readonly tab: OperationTab;
  readonly activeTab: OperationTab;
  readonly children: React.ReactNode;
}) {
  return (
    <div
      id={`${id}-panel-${tab}`}
      className="advanced-operations__panel"
      role="tabpanel"
      aria-labelledby={`${id}-tab-${tab}`}
      hidden={activeTab !== tab}
    >
      {children}
    </div>
  );
}

function SplitPanel({
  source,
  initialMarkers,
  splitBatch,
  chapters,
  timelineClips,
  exportPresets,
  onSplit,
  action,
}: {
  readonly source?: AdvancedSourceContext;
  readonly initialMarkers: readonly number[];
  readonly splitBatch?: AdvancedSplitBatchStatus;
  readonly chapters: readonly EditableChapter[];
  readonly timelineClips: readonly TimeRange[];
  readonly exportPresets: readonly AdvancedExportPresetOption[];
  readonly onSplit?: RequestHandler<AdvancedSplitRequest>;
  readonly action: ActionContext;
}) {
  const splitModeName = useId();
  const [definitionType, setDefinitionType] =
    useState<SplitDefinition['type']>('markers');
  const [mode, setMode] = useState<'fast' | 'accurate'>('fast');
  const [markerOverride, setMarkerOverride] = useState<readonly number[]>();
  const [markerInput, setMarkerInput] = useState('');
  const [segmentDuration, setSegmentDuration] = useState('60');
  const [partCount, setPartCount] = useState('3');
  const [targetExtensionOverride, setTargetExtensionOverride] =
    useState<string>();
  const [presetId, setPresetId] = useState('');

  const markers = markerOverride ?? initialMarkers;
  const targetExtension =
    targetExtensionOverride ??
    normalizeExtension(source?.sourceExtension ?? 'mkv');
  const duration = source?.durationSeconds;
  const availablePresets = exportPresets.filter(
    (preset) => !preset.disabledReason
  );
  const effectivePresetId =
    availablePresets.find((preset) => preset.id === presetId)?.id ??
    availablePresets[0]?.id ??
    '';
  const selectedPreset = exportPresets.find(
    (preset) => preset.id === effectivePresetId
  );

  let definition: SplitDefinition | undefined;
  let definitionReason: string | undefined;
  let segmentCount: number | undefined;
  let segmentRanges: readonly TimeRange[] = [];
  if (!isPositive(duration)) {
    definitionReason = 'A probed source duration is required.';
  } else {
    try {
      if (definitionType === 'markers') {
        definition = {
          type: 'markers',
          markers: validateSplitMarkers(markers, duration),
        };
        if (markers.length === 0) {
          definitionReason = 'Add at least one marker inside the source.';
        }
      } else if (definitionType === 'equal-duration') {
        definition = {
          type: 'equal-duration',
          segmentDurationSeconds: Number(segmentDuration),
        };
      } else if (definitionType === 'part-count') {
        definition = { type: 'part-count', parts: Number(partCount) };
      } else if (definitionType === 'chapters') {
        definition = {
          type: 'chapters',
          chapters: chapters.map(chapterToRange),
        };
        if (chapters.length === 0) {
          definitionReason = 'No validated chapter ranges are available.';
        }
      } else {
        definition = { type: 'clips', clips: timelineClips };
        if (timelineClips.length === 0) {
          definitionReason = 'The timeline has no clips to export separately.';
        }
      }
      if (definition && !definitionReason) {
        segmentRanges = rangesForSplit(definition, duration);
        segmentCount = segmentRanges.length;
      }
    } catch (error) {
      definitionReason =
        error instanceof Error
          ? error.message
          : 'The split settings are invalid.';
    }
  }

  const extensionReason = /^[a-z0-9]{1,10}$/u.test(targetExtension)
    ? undefined
    : 'Use a 1–10 character lowercase file extension.';
  const modeReason =
    mode === 'accurate' && !selectedPreset
      ? 'Accurate splitting requires an available export preset.'
      : undefined;
  const localReason = !source
    ? 'Select a source before splitting.'
    : (definitionReason ?? extensionReason ?? modeReason);
  const capability = mode === 'fast' ? 'split-fast' : 'split-accurate';
  const reason = action.reasonFor(capability, localReason, Boolean(onSplit));

  const addMarker = () => {
    if (!isPositive(duration)) {
      return;
    }
    const next = Number(markerInput);
    try {
      const validated = validateSplitMarkers([...markers, next], duration);
      setMarkerOverride([...validated]);
      setMarkerInput('');
    } catch {
      // The inline validity message below remains the single user-facing error.
    }
  };

  return (
    <div className="advanced-operations__section-grid">
      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">1</span>
          <div>
            <h3>Segment definition</h3>
            <p>Every output receives a deterministic name and visible range.</p>
          </div>
        </header>
        <label className="advanced-field">
          <span>Split by</span>
          <select
            value={definitionType}
            onChange={(event) =>
              setDefinitionType(
                event.currentTarget.value as SplitDefinition['type']
              )
            }
          >
            <option value="markers">Manual markers</option>
            <option value="equal-duration">Equal duration</option>
            <option value="part-count">Number of parts</option>
            <option value="chapters" disabled={chapters.length === 0}>
              Chapter boundaries
            </option>
            <option value="clips" disabled={timelineClips.length === 0}>
              Selected-source timeline clip ranges
            </option>
          </select>
        </label>

        {definitionType === 'markers' ? (
          <>
            <div className="advanced-inline-fields">
              <label className="advanced-field">
                <span>Marker time (seconds)</span>
                <input
                  type="number"
                  min="0.04"
                  max={isPositive(duration) ? duration - 0.04 : undefined}
                  step="0.001"
                  value={markerInput}
                  onChange={(event) =>
                    setMarkerInput(event.currentTarget.value)
                  }
                />
              </label>
              <button
                className="advanced-button advanced-button--secondary"
                type="button"
                disabled={
                  !isPositive(duration) ||
                  !Number.isFinite(Number(markerInput)) ||
                  Number(markerInput) <= 0 ||
                  Number(markerInput) >= duration
                }
                onClick={addMarker}
              >
                Add marker
              </button>
            </div>
            {markers.length > 0 ? (
              <ol className="advanced-chip-list" aria-label="Split markers">
                {[...markers]
                  .sort((left, right) => left - right)
                  .map((marker) => (
                    <li key={marker}>
                      <span>{formatSeconds(marker)}</span>
                      <button
                        type="button"
                        aria-label={`Remove split marker at ${formatSeconds(marker)}`}
                        onClick={() =>
                          setMarkerOverride(
                            markers.filter((entry) => entry !== marker)
                          )
                        }
                      >
                        ×
                      </button>
                    </li>
                  ))}
              </ol>
            ) : (
              <p className="advanced-empty">No manual markers yet.</p>
            )}
          </>
        ) : null}

        {definitionType === 'equal-duration' ? (
          <label className="advanced-field">
            <span>Segment duration (seconds)</span>
            <input
              type="number"
              min="0.04"
              max={duration}
              step="0.001"
              value={segmentDuration}
              onChange={(event) =>
                setSegmentDuration(event.currentTarget.value)
              }
            />
          </label>
        ) : null}

        {definitionType === 'part-count' ? (
          <label className="advanced-field">
            <span>Number of parts</span>
            <input
              type="number"
              min="1"
              max={MAX_GENERATED_OUTPUT_FILES}
              step="1"
              value={partCount}
              onChange={(event) => setPartCount(event.currentTarget.value)}
            />
          </label>
        ) : null}

        {segmentCount !== undefined ? (
          <>
            <p className="advanced-summary">
              This configuration creates {segmentCount}{' '}
              {segmentCount === 1 ? 'segment' : 'segments'} in one FFmpeg
              execution.
            </p>
            <SegmentRangeList
              ranges={segmentRanges}
              label="Exact planned segment ranges"
            />
          </>
        ) : (
          <InlineReason reason={definitionReason} />
        )}
      </section>

      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">2</span>
          <div>
            <h3>Boundary strategy</h3>
            <p>Choose speed or frame-accurate boundaries explicitly.</p>
          </div>
        </header>
        <fieldset className="advanced-choice-cards">
          <legend className="advanced-visually-hidden">Split strategy</legend>
          <label>
            <input
              type="radio"
              name={splitModeName}
              checked={mode === 'fast'}
              onChange={() => setMode('fast')}
            />
            <span>
              <strong>Fast stream copy</strong>
              <small>
                Avoids quality loss, but boundaries may move to nearby
                keyframes.
              </small>
            </span>
          </label>
          <label>
            <input
              type="radio"
              name={splitModeName}
              checked={mode === 'accurate'}
              onChange={() => setMode('accurate')}
            />
            <span>
              <strong>Accurate re-encode</strong>
              <small>
                Decodes each segment for practical frame accuracy; slower and
                may alter quality.
              </small>
            </span>
          </label>
        </fieldset>

        {mode === 'fast' ? (
          <label className="advanced-field">
            <span>Output extension</span>
            <input
              inputMode="text"
              pattern="[a-z0-9]{1,10}"
              maxLength={10}
              value={targetExtension}
              onChange={(event) =>
                setTargetExtensionOverride(
                  normalizeExtension(event.currentTarget.value)
                )
              }
            />
            <small>
              Use the source container unless compatibility was verified.
            </small>
          </label>
        ) : (
          <PresetSelect
            label="Accurate split preset"
            presets={exportPresets}
            value={effectivePresetId}
            onChange={setPresetId}
          />
        )}

        <ActionButton
          label={
            segmentCount
              ? `Queue ${String(segmentCount)} segment${segmentCount === 1 ? '' : 's'}`
              : 'Queue split'
          }
          reason={reason}
          onClick={() => {
            if (!definition || reason) {
              return;
            }
            const request: AdvancedSplitRequest =
              mode === 'fast'
                ? {
                    mode,
                    definition,
                    targetExtension,
                  }
                : {
                    mode,
                    definition,
                    targetExtension: selectedPreset?.fileExtension ?? '',
                    presetId: effectivePresetId,
                  };
            action.run(onSplit, request, 'Split job added to the local queue.');
          }}
        />
      </section>
      {splitBatch ? <SplitBatchStatus batch={splitBatch} /> : null}
    </div>
  );
}

function SegmentRangeList({
  ranges,
  label,
}: {
  readonly ranges: readonly TimeRange[];
  readonly label: string;
}) {
  return (
    <div className="advanced-segment-list" aria-label={label}>
      <strong>{label}</strong>
      <ol>
        {ranges.map((range, index) => (
          <li key={`${range.startSeconds}-${range.endSeconds}-${index}`}>
            <b>Segment {String(index + 1).padStart(3, '0')}</b>
            <span>
              {formatSplitTime(range.startSeconds)}–
              {formatSplitTime(range.endSeconds)}
            </span>
            <small>
              {formatSplitTime(range.endSeconds - range.startSeconds)} duration
            </small>
          </li>
        ))}
      </ol>
    </div>
  );
}

function SplitBatchStatus({
  batch,
}: {
  readonly batch: AdvancedSplitBatchStatus;
}) {
  return (
    <section className="advanced-operation-card advanced-operation-card--wide">
      <header>
        <span className="advanced-operation-card__step">S</span>
        <div>
          <h3>Latest split batch status</h3>
          <p>
            All segments share one FFmpeg execution; each expected output is
            still tracked and verified separately.
          </p>
        </div>
      </header>
      <ol className="advanced-segment-status" aria-label="Split segment status">
        {batch.segments.map((segment) => (
          <li
            key={segment.outputId}
            className={`advanced-segment-status--${segment.state}`}
          >
            <div>
              <strong>{segment.fileName}</strong>
              <span>
                {formatSplitTime(segment.startSeconds)}–
                {formatSplitTime(segment.endSeconds)}
              </span>
            </div>
            <b>{segment.state}</b>
            {segment.message ? <small>{segment.message}</small> : null}
          </li>
        ))}
      </ol>
    </section>
  );
}

function AudioPanel({
  source,
  measurement,
  exportPresets,
  onAction,
  action,
}: {
  readonly source?: AdvancedSourceContext;
  readonly measurement?: LoudnessMeasurement;
  readonly exportPresets: readonly AdvancedExportPresetOption[];
  readonly onAction?: RequestHandler<AdvancedAudioRequest>;
  readonly action: ActionContext;
}) {
  const [waveformWidth, setWaveformWidth] = useState('1600');
  const [waveformHeight, setWaveformHeight] = useState('320');
  const [waveformForeground, setWaveformForeground] = useState('#5b7cff');
  const [waveformBackground, setWaveformBackground] = useState('#101629');
  const [sampleRate, setSampleRate] = useState('8000');
  const [bucketCount, setBucketCount] = useState('2000');
  const [profile, setProfile] = useState<
    keyof typeof LOUDNESS_PROFILES | 'custom'
  >('web-video');
  const [integratedLufs, setIntegratedLufs] = useState('-14');
  const [truePeakDbtp, setTruePeakDbtp] = useState('-1');
  const [loudnessRange, setLoudnessRange] = useState('11');
  const [targetPeakDb, setTargetPeakDb] = useState('-1');
  const [presetId, setPresetId] = useState('');

  const audioPresets = exportPresets.filter(
    (preset) => preset.supportsAudio && !preset.supportsVideo
  );
  const effectivePresetId =
    audioPresets.find(
      (preset) => preset.id === presetId && !preset.disabledReason
    )?.id ??
    audioPresets.find((preset) => !preset.disabledReason)?.id ??
    '';
  const presetReason = effectivePresetId
    ? undefined
    : 'An available audio-capable export preset is required.';
  const sourceReason = !source
    ? 'Select a source first.'
    : !source.hasAudio
      ? 'The selected source has no audio stream.'
      : undefined;
  const targets = {
    integratedLufs: Number(integratedLufs),
    truePeakDbtp: Number(truePeakDbtp),
    loudnessRangeLu: Number(loudnessRange),
  };
  const targetsReason = loudnessTargetsReason(targets);
  const staticReason =
    sourceReason ??
    integerRangeReason(waveformWidth, 64, 8192, 'Width') ??
    integerRangeReason(waveformHeight, 32, 4096, 'Height');
  const peaksReason =
    sourceReason ??
    integerRangeReason(sampleRate, 1000, 48000, 'Sample rate') ??
    integerRangeReason(bucketCount, 64, 100_000, 'Peak bucket count');
  const peakNormalizeReason =
    sourceReason ??
    numberRangeReason(targetPeakDb, -20, 0, 'Peak target') ??
    presetReason;
  const loudnessNormalizeReason =
    sourceReason ??
    targetsReason ??
    presetReason ??
    (measurement
      ? undefined
      : 'Run the first-pass loudness measurement before normalization.');

  const changeProfile = (
    nextProfile: keyof typeof LOUDNESS_PROFILES | 'custom'
  ) => {
    setProfile(nextProfile);
    if (nextProfile === 'custom') {
      return;
    }
    const values = LOUDNESS_PROFILES[nextProfile];
    setIntegratedLufs(String(values.integratedLufs));
    setTruePeakDbtp(String(values.truePeakDbtp));
    setLoudnessRange(String(values.loudnessRangeLu));
  };

  return (
    <div className="advanced-operations__stack">
      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">W</span>
          <div>
            <h3>Waveform derivatives</h3>
            <p>
              Only reduced analysis audio and bounded peak arrays may be cached;
              original media is never stored automatically.
            </p>
          </div>
        </header>
        <div className="advanced-form-grid advanced-form-grid--four">
          <NumberField
            label="Image width"
            value={waveformWidth}
            min={64}
            max={8192}
            onChange={setWaveformWidth}
          />
          <NumberField
            label="Image height"
            value={waveformHeight}
            min={32}
            max={4096}
            onChange={setWaveformHeight}
          />
          <ColorField
            label="Wave colour"
            value={waveformForeground}
            onChange={setWaveformForeground}
          />
          <ColorField
            label="Background"
            value={waveformBackground}
            onChange={setWaveformBackground}
          />
        </div>
        <ActionButton
          label="Generate static waveform"
          reason={action.reasonFor(
            'waveform-static',
            staticReason,
            Boolean(onAction)
          )}
          secondary
          onClick={() =>
            action.run(
              onAction,
              {
                type: 'static-waveform',
                audioStreamIndex: source?.selectedAudioStreamIndex,
                width: Number(waveformWidth),
                height: Number(waveformHeight),
                foreground: waveformForeground,
                background: waveformBackground,
              },
              'Static waveform job added to the local queue.'
            )
          }
        />

        <div className="advanced-divider" />
        <div className="advanced-form-grid">
          <NumberField
            label="Analysis sample rate"
            value={sampleRate}
            min={1000}
            max={48000}
            step={1000}
            suffix="Hz"
            onChange={setSampleRate}
          />
          <NumberField
            label="Peak buckets"
            value={bucketCount}
            min={64}
            max={100000}
            step={64}
            onChange={setBucketCount}
          />
        </div>
        <p className="advanced-hint">
          FFmpeg first creates reduced-rate mono PCM. A dedicated worker then
          aggregates min/max peaks and can be cancelled without retaining the
          source.
        </p>
        <ActionButton
          label="Analyze peak waveform"
          reason={action.reasonFor(
            'waveform-peaks',
            peaksReason,
            Boolean(onAction)
          )}
          onClick={() =>
            action.run(
              onAction,
              {
                type: 'waveform-peaks',
                audioStreamIndex: source?.selectedAudioStreamIndex,
                sampleRate: Number(sampleRate),
                bucketCount: Number(bucketCount),
              },
              'Waveform peak analysis added to the local queue.'
            )
          }
        />
      </section>

      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">L</span>
          <div>
            <h3>Loudness and normalization</h3>
            <p>
              Peak matching is not perceived-loudness normalization. EBU R128
              uses a measured first pass and a separate apply pass.
            </p>
          </div>
        </header>
        <label className="advanced-field">
          <span>Target profile</span>
          <select
            value={profile}
            onChange={(event) =>
              changeProfile(
                event.currentTarget.value as
                  keyof typeof LOUDNESS_PROFILES | 'custom'
              )
            }
          >
            <option value="web-video">Web video (starting point)</option>
            <option value="podcast">Podcast (starting point)</option>
            <option value="broadcast-style">
              Broadcast-style (starting point)
            </option>
            <option value="custom">Custom</option>
          </select>
          <small>No profile is universally correct.</small>
        </label>
        <div className="advanced-form-grid advanced-form-grid--three">
          <NumberField
            label="Integrated loudness"
            value={integratedLufs}
            min={-70}
            max={-5}
            step={0.1}
            suffix="LUFS"
            onChange={(value) => {
              setIntegratedLufs(value);
              setProfile('custom');
            }}
          />
          <NumberField
            label="True peak"
            value={truePeakDbtp}
            min={-9}
            max={0}
            step={0.1}
            suffix="dBTP"
            onChange={(value) => {
              setTruePeakDbtp(value);
              setProfile('custom');
            }}
          />
          <NumberField
            label="Loudness range"
            value={loudnessRange}
            min={1}
            max={50}
            step={0.1}
            suffix="LU"
            onChange={(value) => {
              setLoudnessRange(value);
              setProfile('custom');
            }}
          />
        </div>
        <div className="advanced-action-row">
          <ActionButton
            label="Measure EBU R128"
            reason={action.reasonFor(
              'loudness-measure',
              sourceReason ?? targetsReason,
              Boolean(onAction)
            )}
            secondary
            onClick={() =>
              action.run(
                onAction,
                {
                  type: 'loudness-measurement',
                  audioStreamIndex: source?.selectedAudioStreamIndex,
                  targets,
                },
                'First-pass loudness measurement added to the local queue.'
              )
            }
          />
          <ActionButton
            label="Apply measured normalization"
            reason={action.reasonFor(
              'loudness-normalize',
              loudnessNormalizeReason,
              Boolean(onAction)
            )}
            onClick={() => {
              if (!measurement) {
                return;
              }
              action.run(
                onAction,
                {
                  type: 'loudness-normalization',
                  audioStreamIndex: source?.selectedAudioStreamIndex,
                  targets,
                  measurement,
                  presetId: effectivePresetId,
                },
                'Second-pass loudness normalization added to the local queue.'
              );
            }}
          />
        </div>
        {measurement ? (
          <dl className="advanced-measurement">
            <div>
              <dt>Measured loudness</dt>
              <dd>{measurement.inputIntegratedLufs} LUFS</dd>
            </div>
            <div>
              <dt>Measured true peak</dt>
              <dd>{measurement.inputTruePeakDbtp} dBTP</dd>
            </div>
            <div>
              <dt>Measured range</dt>
              <dd>{measurement.inputLoudnessRangeLu} LU</dd>
            </div>
          </dl>
        ) : (
          <p className="advanced-empty">
            No complete loudnorm measurement is attached to this source.
          </p>
        )}

        <div className="advanced-divider" />
        <div className="advanced-form-grid">
          <NumberField
            label="Peak normalization target"
            value={targetPeakDb}
            min={-20}
            max={0}
            step={0.1}
            suffix="dBFS"
            onChange={setTargetPeakDb}
          />
          <PresetSelect
            label="Normalization preset"
            presets={audioPresets}
            value={effectivePresetId}
            onChange={setPresetId}
          />
        </div>
        <ActionButton
          label="Apply peak normalization"
          reason={action.reasonFor(
            'peak-normalize',
            peakNormalizeReason,
            Boolean(onAction)
          )}
          secondary
          onClick={() =>
            action.run(
              onAction,
              {
                type: 'peak-normalization',
                audioStreamIndex: source?.selectedAudioStreamIndex,
                targetPeakDb: Number(targetPeakDb),
                presetId: effectivePresetId,
              },
              'Peak normalization added to the local queue.'
            )
          }
        />
      </section>
    </div>
  );
}

function ImagePanel({
  source,
  markers,
  chapters,
  formatAvailability,
  labelAvailability,
  sceneAvailability,
  onThumbnailSeries,
  onContactSheet,
  action,
}: {
  readonly source?: AdvancedSourceContext;
  readonly markers: readonly number[];
  readonly chapters: readonly EditableChapter[];
  readonly formatAvailability: Partial<
    Readonly<Record<ImageFormat, AdvancedCapabilityStatus>>
  >;
  readonly labelAvailability?: AdvancedCapabilityStatus;
  readonly sceneAvailability?: AdvancedCapabilityStatus;
  readonly onThumbnailSeries?: RequestHandler<ThumbnailSeriesRequest>;
  readonly onContactSheet?: RequestHandler<ContactSheetRequest>;
  readonly action: ActionContext;
}) {
  const [distribution, setDistribution] =
    useState<ThumbnailDistribution['type']>('evenly-spaced');
  const [seriesCount, setSeriesCount] = useState('12');
  const [interval, setInterval] = useState('30');
  const [width, setWidth] = useState('480');
  const [height, setHeight] = useState('');
  const [format, setFormat] = useState<ImageFormat>('jpeg');
  const [quality, setQuality] = useState('85');
  const [sheetFrames, setSheetFrames] = useState('12');
  const [sheetRows, setSheetRows] = useState('3');
  const [sheetColumns, setSheetColumns] = useState('4');
  const [cellWidth, setCellWidth] = useState('320');
  const [spacing, setSpacing] = useState('6');
  const [background, setBackground] = useState('#101629');
  const [maintainAspect, setMaintainAspect] = useState(true);
  const [timestampLabels, setTimestampLabels] = useState(false);
  const [sourceLabel, setSourceLabel] = useState(false);
  const [sceneThreshold, setSceneThreshold] = useState('0.35');
  const [sheetSelection, setSheetSelection] = useState<
    'evenly-spaced' | 'scene-changes'
  >('evenly-spaced');

  const videoReason = !source
    ? 'Select a source first.'
    : !source.hasVideo
      ? 'The selected source has no video stream.'
      : !isPositive(source.durationSeconds)
        ? 'A probed source duration is required.'
        : undefined;
  const formatReason = formatAvailability[format]?.available
    ? undefined
    : (formatAvailability[format]?.reason ??
      `${format.toUpperCase()} image output has not been verified.`);
  const sizeReason =
    optionalIntegerRangeReason(width, 1, 8192, 'Width') ??
    optionalIntegerRangeReason(height, 1, 8192, 'Height');
  let distributionReason: string | undefined;
  let resolvedDistribution: ThumbnailDistribution | undefined;
  if (distribution === 'evenly-spaced') {
    distributionReason = integerRangeReason(
      seriesCount,
      1,
      MAX_GENERATED_OUTPUT_FILES,
      'Frame count'
    );
    resolvedDistribution = {
      type: distribution,
      count: Number(seriesCount),
    };
  } else if (distribution === 'fixed-interval') {
    distributionReason = numberRangeReason(
      interval,
      0.04,
      source?.durationSeconds ?? Number.MAX_SAFE_INTEGER,
      'Interval'
    );
    if (
      !distributionReason &&
      isPositive(source?.durationSeconds) &&
      Math.ceil(source.durationSeconds / Number(interval)) >
        MAX_GENERATED_OUTPUT_FILES
    ) {
      distributionReason = `This interval would create more than ${MAX_GENERATED_OUTPUT_FILES} images. ${GENERATED_OUTPUT_LIMIT_EXPLANATION}`;
    }
    resolvedDistribution = {
      type: distribution,
      intervalSeconds: Number(interval),
    };
  } else if (distribution === 'scene-changes') {
    distributionReason =
      integerRangeReason(
        seriesCount,
        1,
        MAX_SCENE_CHANGE_FRAMES,
        'Frame count'
      ) ??
      numberRangeReason(
        sceneThreshold,
        MIN_SCENE_CHANGE_THRESHOLD,
        MAX_SCENE_CHANGE_THRESHOLD,
        'Scene threshold'
      ) ??
      (sceneAvailability?.available
        ? undefined
        : (sceneAvailability?.reason ??
          'Scene-change analysis has not been verified.'));
    resolvedDistribution = {
      type: distribution,
      count: Number(seriesCount),
      threshold: Number(sceneThreshold),
    };
  } else if (distribution === 'chapter-starts') {
    const times = chapters.map((chapter) => chapter.startSeconds);
    distributionReason =
      times.length === 0
        ? 'No chapter starts are available.'
        : times.length > MAX_GENERATED_OUTPUT_FILES
          ? `This source has more than ${MAX_GENERATED_OUTPUT_FILES} chapter starts. ${GENERATED_OUTPUT_LIMIT_EXPLANATION}`
          : undefined;
    resolvedDistribution = { type: distribution, timesSeconds: times };
  } else {
    distributionReason =
      markers.length === 0
        ? 'No split markers are available.'
        : markers.length > MAX_GENERATED_OUTPUT_FILES
          ? `This selection has more than ${MAX_GENERATED_OUTPUT_FILES} split markers. ${GENERATED_OUTPUT_LIMIT_EXPLANATION}`
          : undefined;
    resolvedDistribution = {
      type: distribution,
      timesSeconds: [...markers],
    };
  }
  const seriesReason =
    videoReason ??
    distributionReason ??
    sizeReason ??
    numberRangeReason(quality, 1, 100, 'Quality') ??
    formatReason;

  const labelsRequested = timestampLabels || sourceLabel;
  const labelReason =
    labelsRequested && !labelAvailability?.available
      ? (labelAvailability?.reason ??
        'Timestamp and filename labels require verified drawtext and a safe local font.')
      : undefined;
  const gridReason =
    integerRangeReason(
      sheetFrames,
      1,
      sheetSelection === 'scene-changes' ? MAX_SCENE_CHANGE_FRAMES : 500,
      'Frame count'
    ) ??
    integerRangeReason(sheetRows, 1, 500, 'Rows') ??
    integerRangeReason(sheetColumns, 1, 500, 'Columns') ??
    integerRangeReason(cellWidth, 32, 4096, 'Cell width') ??
    integerRangeReason(spacing, 0, 100, 'Spacing') ??
    (Number(sheetRows) * Number(sheetColumns) < Number(sheetFrames)
      ? 'The grid needs at least one cell per requested frame.'
      : Number(sheetRows) * Number(sheetColumns) > 500
        ? 'The grid may contain at most 500 cells.'
        : undefined);
  const contactReason =
    videoReason ??
    gridReason ??
    numberRangeReason(quality, 1, 100, 'Quality') ??
    formatReason ??
    labelReason ??
    (sheetSelection === 'scene-changes'
      ? (numberRangeReason(
          sceneThreshold,
          MIN_SCENE_CHANGE_THRESHOLD,
          MAX_SCENE_CHANGE_THRESHOLD,
          'Scene threshold'
        ) ??
        (sceneAvailability?.available
          ? undefined
          : (sceneAvailability?.reason ??
            'Scene-change analysis has not been verified.')))
      : undefined);

  return (
    <div className="advanced-operations__section-grid">
      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">T</span>
          <div>
            <h3>Thumbnail series</h3>
            <p>
              Decode the sequence once, preview each result, then save or ZIP.
            </p>
          </div>
        </header>
        <label className="advanced-field">
          <span>Frame selection</span>
          <select
            value={distribution}
            onChange={(event) =>
              setDistribution(
                event.currentTarget.value as ThumbnailDistribution['type']
              )
            }
          >
            <option value="evenly-spaced">Evenly spaced</option>
            <option value="fixed-interval">Fixed interval</option>
            <option
              value="scene-changes"
              disabled={!sceneAvailability?.available}
            >
              Scene changes
            </option>
            <option value="chapter-starts" disabled={chapters.length === 0}>
              Chapter starts
            </option>
            <option value="split-markers" disabled={markers.length === 0}>
              Split markers
            </option>
          </select>
        </label>
        {distribution === 'evenly-spaced' ||
        distribution === 'scene-changes' ? (
          <NumberField
            label="Number of frames"
            value={seriesCount}
            min={1}
            max={
              distribution === 'scene-changes'
                ? MAX_SCENE_CHANGE_FRAMES
                : MAX_GENERATED_OUTPUT_FILES
            }
            onChange={setSeriesCount}
          />
        ) : null}
        {distribution === 'fixed-interval' ? (
          <NumberField
            label="Interval"
            value={interval}
            min={0.04}
            max={source?.durationSeconds}
            step={0.1}
            suffix="seconds"
            onChange={setInterval}
          />
        ) : null}
        {distribution === 'scene-changes' ? (
          <NumberField
            label="Scene sensitivity threshold"
            value={sceneThreshold}
            min={MIN_SCENE_CHANGE_THRESHOLD}
            max={MAX_SCENE_CHANGE_THRESHOLD}
            step={0.01}
            onChange={setSceneThreshold}
          />
        ) : null}
        <ImageSettings
          width={width}
          height={height}
          format={format}
          quality={quality}
          onWidth={setWidth}
          onHeight={setHeight}
          onFormat={setFormat}
          onQuality={setQuality}
        />
        <ActionButton
          label="Generate thumbnail series"
          reason={action.reasonFor(
            'thumbnail-series',
            seriesReason,
            Boolean(onThumbnailSeries)
          )}
          onClick={() => {
            if (!resolvedDistribution) {
              return;
            }
            action.run(
              onThumbnailSeries,
              {
                distribution: resolvedDistribution,
                ...(width ? { width: Number(width) } : {}),
                ...(height ? { height: Number(height) } : {}),
                format,
                quality: Number(quality),
              },
              'Thumbnail series added to the local queue.'
            );
          }}
        />
      </section>

      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">C</span>
          <div>
            <h3>Contact sheet</h3>
            <p>
              Produces one independent image. There is no runtime dependency on
              image-tools.
            </p>
          </div>
        </header>
        <div className="advanced-form-grid advanced-form-grid--three">
          <label className="advanced-field">
            <span>Frame selection</span>
            <select
              value={sheetSelection}
              onChange={(event) =>
                setSheetSelection(
                  event.currentTarget.value as 'evenly-spaced' | 'scene-changes'
                )
              }
            >
              <option value="evenly-spaced">Evenly spaced</option>
              <option
                value="scene-changes"
                disabled={!sceneAvailability?.available}
              >
                Scene changes
              </option>
            </select>
          </label>
          <NumberField
            label="Frames"
            value={sheetFrames}
            min={1}
            max={
              sheetSelection === 'scene-changes' ? MAX_SCENE_CHANGE_FRAMES : 500
            }
            onChange={setSheetFrames}
          />
          <NumberField
            label="Rows"
            value={sheetRows}
            min={1}
            max={500}
            onChange={setSheetRows}
          />
          <NumberField
            label="Columns"
            value={sheetColumns}
            min={1}
            max={500}
            onChange={setSheetColumns}
          />
          <NumberField
            label="Cell width"
            value={cellWidth}
            min={32}
            max={4096}
            suffix="px"
            onChange={setCellWidth}
          />
          <NumberField
            label="Spacing"
            value={spacing}
            min={0}
            max={100}
            suffix="px"
            onChange={setSpacing}
          />
          <ColorField
            label="Background"
            value={background}
            onChange={setBackground}
          />
        </div>
        {sheetSelection === 'scene-changes' ? (
          <NumberField
            label="Contact-sheet scene threshold"
            value={sceneThreshold}
            min={MIN_SCENE_CHANGE_THRESHOLD}
            max={MAX_SCENE_CHANGE_THRESHOLD}
            step={0.01}
            onChange={setSceneThreshold}
          />
        ) : null}
        <div className="advanced-check-grid">
          <CheckField
            label="Maintain aspect ratio"
            checked={maintainAspect}
            onChange={setMaintainAspect}
          />
          <CheckField
            label="Timestamp labels"
            checked={timestampLabels}
            disabled={!labelAvailability?.available}
            onChange={setTimestampLabels}
          />
          <CheckField
            label="Source filename label"
            checked={sourceLabel}
            disabled={!labelAvailability?.available}
            onChange={setSourceLabel}
          />
        </div>
        {!labelAvailability?.available ? (
          <p className="advanced-hint">
            {labelAvailability?.reason ??
              'Labels stay off until drawtext and a safe local font are verified.'}
          </p>
        ) : null}
        <ImageSettings
          format={format}
          quality={quality}
          onFormat={setFormat}
          onQuality={setQuality}
        />
        <ActionButton
          label="Generate contact sheet"
          reason={action.reasonFor(
            'contact-sheet',
            contactReason,
            Boolean(onContactSheet)
          )}
          onClick={() =>
            action.run(
              onContactSheet,
              {
                frameCount: Number(sheetFrames),
                rows: Number(sheetRows),
                columns: Number(sheetColumns),
                cellWidth: Number(cellWidth),
                spacing: Number(spacing),
                background,
                maintainAspect,
                timestampLabels,
                sourceLabel,
                format,
                quality: Number(quality),
                selection:
                  sheetSelection === 'scene-changes'
                    ? {
                        type: 'scene-changes',
                        threshold: Number(sceneThreshold),
                      }
                    : { type: 'evenly-spaced' },
              },
              'Contact sheet added to the local queue.'
            )
          }
        />
      </section>
    </div>
  );
}

function MetadataPanel({
  source,
  streams,
  formatTags,
  onAction,
  action,
}: {
  readonly source?: AdvancedSourceContext;
  readonly streams: readonly MetadataStreamDescription[];
  readonly formatTags?: Readonly<Record<string, string>>;
  readonly onAction?: RequestHandler<MetadataExecutionRequest>;
  readonly action: ActionContext;
}) {
  const [title, setTitle] = useState(() => metadataText(formatTags?.title));
  const [artist, setArtist] = useState(() => metadataText(formatTags?.artist));
  const [album, setAlbum] = useState(() => metadataText(formatTags?.album));
  const [date, setDate] = useState(() => metadataText(formatTags?.date));
  const [comment, setComment] = useState(() =>
    metadataText(formatTags?.comment)
  );
  const [touchedFields, setTouchedFields] = useState<
    ReadonlySet<MetadataFieldKey>
  >(() => new Set());
  const [importedFormatExtras, setImportedFormatExtras] = useState<
    Readonly<Record<string, string | number | null>>
  >({});
  const [sourcePolicy, setSourcePolicy] = useState<'copy' | 'remove'>('copy');
  const [chapterPolicy, setChapterPolicy] = useState<'keep' | 'remove'>('keep');
  const [coverPolicy, setCoverPolicy] = useState<'keep' | 'remove'>('keep');
  const [streamEdits, setStreamEdits] = useState<MetadataEdits['streams']>();
  const [streamDispositions, setStreamDispositions] =
    useState<MetadataEdits['dispositions']>();
  const [fileMessage, setFileMessage] = useState<string>();

  const commonFormatEdits =
    metadataFromFields({ title, artist, album, date, comment }, touchedFields)
      .format ?? {};
  const edits: MetadataEdits = {
    format: { ...importedFormatExtras, ...commonFormatEdits },
    ...(streamEdits ? { streams: streamEdits } : {}),
    ...(streamDispositions ? { dispositions: streamDispositions } : {}),
  };
  const writeReason = action.reasonFor(
    'metadata-write',
    source ? undefined : 'Select a source before writing metadata.',
    Boolean(onAction)
  );

  const updateMetadataField = (
    field: MetadataFieldKey,
    setter: (value: string) => void,
    value: string
  ) => {
    setter(value);
    setTouchedFields((current) => new Set(current).add(field));
  };

  const importMetadata = async (event: ChangeEvent<HTMLInputElement>) => {
    const file = event.currentTarget.files?.[0];
    event.currentTarget.value = '';
    if (!file) {
      return;
    }
    try {
      const json = await readSmallTextFile(file, 256 * 1024);
      const imported = parseMetadataJson(json);
      setMetadataFields(imported, {
        setTitle,
        setArtist,
        setAlbum,
        setDate,
        setComment,
      });
      setTouchedFields(
        new Set(
          METADATA_FIELD_KEYS.filter(
            (key) => imported.format?.[key] !== undefined
          )
        )
      );
      setImportedFormatExtras(
        Object.fromEntries(
          Object.entries(imported.format ?? {}).filter(
            ([key]) => !METADATA_FIELD_KEYS.includes(key as MetadataFieldKey)
          )
        )
      );
      setStreamEdits(imported.streams);
      setStreamDispositions(imported.dispositions);
      setFileMessage(`Validated ${file.name}. Review before applying.`);
      action.run(
        onAction,
        { type: 'import', edits: imported },
        'Metadata JSON imported into the typed editor.'
      );
    } catch (error) {
      setFileMessage(
        error instanceof Error ? error.message : 'Metadata import failed.'
      );
    }
  };

  const streamTagValue = (
    stream: MetadataStreamDescription,
    key: 'language' | 'title'
  ): string => {
    const edited = streamEdits?.[stream.index]?.[key];
    if (edited !== undefined) {
      return edited === null ? '' : String(edited);
    }
    return stream[key] ?? '';
  };

  const updateStreamTag = (
    streamIndex: number,
    key: 'language' | 'title',
    value: string
  ) => {
    setStreamEdits((current) => ({
      ...current,
      [streamIndex]: {
        ...current?.[streamIndex],
        [key]: value === '' ? null : value,
      },
    }));
  };

  const dispositionsFor = (
    stream: MetadataStreamDescription
  ): readonly StreamDisposition[] => {
    const edited = streamDispositions?.[stream.index];
    if (edited !== undefined) return edited;
    return EDITABLE_STREAM_DISPOSITIONS.filter(
      (disposition) => stream.disposition[disposition] === true
    );
  };

  const toggleDisposition = (
    stream: MetadataStreamDescription,
    disposition: StreamDisposition,
    enabled: boolean
  ) => {
    const current = new Set(dispositionsFor(stream));
    if (enabled) current.add(disposition);
    else current.delete(disposition);
    setStreamDispositions((edits) => ({
      ...edits,
      [stream.index]: EDITABLE_STREAM_DISPOSITIONS.filter((entry) =>
        current.has(entry)
      ),
    }));
  };

  return (
    <div className="advanced-operations__stack">
      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">M</span>
          <div>
            <h3>Container metadata</h3>
            <p>
              Common tags are allowlisted. Output is re-probed to report which
              tags and container-specific fields survived.
            </p>
          </div>
        </header>
        <div className="advanced-form-grid advanced-form-grid--three">
          <TextField
            label="Title"
            value={title}
            maxLength={256}
            onChange={(value) => updateMetadataField('title', setTitle, value)}
          />
          <TextField
            label="Artist"
            value={artist}
            maxLength={256}
            onChange={(value) =>
              updateMetadataField('artist', setArtist, value)
            }
          />
          <TextField
            label="Album"
            value={album}
            maxLength={256}
            onChange={(value) => updateMetadataField('album', setAlbum, value)}
          />
          <TextField
            label="Date"
            value={date}
            maxLength={64}
            onChange={(value) => updateMetadataField('date', setDate, value)}
          />
          <TextField
            label="Comment"
            value={comment}
            maxLength={1000}
            onChange={(value) =>
              updateMetadataField('comment', setComment, value)
            }
          />
        </div>
        {streamEdits ? (
          <p className="advanced-summary">
            {Object.keys(streamEdits).length} stream metadata edit
            {Object.keys(streamEdits).length === 1 ? '' : 's'} loaded from the
            validated JSON and retained for Apply/Export.
          </p>
        ) : (
          <p className="advanced-summary">
            Stream language, title, and other allowlisted tags can be supplied
            through the reproducible metadata JSON editor below.
          </p>
        )}
        {streams.length > 0 ? (
          <div
            className="advanced-stream-metadata"
            aria-label="Stream metadata and dispositions"
          >
            {streams.map((stream) => {
              const dispositions = dispositionsFor(stream);
              return (
                <fieldset key={stream.index}>
                  <legend>
                    #{stream.index} · {stream.type} · {stream.codec}
                  </legend>
                  <div className="advanced-form-grid">
                    <TextField
                      label={`Stream #${stream.index} language`}
                      value={streamTagValue(stream, 'language')}
                      maxLength={35}
                      onChange={(value) =>
                        updateStreamTag(stream.index, 'language', value)
                      }
                    />
                    <TextField
                      label={`Stream #${stream.index} title`}
                      value={streamTagValue(stream, 'title')}
                      maxLength={256}
                      onChange={(value) =>
                        updateStreamTag(stream.index, 'title', value)
                      }
                    />
                  </div>
                  <div className="advanced-check-grid">
                    {(['default', 'forced'] as const).map((disposition) => (
                      <CheckField
                        key={disposition}
                        label={`${humanizeDisposition(disposition)} stream #${stream.index}`}
                        checked={dispositions.includes(disposition)}
                        onChange={(enabled) =>
                          toggleDisposition(stream, disposition, enabled)
                        }
                      />
                    ))}
                  </div>
                  <details className="advanced-disposition-details">
                    <summary>Other stream dispositions</summary>
                    <div className="advanced-check-grid">
                      {EDITABLE_STREAM_DISPOSITIONS.filter(
                        (disposition) =>
                          disposition !== 'default' && disposition !== 'forced'
                      ).map((disposition) => (
                        <CheckField
                          key={disposition}
                          label={`${humanizeDisposition(disposition)} stream #${stream.index}`}
                          checked={dispositions.includes(disposition)}
                          onChange={(enabled) =>
                            toggleDisposition(stream, disposition, enabled)
                          }
                        />
                      ))}
                    </div>
                  </details>
                  <small className="advanced-hint">
                    Dispositions are requested explicitly; the target muxer may
                    normalize or auto-assign defaults, which the result probe
                    reports.
                  </small>
                </fieldset>
              );
            })}
          </div>
        ) : (
          <p className="advanced-empty">
            Probe a source to edit per-stream language, title, and disposition.
          </p>
        )}
        <div className="advanced-form-grid advanced-form-grid--three">
          <PolicySelect
            label="Source metadata"
            value={sourcePolicy}
            options={[
              ['copy', 'Copy source metadata'],
              ['remove', 'Remove removable metadata'],
            ]}
            onChange={(value) => setSourcePolicy(value as 'copy' | 'remove')}
          />
          <PolicySelect
            label="Chapters"
            value={chapterPolicy}
            options={[
              ['keep', 'Keep chapters'],
              ['remove', 'Remove chapters'],
            ]}
            onChange={(value) => setChapterPolicy(value as 'keep' | 'remove')}
          />
          <PolicySelect
            label="Cover art"
            value={coverPolicy}
            options={[
              ['keep', 'Keep cover art'],
              ['remove', 'Remove cover art'],
            ]}
            onChange={(value) => setCoverPolicy(value as 'keep' | 'remove')}
          />
        </div>
        {sourcePolicy === 'remove' ? (
          <p className="advanced-warning">
            “Remove” prevents copying common removable tags. It does not claim
            that every structural or container-specific metadata field
            disappears.
          </p>
        ) : null}
        <div className="advanced-action-row">
          <ActionButton
            label="Apply metadata policies"
            reason={writeReason}
            onClick={() =>
              action.run(
                onAction,
                {
                  type: 'apply',
                  edits,
                  sourceMetadataPolicy: sourcePolicy,
                  chapterPolicy,
                  coverArtPolicy: coverPolicy,
                },
                'Metadata operation added to the local queue.'
              )
            }
          />
          <ActionButton
            label="Export metadata JSON"
            reason={
              onAction
                ? undefined
                : 'Metadata export has not been connected to a file saver.'
            }
            secondary
            onClick={() =>
              action.run(
                onAction,
                { type: 'export', edits, json: serializeMetadataJson(edits) },
                'Versioned metadata JSON prepared for saving.'
              )
            }
          />
          <label className="advanced-file-button">
            Import metadata JSON
            <input
              type="file"
              accept=".json,application/json"
              onChange={importMetadata}
            />
          </label>
        </div>
        {fileMessage ? <p role="status">{fileMessage}</p> : null}
      </section>
    </div>
  );
}

function ChapterPanel({
  source,
  markers,
  chapters,
  onAction,
  action,
}: {
  readonly source?: AdvancedSourceContext;
  readonly markers: readonly number[];
  readonly chapters: readonly EditableChapter[];
  readonly onAction?: RequestHandler<ChapterExecutionRequest>;
  readonly action: ActionContext;
}) {
  const [chapterSource, setChapterSource] = useState<
    'existing' | 'markers' | 'fixed'
  >('existing');
  const [interval, setInterval] = useState('300');
  const [fileMessage, setFileMessage] = useState<string>();
  const duration = source?.durationSeconds;

  let proposedChapters: readonly EditableChapter[] = chapters;
  let chapterReason: string | undefined;
  try {
    if (chapterSource === 'markers') {
      if (!isPositive(duration) || markers.length === 0) {
        chapterReason =
          'Validated split markers and a probed duration are required.';
      } else {
        proposedChapters = createChaptersFromMarkers(markers, duration);
      }
    } else if (chapterSource === 'fixed') {
      if (!isPositive(duration)) {
        chapterReason = 'A probed source duration is required.';
      } else {
        proposedChapters = createChaptersAtIntervals(
          duration,
          Number(interval)
        );
      }
    } else if (chapters.length === 0) {
      chapterReason = 'There are no edited chapters to write.';
    }
  } catch (error) {
    chapterReason =
      error instanceof Error ? error.message : 'Chapter settings are invalid.';
  }

  const writeReason = action.reasonFor(
    'chapter-write',
    !source ? 'Select a source before writing chapters.' : chapterReason,
    Boolean(onAction)
  );

  const importChapters = async (event: ChangeEvent<HTMLInputElement>) => {
    const file = event.currentTarget.files?.[0];
    event.currentTarget.value = '';
    if (!file) {
      return;
    }
    try {
      const json = await readSmallTextFile(file, 256 * 1024);
      const document = parseChapterJson(json);
      setFileMessage(
        `Validated ${String(document.chapters.length)} chapter(s) from ${file.name}.`
      );
      action.run(
        onAction,
        { type: 'import', chapters: document.chapters },
        'Chapter JSON imported into the project.'
      );
    } catch (error) {
      setFileMessage(
        error instanceof Error ? error.message : 'Chapter import failed.'
      );
    }
  };

  return (
    <div className="advanced-operations__section-grid">
      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">C</span>
          <div>
            <h3>Chapter source</h3>
            <p>
              Chapter titles and times are validated before an escaped
              FFMETADATA derivative is generated.
            </p>
          </div>
        </header>
        <label className="advanced-field">
          <span>Create output chapters from</span>
          <select
            value={chapterSource}
            onChange={(event) =>
              setChapterSource(
                event.currentTarget.value as 'existing' | 'markers' | 'fixed'
              )
            }
          >
            <option value="existing" disabled={chapters.length === 0}>
              Current chapter editor ({chapters.length})
            </option>
            <option value="markers" disabled={markers.length === 0}>
              Split markers ({markers.length})
            </option>
            <option value="fixed">Fixed interval</option>
          </select>
        </label>
        {chapterSource === 'fixed' ? (
          <NumberField
            label="Chapter interval"
            value={interval}
            min={0.04}
            max={duration}
            step={0.1}
            suffix="seconds"
            onChange={setInterval}
          />
        ) : null}
        <p className="advanced-summary">
          {chapterReason ??
            `${String(proposedChapters.length)} chapter(s) will be written and verified by re-probing.`}
        </p>
        <div className="advanced-action-row">
          <ActionButton
            label="Replace output chapters"
            reason={writeReason}
            onClick={() =>
              action.run(
                onAction,
                { type: 'replace', chapters: proposedChapters },
                'Chapter mux operation added to the local queue.'
              )
            }
          />
          <ActionButton
            label="Remove output chapters"
            reason={action.reasonFor(
              'chapter-write',
              source ? undefined : 'Select a source before removing chapters.',
              Boolean(onAction)
            )}
            secondary
            onClick={() =>
              action.run(
                onAction,
                { type: 'remove' },
                'Chapter removal added to the local queue.'
              )
            }
          />
        </div>
      </section>

      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">J</span>
          <div>
            <h3>Chapter JSON</h3>
            <p>Import and export the documented schemaVersion 1 format.</p>
          </div>
        </header>
        <div className="advanced-action-row">
          <ActionButton
            label="Export current chapters"
            reason={
              !onAction
                ? 'Chapter export has not been connected to a file saver.'
                : chapters.length === 0
                  ? 'There are no chapters to export.'
                  : undefined
            }
            secondary
            onClick={() =>
              action.run(
                onAction,
                {
                  type: 'export',
                  chapters,
                  json: serializeChapterJson(chapters),
                },
                'Versioned chapter JSON prepared for saving.'
              )
            }
          />
          <label className="advanced-file-button">
            Import chapter JSON
            <input
              type="file"
              accept=".json,application/json"
              onChange={importChapters}
            />
          </label>
        </div>
        {fileMessage ? <p role="status">{fileMessage}</p> : null}
      </section>
    </div>
  );
}

function SubtitlePanel({
  source,
  streams,
  exportPresets,
  formatAvailability,
  containerAvailability,
  onAction,
  action,
}: {
  readonly source?: AdvancedSourceContext;
  readonly streams: readonly SubtitleStreamDescription[];
  readonly exportPresets: readonly AdvancedExportPresetOption[];
  readonly formatAvailability: Partial<
    Readonly<Record<SubtitleFormat, AdvancedCapabilityStatus>>
  >;
  readonly containerAvailability: Partial<
    Readonly<Record<'mp4' | 'webm' | 'matroska', AdvancedCapabilityStatus>>
  >;
  readonly onAction?: RequestHandler<SubtitleExecutionRequest>;
  readonly action: ActionContext;
}) {
  const [operation, setOperation] = useState<
    'extract' | 'convert' | 'soft-mux' | 'burn-in'
  >('extract');
  const [streamIndex, setStreamIndex] = useState('');
  const [outputFormat, setOutputFormat] = useState<SubtitleFormat>('srt');
  const [subtitleFile, setSubtitleFile] = useState<File>();
  const [inputFormat, setInputFormat] = useState<SubtitleFormat>();
  const [offset, setOffset] = useState('0');
  const [targetContainer, setTargetContainer] = useState<
    'mp4' | 'webm' | 'matroska'
  >('matroska');
  const [language, setLanguage] = useState('');
  const [title, setTitle] = useState('');
  const [isDefault, setIsDefault] = useState(false);
  const [isForced, setIsForced] = useState(false);
  const [presetId, setPresetId] = useState('');
  const [fileMessage, setFileMessage] = useState<string>();

  const selectedStream =
    streams.find((stream) => String(stream.index) === streamIndex) ??
    streams[0];
  const effectiveStreamIndex = selectedStream
    ? String(selectedStream.index)
    : '';
  const videoPresets = exportPresets.filter((preset) => preset.supportsVideo);
  const effectivePresetId =
    videoPresets.find(
      (preset) => preset.id === presetId && !preset.disabledReason
    )?.id ??
    videoPresets.find((preset) => !preset.disabledReason)?.id ??
    '';
  const presetReason = effectivePresetId
    ? undefined
    : 'An available video export preset is required.';
  const fileReason =
    subtitleFile && inputFormat
      ? undefined
      : 'Choose a local SRT, WebVTT, ASS, or SSA file.';
  const sourceReason = source ? undefined : 'Select a media source first.';
  const extractReason =
    sourceReason ??
    (!selectedStream
      ? 'No subtitle stream is available.'
      : classifySubtitleCodec(selectedStream.codec) === 'bitmap'
        ? 'Bitmap subtitle extraction is not text conversion. Preserve it in a compatible bitmap format instead.'
        : classifySubtitleCodec(selectedStream.codec) === 'unknown'
          ? `The ${selectedStream.codec} subtitle codec is not classified as supported text.`
          : undefined);
  const convertReason =
    fileReason ??
    numberRangeReason(offset, -86400, 86400, 'Time offset') ??
    availabilityReason(formatAvailability[outputFormat]);
  const muxReason =
    sourceReason ??
    fileReason ??
    availabilityReason(containerAvailability[targetContainer]);
  const exactExtractReason =
    extractReason ?? availabilityReason(formatAvailability[outputFormat]);
  const burnReason =
    sourceReason ??
    (!source?.hasVideo
      ? 'Subtitle burn-in requires a video stream.'
      : undefined) ??
    fileReason ??
    presetReason;

  const chooseSubtitleFile = (event: ChangeEvent<HTMLInputElement>) => {
    const file = event.currentTarget.files?.[0];
    if (!file) {
      setSubtitleFile(undefined);
      setInputFormat(undefined);
      return;
    }
    const detected = detectSubtitleFormat(file.name);
    if (!detected) {
      setSubtitleFile(undefined);
      setInputFormat(undefined);
      setFileMessage(
        'Only local SRT, WebVTT, ASS, and SSA files are accepted.'
      );
      event.currentTarget.value = '';
      return;
    }
    setSubtitleFile(file);
    setInputFormat(detected);
    setFileMessage(`${file.name} will be mounted only for this local job.`);
  };

  const capability: AdvancedCapabilityKey =
    operation === 'extract'
      ? 'subtitle-extract'
      : operation === 'convert'
        ? 'subtitle-convert'
        : operation === 'soft-mux'
          ? 'subtitle-mux'
          : 'subtitle-burn';
  const localReason =
    operation === 'extract'
      ? exactExtractReason
      : operation === 'convert'
        ? convertReason
        : operation === 'soft-mux'
          ? muxReason
          : burnReason;
  const reason = action.reasonFor(capability, localReason, Boolean(onAction));

  const submit = () => {
    if (reason) {
      return;
    }
    let request: SubtitleExecutionRequest | undefined;
    if (operation === 'extract' && selectedStream) {
      request = {
        type: 'extract',
        stream: selectedStream,
        outputFormat,
      };
    } else if (operation === 'convert' && subtitleFile && inputFormat) {
      request = {
        type: 'convert',
        file: subtitleFile,
        inputFormat,
        outputFormat,
        offsetSeconds: Number(offset),
      };
    } else if (operation === 'soft-mux' && subtitleFile && inputFormat) {
      request = {
        type: 'soft-mux',
        file: subtitleFile,
        inputFormat,
        subtitleCodec: codecForSubtitleFormat(inputFormat),
        targetContainer,
        ...(language.trim() ? { language: language.trim() } : {}),
        ...(title.trim() ? { title: title.trim() } : {}),
        default: isDefault,
        forced: isForced,
      };
    } else if (operation === 'burn-in' && subtitleFile && inputFormat) {
      request = {
        type: 'burn-in',
        file: subtitleFile,
        inputFormat,
        presetId: effectivePresetId,
      };
    }
    if (request) {
      action.run(
        onAction,
        request,
        'Subtitle operation added to the local queue.'
      );
    }
  };

  return (
    <div className="advanced-operations__section-grid">
      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">S</span>
          <div>
            <h3>Subtitle operation</h3>
            <p>
              Text and bitmap streams are distinguished. No OCR or automatic
              bitmap-to-text conversion is implied.
            </p>
          </div>
        </header>
        <label className="advanced-field">
          <span>Operation</span>
          <select
            value={operation}
            onChange={(event) =>
              setOperation(
                event.currentTarget.value as
                  'extract' | 'convert' | 'soft-mux' | 'burn-in'
              )
            }
          >
            <option value="extract">Extract a text stream</option>
            <option value="convert">Convert or time-shift a local file</option>
            <option value="soft-mux">Mux a local soft subtitle</option>
            <option value="burn-in">Burn a local subtitle into video</option>
          </select>
        </label>

        {operation === 'extract' ? (
          <label className="advanced-field">
            <span>Subtitle stream</span>
            <select
              value={effectiveStreamIndex}
              disabled={streams.length === 0}
              onChange={(event) => setStreamIndex(event.currentTarget.value)}
            >
              {streams.map((stream) => (
                <option key={stream.index} value={stream.index}>
                  #{stream.index} · {stream.codec} ·{' '}
                  {classifySubtitleCodec(stream.codec)}
                  {stream.language ? ` · ${stream.language}` : ''}
                </option>
              ))}
            </select>
          </label>
        ) : (
          <>
            <label className="advanced-file-field">
              <span>Local subtitle file</span>
              <input
                type="file"
                accept=".srt,.vtt,.ass,.ssa,text/vtt,application/x-subrip,text/x-ass"
                onChange={chooseSubtitleFile}
              />
              <small>{fileMessage ?? 'The file is never uploaded.'}</small>
            </label>
          </>
        )}

        {operation === 'extract' || operation === 'convert' ? (
          <label className="advanced-field">
            <span>Output text format</span>
            <select
              value={outputFormat}
              onChange={(event) =>
                setOutputFormat(event.currentTarget.value as SubtitleFormat)
              }
            >
              <option value="srt">SubRip (.srt)</option>
              <option value="vtt">WebVTT (.vtt)</option>
              <option value="ass">ASS (.ass)</option>
            </select>
          </label>
        ) : null}

        {operation === 'convert' ? (
          <NumberField
            label="Time offset"
            value={offset}
            min={-86400}
            max={86400}
            step={0.001}
            suffix="seconds"
            onChange={setOffset}
          />
        ) : null}

        {operation === 'soft-mux' ? (
          <>
            <label className="advanced-field">
              <span>Target container</span>
              <select
                value={targetContainer}
                onChange={(event) =>
                  setTargetContainer(
                    event.currentTarget.value as 'mp4' | 'webm' | 'matroska'
                  )
                }
              >
                <option value="matroska">Matroska</option>
                <option value="mp4">MP4 (converts text to mov_text)</option>
                <option value="webm">WebM (converts text to WebVTT)</option>
              </select>
              <small>
                Compatibility is checked again by the command builder before
                execution.
              </small>
            </label>
            <div className="advanced-form-grid">
              <TextField
                label="Language"
                value={language}
                maxLength={35}
                onChange={setLanguage}
              />
              <TextField
                label="Track title"
                value={title}
                maxLength={128}
                onChange={setTitle}
              />
            </div>
            <div className="advanced-check-grid">
              <CheckField
                label="Default track"
                checked={isDefault}
                onChange={setIsDefault}
              />
              <CheckField
                label="Forced track"
                checked={isForced}
                onChange={setIsForced}
              />
            </div>
          </>
        ) : null}

        {operation === 'burn-in' ? (
          <PresetSelect
            label="Video export preset"
            presets={videoPresets}
            value={effectivePresetId}
            onChange={setPresetId}
          />
        ) : null}

        <ActionButton
          label={
            operation === 'extract'
              ? 'Extract subtitle'
              : operation === 'convert'
                ? 'Convert subtitle'
                : operation === 'soft-mux'
                  ? 'Mux soft subtitle'
                  : 'Burn subtitle into video'
          }
          reason={reason}
          onClick={submit}
        />
      </section>

      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">i</span>
          <div>
            <h3>Known source streams</h3>
            <p>Classification is codec-based and deliberately conservative.</p>
          </div>
        </header>
        {streams.length > 0 ? (
          <ul className="advanced-stream-list">
            {streams.map((stream) => (
              <li key={stream.index}>
                <strong>
                  #{stream.index} · {stream.codec}
                </strong>
                <span>{classifySubtitleCodec(stream.codec)}</span>
                <small>
                  {[stream.language, stream.title]
                    .filter(Boolean)
                    .join(' · ') || 'No language or title'}
                </small>
              </li>
            ))}
          </ul>
        ) : (
          <p className="advanced-empty">
            No subtitle streams were identified by the validated probe.
          </p>
        )}
      </section>
    </div>
  );
}

interface PresetDraft {
  readonly id: string;
  readonly name: string;
  readonly description: string;
  readonly kind: Exclude<PresetKind, 'remux'>;
  readonly container: string;
  readonly fileExtension: string;
  readonly videoCodec: VideoCodec | 'none';
  readonly videoQuality: QualityMode;
  readonly videoCrf: string;
  readonly videoBitrate: string;
  readonly width: string;
  readonly height: string;
  readonly frameRate: string;
  readonly audioCodec: AudioCodec | 'none';
  readonly audioBitrate: string;
  readonly sampleRate: '' | '22050' | '32000' | '44100' | '48000' | '96000';
  readonly channels: '' | '1' | '2' | '6';
  readonly metadataPolicy: MetadataPolicy;
  readonly chapterPolicy: ChapterPolicy;
  readonly subtitlePolicy: SubtitlePolicy;
  readonly fastStart: boolean;
}

const EMPTY_PRESET_DRAFT: PresetDraft = {
  id: '',
  name: '',
  description: '',
  kind: 'video',
  container: 'mp4',
  fileExtension: 'mp4',
  videoCodec: 'libx264',
  videoQuality: 'balanced',
  videoCrf: '23',
  videoBitrate: '',
  width: '',
  height: '',
  frameRate: '',
  audioCodec: 'aac',
  audioBitrate: '160',
  sampleRate: '48000',
  channels: '2',
  metadataPolicy: 'copy',
  chapterPolicy: 'keep',
  subtitlePolicy: 'none',
  fastStart: true,
};

function PresetPanel({
  presets,
  onAction,
  action,
}: {
  readonly presets: readonly UserExportPreset[];
  readonly onAction?: RequestHandler<UserPresetAction>;
  readonly action: ActionContext;
}) {
  const [selectedId, setSelectedId] = useState('');
  const selected =
    presets.find((preset) => preset.id === selectedId) ?? presets[0];
  const effectiveSelectedId = selected?.id ?? '';
  const [draftOverride, setDraftOverride] = useState<PresetDraft>();
  const draft =
    draftOverride ?? (selected ? presetToDraft(selected) : EMPTY_PRESET_DRAFT);
  const [creatingOverride, setCreatingOverride] = useState<boolean>();
  const isCreating = creatingOverride ?? !selected;
  const [editorMessage, setEditorMessage] = useState<string>();
  const [confirmDelete, setConfirmDelete] = useState(false);
  const [confirmReset, setConfirmReset] = useState(false);

  const setDraft = (
    value: PresetDraft | ((current: PresetDraft) => PresetDraft)
  ) => {
    setDraftOverride((current) =>
      typeof value === 'function' ? value(current ?? draft) : value
    );
  };
  const setIsCreating = (value: boolean) => setCreatingOverride(value);

  const capabilityReason = action.reasonFor(
    'preset-storage',
    undefined,
    Boolean(onAction)
  );

  const selectPreset = (id: string) => {
    setSelectedId(id);
    const preset = presets.find((entry) => entry.id === id);
    if (preset) {
      setDraft(presetToDraft(preset));
      setIsCreating(false);
      setEditorMessage(undefined);
      setConfirmDelete(false);
    }
  };

  const saveDraft = () => {
    try {
      const preset = draftToPreset(draft);
      action.run(
        onAction,
        { type: isCreating ? 'create' : 'update', preset },
        isCreating ? 'User preset created.' : 'User preset updated.'
      );
      setEditorMessage(undefined);
    } catch (error) {
      setEditorMessage(
        error instanceof Error ? error.message : 'Preset settings are invalid.'
      );
    }
  };

  const importPreset = async (event: ChangeEvent<HTMLInputElement>) => {
    const file = event.currentTarget.files?.[0];
    event.currentTarget.value = '';
    if (!file) {
      return;
    }
    try {
      const json = await readSmallTextFile(file, 64 * 1024);
      const preset = parseUserPresetJson(json);
      setDraft(presetToDraft(preset));
      setIsCreating(true);
      setEditorMessage(
        `Validated ${file.name}. Save or revise the allowlisted fields.`
      );
      action.run(
        onAction,
        { type: 'import', preset },
        'Validated user preset imported.'
      );
    } catch (error) {
      setEditorMessage(
        error instanceof Error ? error.message : 'Preset import failed.'
      );
    }
  };

  const updateDraft = <K extends keyof PresetDraft>(
    key: K,
    value: PresetDraft[K]
  ) => setDraft((current) => ({ ...current, [key]: value }));

  return (
    <div className="advanced-operations__stack">
      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">P</span>
          <div>
            <h3>User preset library</h3>
            <p>
              Imported presets use a strict versioned schema. Unknown fields,
              raw arguments, filters, paths, and protocols are rejected.
            </p>
          </div>
        </header>
        <div className="advanced-preset-toolbar">
          <label className="advanced-field">
            <span>Saved preset</span>
            <select
              value={effectiveSelectedId}
              disabled={presets.length === 0}
              onChange={(event) => selectPreset(event.currentTarget.value)}
            >
              {presets.map((preset) => (
                <option key={preset.id} value={preset.id}>
                  {preset.name}
                </option>
              ))}
            </select>
          </label>
          <button
            type="button"
            className="advanced-button advanced-button--secondary"
            onClick={() => {
              setDraft(EMPTY_PRESET_DRAFT);
              setIsCreating(true);
              setEditorMessage(undefined);
            }}
          >
            New
          </button>
          <ActionButton
            label="Clone"
            reason={
              selected ? capabilityReason : 'Select a saved preset to clone.'
            }
            secondary
            onClick={() => {
              if (!selected) {
                return;
              }
              const newId = uniqueCloneId(selected.id, presets);
              action.run(
                onAction,
                {
                  type: 'clone',
                  presetId: selected.id,
                  newId,
                  newName: `${selected.name} copy`,
                },
                'User preset cloned.'
              );
            }}
          />
          <label
            className={`advanced-file-button${
              capabilityReason ? ' advanced-file-button--disabled' : ''
            }`}
          >
            Import JSON
            <input
              type="file"
              accept=".json,application/json"
              disabled={Boolean(capabilityReason)}
              onChange={importPreset}
            />
          </label>
        </div>
      </section>

      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">E</span>
          <div>
            <h3>{isCreating ? 'Create typed preset' : 'Edit typed preset'}</h3>
            <p>Every field maps to a reviewed setting in the preset model.</p>
          </div>
        </header>
        <div className="advanced-form-grid advanced-form-grid--three">
          <TextField
            label="Safe ID"
            value={draft.id}
            maxLength={80}
            onChange={(value) => updateDraft('id', value)}
          />
          <TextField
            label="Name"
            value={draft.name}
            maxLength={80}
            onChange={(value) => updateDraft('name', value)}
          />
          <TextField
            label="Description"
            value={draft.description}
            maxLength={400}
            onChange={(value) => updateDraft('description', value)}
          />
          <PolicySelect
            label="Kind"
            value={draft.kind}
            options={[
              ['video', 'Video'],
              ['audio', 'Audio'],
              ['derivative', 'Derivative'],
            ]}
            onChange={(value) => {
              const kind = value as PresetDraft['kind'];
              setDraft((current) => ({
                ...current,
                kind,
                ...(kind === 'audio'
                  ? { videoCodec: 'none' as const }
                  : current.videoCodec === 'none'
                    ? { videoCodec: 'libx264' as const }
                    : {}),
              }));
            }}
          />
          <TextField
            label="Container"
            value={draft.container}
            maxLength={80}
            onChange={(value) => updateDraft('container', value)}
          />
          <TextField
            label="File extension"
            value={draft.fileExtension}
            maxLength={10}
            onChange={(value) =>
              updateDraft('fileExtension', normalizeExtension(value))
            }
          />
        </div>

        <h4>Video settings</h4>
        <div className="advanced-form-grid advanced-form-grid--four">
          <PolicySelect
            label="Video codec"
            value={draft.videoCodec}
            options={[
              ['none', 'No video'],
              ['libx264', 'H.264 / libx264'],
              ['libvpx-vp9', 'VP9'],
              ['libvpx', 'VP8'],
              ['libwebp_anim', 'Animated WebP'],
              ['gif', 'GIF'],
            ]}
            disabled={draft.kind === 'audio'}
            onChange={(value) =>
              updateDraft('videoCodec', value as PresetDraft['videoCodec'])
            }
          />
          <PolicySelect
            label="Quality mode"
            value={draft.videoQuality}
            options={[
              ['compatibility', 'Compatibility'],
              ['balanced', 'Balanced'],
              ['smaller-file', 'Smaller file'],
              ['higher-quality', 'Higher quality'],
              ['custom', 'Custom'],
            ]}
            disabled={draft.videoCodec === 'none'}
            onChange={(value) =>
              updateDraft('videoQuality', value as QualityMode)
            }
          />
          <NumberField
            label="CRF"
            value={draft.videoCrf}
            min={0}
            max={63}
            optional
            disabled={draft.videoCodec === 'none'}
            onChange={(value) => updateDraft('videoCrf', value)}
          />
          <NumberField
            label="Video bitrate"
            value={draft.videoBitrate}
            min={0}
            max={1000000}
            suffix="kbit/s"
            optional
            disabled={draft.videoCodec === 'none'}
            onChange={(value) => updateDraft('videoBitrate', value)}
          />
          <NumberField
            label="Width"
            value={draft.width}
            min={2}
            max={8192}
            optional
            disabled={draft.videoCodec === 'none'}
            onChange={(value) => updateDraft('width', value)}
          />
          <NumberField
            label="Height"
            value={draft.height}
            min={2}
            max={8192}
            optional
            disabled={draft.videoCodec === 'none'}
            onChange={(value) => updateDraft('height', value)}
          />
          <NumberField
            label="Frame rate"
            value={draft.frameRate}
            min={0.01}
            max={240}
            step={0.001}
            optional
            disabled={draft.videoCodec === 'none'}
            onChange={(value) => updateDraft('frameRate', value)}
          />
        </div>

        <h4>Audio settings</h4>
        <div className="advanced-form-grid advanced-form-grid--four">
          <PolicySelect
            label="Audio codec"
            value={draft.audioCodec}
            options={[
              ['none', 'No audio'],
              ['aac', 'AAC'],
              ['libmp3lame', 'MP3'],
              ['libvorbis', 'Vorbis'],
              ['libopus', 'Opus'],
              ['pcm_s16le', 'PCM 16-bit'],
              ['flac', 'FLAC'],
            ]}
            onChange={(value) =>
              updateDraft('audioCodec', value as PresetDraft['audioCodec'])
            }
          />
          <NumberField
            label="Audio bitrate"
            value={draft.audioBitrate}
            min={8}
            max={2000}
            suffix="kbit/s"
            optional
            disabled={draft.audioCodec === 'none'}
            onChange={(value) => updateDraft('audioBitrate', value)}
          />
          <PolicySelect
            label="Sample rate"
            value={draft.sampleRate}
            options={[
              ['', 'Preserve/default'],
              ['22050', '22,050 Hz'],
              ['32000', '32,000 Hz'],
              ['44100', '44,100 Hz'],
              ['48000', '48,000 Hz'],
              ['96000', '96,000 Hz'],
            ]}
            disabled={draft.audioCodec === 'none'}
            onChange={(value) =>
              updateDraft('sampleRate', value as PresetDraft['sampleRate'])
            }
          />
          <PolicySelect
            label="Channels"
            value={draft.channels}
            options={[
              ['', 'Preserve/default'],
              ['1', 'Mono'],
              ['2', 'Stereo'],
              ['6', '5.1'],
            ]}
            disabled={draft.audioCodec === 'none'}
            onChange={(value) =>
              updateDraft('channels', value as PresetDraft['channels'])
            }
          />
        </div>

        <h4>Output policies</h4>
        <p className="advanced-operation-card__hint">
          User presets expose only policies the Quick workflow can execute
          directly. Metadata edits, chapter replacement, and subtitle burn-in
          remain explicit operations with their own required inputs.
        </p>
        <div className="advanced-form-grid advanced-form-grid--three">
          <PolicySelect
            label="Metadata"
            value={draft.metadataPolicy}
            options={[
              ['copy', 'Copy'],
              ['remove', 'Remove removable'],
            ]}
            onChange={(value) =>
              updateDraft('metadataPolicy', value as MetadataPolicy)
            }
          />
          <PolicySelect
            label="Chapters"
            value={draft.chapterPolicy}
            options={[
              ['keep', 'Keep'],
              ['remove', 'Remove'],
            ]}
            onChange={(value) =>
              updateDraft('chapterPolicy', value as ChapterPolicy)
            }
          />
          <PolicySelect
            label="Subtitles"
            value={draft.subtitlePolicy}
            options={[
              ['none', 'Exclude'],
              ['copy-compatible', 'Copy compatible'],
              ['convert', 'Convert'],
            ]}
            onChange={(value) =>
              updateDraft('subtitlePolicy', value as SubtitlePolicy)
            }
          />
        </div>
        <CheckField
          label="Enable MP4 fast-start where supported"
          checked={draft.fastStart}
          onChange={(value) => updateDraft('fastStart', value)}
        />
        {editorMessage ? (
          <p className="advanced-warning" role="alert">
            {editorMessage}
          </p>
        ) : null}
        <div className="advanced-action-row">
          <ActionButton
            label={isCreating ? 'Create preset' : 'Save typed changes'}
            reason={capabilityReason}
            onClick={saveDraft}
          />
          {!isCreating && selected ? (
            <ActionButton
              label="Rename only"
              reason={
                draft.name.trim()
                  ? capabilityReason
                  : 'Enter a nonempty preset name.'
              }
              secondary
              onClick={() =>
                action.run(
                  onAction,
                  {
                    type: 'rename',
                    presetId: selected.id,
                    name: draft.name.trim(),
                  },
                  'User preset renamed.'
                )
              }
            />
          ) : null}
          <button
            className="advanced-button advanced-button--secondary"
            type="button"
            onClick={() => {
              setDraft(
                !isCreating && selected
                  ? presetToDraft(selected)
                  : EMPTY_PRESET_DRAFT
              );
              setEditorMessage(undefined);
            }}
          >
            Reset editor
          </button>
        </div>
      </section>

      <section className="advanced-operation-card">
        <header>
          <span className="advanced-operation-card__step">⋯</span>
          <div>
            <h3>Preset file and deletion actions</h3>
            <p>Destructive actions require a second explicit click.</p>
          </div>
        </header>
        <div className="advanced-action-row">
          <ActionButton
            label="Export selected JSON"
            reason={
              selected ? capabilityReason : 'Select a saved preset to export.'
            }
            secondary
            onClick={() => {
              if (!selected) {
                return;
              }
              action.run(
                onAction,
                {
                  type: 'export',
                  preset: selected,
                  json: serializeUserPreset(selected),
                },
                'Versioned preset JSON prepared for saving.'
              );
            }}
          />
          {!confirmDelete ? (
            <button
              className="advanced-button advanced-button--danger"
              type="button"
              disabled={!selected || Boolean(capabilityReason)}
              onClick={() => setConfirmDelete(true)}
            >
              Delete selected
            </button>
          ) : (
            <>
              <button
                className="advanced-button advanced-button--danger"
                type="button"
                onClick={() => {
                  if (selected) {
                    action.run(
                      onAction,
                      { type: 'delete', presetId: selected.id },
                      'User preset deleted.'
                    );
                  }
                  setSelectedId('');
                  setDraftOverride(undefined);
                  setCreatingOverride(undefined);
                  setConfirmDelete(false);
                }}
              >
                Confirm delete
              </button>
              <button
                className="advanced-button advanced-button--secondary"
                type="button"
                onClick={() => setConfirmDelete(false)}
              >
                Cancel
              </button>
            </>
          )}
          {!confirmReset ? (
            <button
              className="advanced-button advanced-button--danger"
              type="button"
              disabled={presets.length === 0 || Boolean(capabilityReason)}
              onClick={() => setConfirmReset(true)}
            >
              Reset all user presets
            </button>
          ) : (
            <>
              <button
                className="advanced-button advanced-button--danger"
                type="button"
                onClick={() => {
                  action.run(
                    onAction,
                    { type: 'reset-all' },
                    'All user presets were reset.'
                  );
                  setSelectedId('');
                  setDraftOverride(undefined);
                  setCreatingOverride(undefined);
                  setConfirmReset(false);
                }}
              >
                Confirm reset all
              </button>
              <button
                className="advanced-button advanced-button--secondary"
                type="button"
                onClick={() => setConfirmReset(false)}
              >
                Cancel
              </button>
            </>
          )}
        </div>
        <InlineReason reason={capabilityReason} />
      </section>
    </div>
  );
}

function ActionButton({
  label,
  reason,
  onClick,
  secondary = false,
}: {
  readonly label: string;
  readonly reason?: string;
  readonly onClick: () => void;
  readonly secondary?: boolean;
}) {
  const reasonId = useId();
  return (
    <div className="advanced-action">
      <button
        type="button"
        className={`advanced-button ${
          secondary ? 'advanced-button--secondary' : 'advanced-button--primary'
        }`}
        disabled={Boolean(reason)}
        aria-describedby={reason ? reasonId : undefined}
        onClick={onClick}
      >
        {label}
      </button>
      {reason ? (
        <small id={reasonId} className="advanced-action__reason">
          {reason}
        </small>
      ) : null}
    </div>
  );
}

function InlineReason({ reason }: { readonly reason?: string }) {
  return reason ? <p className="advanced-warning">{reason}</p> : null;
}

function NumberField({
  label,
  value,
  min,
  max,
  step = 1,
  suffix,
  optional = false,
  disabled = false,
  onChange,
}: {
  readonly label: string;
  readonly value: string;
  readonly min?: number;
  readonly max?: number;
  readonly step?: number;
  readonly suffix?: string;
  readonly optional?: boolean;
  readonly disabled?: boolean;
  readonly onChange: (value: string) => void;
}) {
  return (
    <label className="advanced-field">
      <span>
        {label}
        {suffix ? <small> · {suffix}</small> : null}
      </span>
      <input
        type="number"
        min={min}
        max={max}
        step={step}
        required={!optional}
        disabled={disabled}
        value={value}
        onChange={(event) => onChange(event.currentTarget.value)}
      />
    </label>
  );
}

function TextField({
  label,
  value,
  maxLength,
  disabled = false,
  onChange,
}: {
  readonly label: string;
  readonly value: string;
  readonly maxLength: number;
  readonly disabled?: boolean;
  readonly onChange: (value: string) => void;
}) {
  return (
    <label className="advanced-field">
      <span>{label}</span>
      <input
        type="text"
        value={value}
        maxLength={maxLength}
        disabled={disabled}
        onChange={(event) => onChange(event.currentTarget.value)}
      />
    </label>
  );
}

function ColorField({
  label,
  value,
  onChange,
}: {
  readonly label: string;
  readonly value: string;
  readonly onChange: (value: string) => void;
}) {
  return (
    <label className="advanced-field advanced-field--color">
      <span>{label}</span>
      <span>
        <input
          type="color"
          value={value}
          onChange={(event) => onChange(event.currentTarget.value)}
        />
        <code>{value}</code>
      </span>
    </label>
  );
}

function CheckField({
  label,
  checked,
  disabled = false,
  onChange,
}: {
  readonly label: string;
  readonly checked: boolean;
  readonly disabled?: boolean;
  readonly onChange: (checked: boolean) => void;
}) {
  return (
    <label className="advanced-check">
      <input
        type="checkbox"
        checked={checked}
        disabled={disabled}
        onChange={(event) => onChange(event.currentTarget.checked)}
      />
      <span>{label}</span>
    </label>
  );
}

function PolicySelect({
  label,
  value,
  options,
  disabled = false,
  onChange,
}: {
  readonly label: string;
  readonly value: string;
  readonly options: readonly (readonly [string, string])[];
  readonly disabled?: boolean;
  readonly onChange: (value: string) => void;
}) {
  return (
    <label className="advanced-field">
      <span>{label}</span>
      <select
        value={value}
        disabled={disabled}
        onChange={(event) => onChange(event.currentTarget.value)}
      >
        {options.map(([optionValue, optionLabel]) => (
          <option key={optionValue || 'default'} value={optionValue}>
            {optionLabel}
          </option>
        ))}
      </select>
    </label>
  );
}

function PresetSelect({
  label,
  presets,
  value,
  onChange,
}: {
  readonly label: string;
  readonly presets: readonly AdvancedExportPresetOption[];
  readonly value: string;
  readonly onChange: (value: string) => void;
}) {
  return (
    <label className="advanced-field">
      <span>{label}</span>
      <select
        value={value}
        disabled={presets.length === 0}
        onChange={(event) => onChange(event.currentTarget.value)}
      >
        {presets.length === 0 ? (
          <option value="">No compatible presets</option>
        ) : null}
        {presets.map((preset) => (
          <option
            key={preset.id}
            value={preset.id}
            disabled={Boolean(preset.disabledReason)}
          >
            {preset.name}
            {preset.disabledReason ? ` — ${preset.disabledReason}` : ''}
          </option>
        ))}
      </select>
    </label>
  );
}

function ImageSettings({
  width,
  height,
  format,
  quality,
  onWidth,
  onHeight,
  onFormat,
  onQuality,
}: {
  readonly width?: string;
  readonly height?: string;
  readonly format: ImageFormat;
  readonly quality: string;
  readonly onWidth?: (value: string) => void;
  readonly onHeight?: (value: string) => void;
  readonly onFormat: (value: ImageFormat) => void;
  readonly onQuality: (value: string) => void;
}) {
  return (
    <div className="advanced-form-grid advanced-form-grid--four">
      {width !== undefined && onWidth ? (
        <NumberField
          label="Width"
          value={width}
          min={1}
          max={8192}
          optional
          onChange={onWidth}
        />
      ) : null}
      {height !== undefined && onHeight ? (
        <NumberField
          label="Height"
          value={height}
          min={1}
          max={8192}
          optional
          onChange={onHeight}
        />
      ) : null}
      <label className="advanced-field">
        <span>Image format</span>
        <select
          value={format}
          onChange={(event) =>
            onFormat(event.currentTarget.value as ImageFormat)
          }
        >
          <option value="png">PNG</option>
          <option value="jpeg">JPEG</option>
          <option value="webp">WebP</option>
        </select>
      </label>
      <NumberField
        label="Quality"
        value={quality}
        min={1}
        max={100}
        suffix="%"
        onChange={onQuality}
      />
    </div>
  );
}

const METADATA_FIELD_KEYS = [
  'title',
  'artist',
  'album',
  'date',
  'comment',
] as const;

type MetadataFieldKey = (typeof METADATA_FIELD_KEYS)[number];

function metadataFromFields(
  fields: {
    readonly title: string;
    readonly artist: string;
    readonly album: string;
    readonly date: string;
    readonly comment: string;
  },
  touchedFields: ReadonlySet<MetadataFieldKey>
): MetadataEdits {
  return {
    format: Object.freeze(
      Object.fromEntries(
        METADATA_FIELD_KEYS.flatMap((key) =>
          touchedFields.has(key) ? [[key, fields[key] || null]] : []
        )
      )
    ),
  };
}

function setMetadataFields(
  edits: MetadataEdits,
  setters: {
    readonly setTitle: (value: string) => void;
    readonly setArtist: (value: string) => void;
    readonly setAlbum: (value: string) => void;
    readonly setDate: (value: string) => void;
    readonly setComment: (value: string) => void;
  }
) {
  const format = edits.format ?? {};
  setters.setTitle(metadataText(format.title));
  setters.setArtist(metadataText(format.artist));
  setters.setAlbum(metadataText(format.album));
  setters.setDate(metadataText(format.date));
  setters.setComment(metadataText(format.comment));
}

function metadataText(value: string | number | null | undefined): string {
  return value === null || value === undefined ? '' : String(value);
}

function availabilityReason(
  availability: AdvancedCapabilityStatus | undefined
): string | undefined {
  return availability && !availability.available
    ? (availability.reason ?? 'This exact output combination is unavailable.')
    : undefined;
}

function humanizeDisposition(disposition: StreamDisposition): string {
  const label = disposition.replaceAll('_', ' ');
  return `${label[0]?.toUpperCase() ?? ''}${label.slice(1)}`;
}

function presetToDraft(preset: UserExportPreset): PresetDraft {
  return {
    id: preset.id,
    name: preset.name,
    description: preset.description ?? '',
    kind: preset.kind,
    container: preset.container,
    fileExtension: preset.fileExtension,
    videoCodec: preset.video?.codec ?? 'none',
    videoQuality: preset.video?.qualityMode ?? 'balanced',
    videoCrf: optionalNumber(preset.video?.crf),
    videoBitrate: optionalNumber(preset.video?.bitrateKbps),
    width: optionalNumber(preset.video?.width),
    height: optionalNumber(preset.video?.height),
    frameRate: optionalNumber(preset.video?.frameRate),
    audioCodec: preset.audio?.codec ?? 'none',
    audioBitrate: optionalNumber(preset.audio?.bitrateKbps),
    sampleRate: preset.audio?.sampleRate
      ? (String(preset.audio.sampleRate) as PresetDraft['sampleRate'])
      : '',
    channels: preset.audio?.channels
      ? (String(preset.audio.channels) as PresetDraft['channels'])
      : '',
    metadataPolicy: preset.metadataPolicy,
    chapterPolicy: preset.chapterPolicy,
    subtitlePolicy: preset.subtitlePolicy,
    fastStart: preset.fastStart ?? false,
  };
}

function draftToPreset(draft: PresetDraft): UserExportPreset {
  const candidate = {
    schemaVersion: 1,
    id: draft.id.trim(),
    name: draft.name.trim(),
    ...(draft.description.trim()
      ? { description: draft.description.trim() }
      : {}),
    kind: draft.kind,
    container: draft.container.trim(),
    fileExtension: draft.fileExtension,
    ...(draft.videoCodec === 'none'
      ? {}
      : {
          video: {
            codec: draft.videoCodec,
            qualityMode: draft.videoQuality,
            ...optionalNumberProperty('crf', draft.videoCrf),
            ...optionalNumberProperty('bitrateKbps', draft.videoBitrate),
            ...optionalNumberProperty('width', draft.width),
            ...optionalNumberProperty('height', draft.height),
            ...optionalNumberProperty('frameRate', draft.frameRate),
          },
        }),
    ...(draft.audioCodec === 'none'
      ? {}
      : {
          audio: {
            codec: draft.audioCodec,
            ...optionalNumberProperty('bitrateKbps', draft.audioBitrate),
            ...(draft.sampleRate
              ? { sampleRate: Number(draft.sampleRate) }
              : {}),
            ...(draft.channels ? { channels: Number(draft.channels) } : {}),
          },
        }),
    metadataPolicy: draft.metadataPolicy,
    chapterPolicy: draft.chapterPolicy,
    subtitlePolicy: draft.subtitlePolicy,
    fastStart: draft.fastStart,
  };
  const result = validateUserPreset(candidate);
  if (!result.valid || !result.preset) {
    throw new TypeError(`Invalid preset: ${result.errors.join('; ')}`);
  }
  return result.preset;
}

function optionalNumberProperty(
  key: string,
  value: string
): Readonly<Record<string, number>> {
  return value ? { [key]: Number(value) } : {};
}

function uniqueCloneId(
  id: string,
  presets: readonly UserExportPreset[]
): string {
  const ids = new Set(presets.map((preset) => preset.id));
  const base = `${id.slice(0, 72)}-copy`;
  if (!ids.has(base)) {
    return base;
  }
  for (let index = 2; index < 1000; index += 1) {
    const suffix = `-copy-${String(index)}`;
    const candidate = `${id.slice(0, 80 - suffix.length)}${suffix}`;
    if (!ids.has(candidate)) {
      return candidate;
    }
  }
  throw new RangeError('Could not create a unique preset clone ID.');
}

function chapterToRange(chapter: EditableChapter): TimeRange {
  return {
    startSeconds: chapter.startSeconds,
    endSeconds: chapter.endSeconds,
    label: chapter.title,
  };
}

function normalizeExtension(value: string): string {
  return value
    .toLowerCase()
    .replace(/^\./u, '')
    .replace(/[^a-z0-9]/gu, '');
}

function detectSubtitleFormat(fileName: string): SubtitleFormat | undefined {
  const extension = fileName.split('.').pop()?.toLowerCase();
  return extension === 'srt'
    ? 'srt'
    : extension === 'vtt'
      ? 'vtt'
      : extension === 'ass' || extension === 'ssa'
        ? 'ass'
        : undefined;
}

function codecForSubtitleFormat(format: SubtitleFormat): string {
  return format === 'srt' ? 'subrip' : format === 'vtt' ? 'webvtt' : 'ass';
}

async function readSmallTextFile(file: File, maximumBytes: number) {
  if (file.size > maximumBytes) {
    throw new RangeError(
      `Import file exceeds the ${String(Math.round(maximumBytes / 1024))} KiB limit.`
    );
  }
  return file.text();
}

function isPositive(value: number | undefined): value is number {
  return value !== undefined && Number.isFinite(value) && value > 0;
}

function integerRangeReason(
  value: string,
  minimum: number,
  maximum: number,
  label: string
): string | undefined {
  const number = Number(value);
  return Number.isSafeInteger(number) && number >= minimum && number <= maximum
    ? undefined
    : `${label} must be an integer from ${String(minimum)} to ${String(maximum)}.`;
}

function optionalIntegerRangeReason(
  value: string,
  minimum: number,
  maximum: number,
  label: string
): string | undefined {
  return value ? integerRangeReason(value, minimum, maximum, label) : undefined;
}

function numberRangeReason(
  value: string,
  minimum: number,
  maximum: number,
  label: string
): string | undefined {
  const number = Number(value);
  return Number.isFinite(number) && number >= minimum && number <= maximum
    ? undefined
    : `${label} must be between ${String(minimum)} and ${String(maximum)}.`;
}

function loudnessTargetsReason(targets: LoudnessTargets): string | undefined {
  if (
    !Number.isFinite(targets.integratedLufs) ||
    targets.integratedLufs < -70 ||
    targets.integratedLufs > -5
  ) {
    return 'Integrated loudness must be between -70 and -5 LUFS.';
  }
  if (
    !Number.isFinite(targets.truePeakDbtp) ||
    targets.truePeakDbtp < -9 ||
    targets.truePeakDbtp > 0
  ) {
    return 'True peak must be between -9 and 0 dBTP.';
  }
  if (
    !Number.isFinite(targets.loudnessRangeLu) ||
    targets.loudnessRangeLu < 1 ||
    targets.loudnessRangeLu > 50
  ) {
    return 'Loudness range must be between 1 and 50 LU.';
  }
  return undefined;
}

function optionalNumber(value: number | undefined): string {
  return value === undefined ? '' : String(value);
}

function formatSeconds(value: number): string {
  return `${value.toFixed(3).replace(/0+$/u, '').replace(/\.$/u, '')} s`;
}

function formatSplitTime(value: number): string {
  return `${value.toFixed(6).replace(/0+$/u, '').replace(/\.$/u, '')} s`;
}
