import { useId, useState } from 'react';
import './StructuralOperations.css';

export type StructuralCapabilityKey =
  | 'trim-fast'
  | 'trim-accurate'
  | 'concat-fast'
  | 'concat-normalized'
  | 'concat-crossfade';

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

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

export interface StructuralSelectedClip {
  readonly id: string;
  readonly label: string;
  readonly sourceInSeconds: number;
  readonly sourceOutSeconds: number;
  readonly sourceDurationSeconds?: number;
  readonly sourceExtension: string;
  readonly hasAudio: boolean;
  readonly hasVideo: boolean;
}

export interface StructuralTimelineClip {
  readonly id: string;
  readonly label: string;
  readonly hasAudio: boolean;
  readonly hasVideo: boolean;
  readonly hasSubtitles: boolean;
  readonly durationSeconds?: number;
}

export interface StructuralCompatibilityDiagnostic {
  readonly code: string;
  readonly severity: 'info' | 'warning' | 'error';
  readonly message: string;
  readonly path?: string;
}

export interface StructuralTimeline {
  /** Clips in their exact export order. */
  readonly clips: readonly StructuralTimelineClip[];
  readonly fastTargetExtension: string;
  readonly fastTargetMuxer: string;
  /**
   * `undefined` means the command layer has not checked the inputs. An empty
   * array means it checked them and found no compatibility diagnostics.
   */
  readonly fastCompatibilityDiagnostics?: readonly StructuralCompatibilityDiagnostic[];
}

export type MissingAudioPolicy = 'insert-silence' | 'drop-all' | 'reject';
export type NormalizedSubtitlePolicy = 'drop-all' | 'reject';

export type StructuralTrimRequest =
  | {
      readonly operation: 'trim';
      readonly mode: 'fast';
      readonly clipId: string;
      readonly startSeconds: number;
      readonly endSeconds: number;
      readonly targetExtension: string;
    }
  | {
      readonly operation: 'trim';
      readonly mode: 'accurate';
      readonly clipId: string;
      readonly startSeconds: number;
      readonly endSeconds: number;
      readonly presetId: string;
    };

export type StructuralConcatRequest =
  | {
      readonly operation: 'concat';
      readonly mode: 'fast';
      readonly clipIds: readonly string[];
      readonly targetExtension: string;
      readonly targetMuxer: string;
    }
  | {
      readonly operation: 'concat';
      readonly mode: 'normalized';
      readonly clipIds: readonly string[];
      readonly presetId: string;
      readonly missingAudioPolicy: MissingAudioPolicy;
      readonly subtitlePolicy: NormalizedSubtitlePolicy;
      readonly crossfadeSeconds: number;
    };

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

export interface StructuralOperationsProps {
  readonly selectedClip?: StructuralSelectedClip;
  readonly timeline?: StructuralTimeline;
  readonly exportPresets?: readonly StructuralExportPresetOption[];
  /**
   * Missing entries fail closed. The integration layer must opt in only after
   * checking the loaded FFmpeg core and the current browser environment.
   */
  readonly availability?: Partial<
    Readonly<Record<StructuralCapabilityKey, StructuralCapabilityStatus>>
  >;
  readonly busy?: boolean;
  readonly onTrim?: RequestHandler<StructuralTrimRequest>;
  readonly onConcat?: RequestHandler<StructuralConcatRequest>;
}

type TrimMode = StructuralTrimRequest['mode'];
type ConcatMode = StructuralConcatRequest['mode'];

export function StructuralOperations({
  selectedClip,
  timeline,
  exportPresets = [],
  availability = {},
  busy = false,
  onTrim,
  onConcat,
}: StructuralOperationsProps) {
  const componentId = useId();
  const [trimMode, setTrimMode] = useState<TrimMode>('fast');
  const [concatMode, setConcatMode] = useState<ConcatMode>('fast');
  const [trimPresetId, setTrimPresetId] = useState('');
  const [concatPresetId, setConcatPresetId] = useState('');
  const [missingAudioPolicy, setMissingAudioPolicy] = useState<
    MissingAudioPolicy | ''
  >('');
  const [subtitlePolicy, setSubtitlePolicy] = useState<
    NormalizedSubtitlePolicy | ''
  >('');
  const [crossfadeSeconds, setCrossfadeSeconds] = useState('0');
  const [pending, setPending] = useState(false);
  const [status, setStatus] = useState<string>();

  const trimPreset = exportPresets.find((preset) => preset.id === trimPresetId);
  const concatPreset = exportPresets.find(
    (preset) => preset.id === concatPresetId
  );
  const trimReason = reasonForTrim({
    mode: trimMode,
    selectedClip,
    preset: trimPreset,
    availability,
    connected: Boolean(onTrim),
    busy: busy || pending,
  });
  const concatReason = reasonForConcat({
    mode: concatMode,
    timeline,
    preset: concatPreset,
    missingAudioPolicy,
    subtitlePolicy,
    crossfadeSeconds,
    availability,
    connected: Boolean(onConcat),
    busy: busy || pending,
  });

  const run = <T,>(
    handler: RequestHandler<T> | undefined,
    request: T,
    successMessage: string
  ) => {
    if (!handler) {
      return;
    }
    setPending(true);
    setStatus(undefined);
    void Promise.resolve()
      .then(() => handler(request))
      .then(() => setStatus(successMessage))
      .catch((error: unknown) => {
        setStatus(
          error instanceof Error
            ? `The operation could not be queued: ${error.message}`
            : 'The operation could not be queued.'
        );
      })
      .finally(() => setPending(false));
  };

  return (
    <section
      className="structural-operations"
      aria-labelledby={`${componentId}-title`}
    >
      <header className="structural-operations__header">
        <div>
          <p className="structural-operations__eyebrow">Structural exports</p>
          <h2 id={`${componentId}-title`}>Trim or join without guesswork</h2>
          <p>
            Choose stream-copy speed or a preset-backed re-encode. Every action
            runs locally in this browser.
          </p>
        </div>
        <span className="structural-operations__local">Local only</span>
      </header>

      <div className="structural-operations__grid">
        <section className="structural-operation-card">
          <header>
            <span className="structural-operation-card__step">1</span>
            <div>
              <h3>Trim the selected clip</h3>
              <p>
                The current timeline in/out points are used as-is; this panel
                does not change the edit.
              </p>
            </div>
          </header>

          {selectedClip ? (
            <dl className="structural-range-summary">
              <div>
                <dt>Clip</dt>
                <dd>{selectedClip.label}</dd>
              </div>
              <div>
                <dt>Current range</dt>
                <dd>
                  {formatTime(selectedClip.sourceInSeconds)}–
                  {formatTime(selectedClip.sourceOutSeconds)}
                </dd>
              </div>
              <div>
                <dt>Export duration</dt>
                <dd>
                  {formatDuration(
                    selectedClip.sourceOutSeconds - selectedClip.sourceInSeconds
                  )}
                </dd>
              </div>
            </dl>
          ) : (
            <p className="structural-empty">
              Select one timeline clip to export its current range.
            </p>
          )}

          <fieldset
            className="structural-choice-cards"
            disabled={busy || pending}
          >
            <legend>Trim method</legend>
            <label>
              <input
                type="radio"
                name={`${componentId}-trim-mode`}
                value="fast"
                checked={trimMode === 'fast'}
                onChange={() => setTrimMode('fast')}
              />
              <span>
                <strong>Fast stream copy</strong>
                <small>
                  No generation loss and usually much faster. The first frame
                  may move to a nearby keyframe, so the boundary is not
                  frame-accurate.
                </small>
              </span>
            </label>
            <label>
              <input
                type="radio"
                name={`${componentId}-trim-mode`}
                value="accurate"
                checked={trimMode === 'accurate'}
                onChange={() => setTrimMode('accurate')}
              />
              <span>
                <strong>Accurate re-encode</strong>
                <small>
                  Decodes around the requested in/out points for practical frame
                  accuracy. It is slower and applies the selected preset&apos;s
                  quality settings.
                </small>
              </span>
            </label>
          </fieldset>

          {trimMode === 'accurate' ? (
            <PresetField
              id={`${componentId}-trim-preset`}
              label="Accurate trim preset"
              value={trimPresetId}
              presets={exportPresets}
              suitable={(preset) =>
                selectedClip?.hasVideo
                  ? preset.supportsVideo
                  : Boolean(selectedClip?.hasAudio && preset.supportsAudio)
              }
              disabled={busy || pending}
              onChange={setTrimPresetId}
            />
          ) : (
            <p className="structural-hint">
              The fast export keeps the source streams and writes{' '}
              <strong>
                .
                {normalizeExtension(selectedClip?.sourceExtension) ||
                  'source format'}
              </strong>
              .
            </p>
          )}

          <StructuralAction
            label={
              trimMode === 'fast' ? 'Export fast trim' : 'Export exact trim'
            }
            reason={trimReason}
            onClick={() => {
              if (!selectedClip) {
                return;
              }
              if (trimMode === 'fast') {
                run(
                  onTrim,
                  {
                    operation: 'trim',
                    mode: 'fast',
                    clipId: selectedClip.id,
                    startSeconds: selectedClip.sourceInSeconds,
                    endSeconds: selectedClip.sourceOutSeconds,
                    targetExtension: normalizeExtension(
                      selectedClip.sourceExtension
                    ),
                  },
                  'Fast trim queued.'
                );
                return;
              }
              if (!trimPreset) {
                return;
              }
              run(
                onTrim,
                {
                  operation: 'trim',
                  mode: 'accurate',
                  clipId: selectedClip.id,
                  startSeconds: selectedClip.sourceInSeconds,
                  endSeconds: selectedClip.sourceOutSeconds,
                  presetId: trimPreset.id,
                },
                'Accurate trim queued.'
              );
            }}
          />
        </section>

        <section className="structural-operation-card">
          <header>
            <span className="structural-operation-card__step">2</span>
            <div>
              <h3>Join the sequential timeline</h3>
              <p>
                Clips are submitted in their visible timeline order. Fast
                concatenation is enabled only after an explicit stream check.
              </p>
            </div>
          </header>

          {timeline ? (
            <TimelineSummary clips={timeline.clips} />
          ) : (
            <p className="structural-empty">
              Add at least two clips to the sequential timeline.
            </p>
          )}

          <fieldset
            className="structural-choice-cards"
            disabled={busy || pending}
          >
            <legend>Join method</legend>
            <label>
              <input
                type="radio"
                name={`${componentId}-concat-mode`}
                value="fast"
                checked={concatMode === 'fast'}
                onChange={() => setConcatMode('fast')}
              />
              <span>
                <strong>Fast compatible concat</strong>
                <small>
                  Copies streams without quality loss. Every clip must have the
                  same stream order, codecs, dimensions, time bases, and audio
                  layout.
                </small>
              </span>
            </label>
            <label>
              <input
                type="radio"
                name={`${componentId}-concat-mode`}
                value="normalized"
                checked={concatMode === 'normalized'}
                onChange={() => setConcatMode('normalized')}
              />
              <span>
                <strong>Normalized re-encode</strong>
                <small>
                  Scales and pads video, normalizes frame rate and audio layout,
                  then encodes the complete sequence with one typed preset.
                </small>
              </span>
            </label>
          </fieldset>

          {concatMode === 'fast' ? (
            <CompatibilityReport
              diagnostics={timeline?.fastCompatibilityDiagnostics}
              clipCount={timeline?.clips.length ?? 0}
            />
          ) : (
            <>
              <PresetField
                id={`${componentId}-concat-preset`}
                label="Normalized concat preset"
                value={concatPresetId}
                presets={exportPresets}
                suitable={(preset) => preset.supportsVideo}
                disabled={busy || pending}
                onChange={setConcatPresetId}
              />
              <MissingAudioPolicyField
                componentId={componentId}
                value={missingAudioPolicy}
                disabled={busy || pending}
                onChange={setMissingAudioPolicy}
              />
              <SubtitlePolicyField
                componentId={componentId}
                value={subtitlePolicy}
                disabled={busy || pending}
                hasSubtitles={Boolean(
                  timeline?.clips.some((clip) => clip.hasSubtitles)
                )}
                onChange={setSubtitlePolicy}
              />
              <label
                className="structural-field"
                htmlFor={`${componentId}-crossfade`}
              >
                <span>Crossfade duration</span>
                <input
                  id={`${componentId}-crossfade`}
                  type="number"
                  aria-label="Crossfade duration"
                  min="0"
                  max="10"
                  step="0.04"
                  inputMode="decimal"
                  value={crossfadeSeconds}
                  disabled={busy || pending}
                  onChange={(event) =>
                    setCrossfadeSeconds(event.currentTarget.value)
                  }
                />
                <small>
                  0 keeps hard cuts. A value from 0.04–10 seconds overlaps video
                  and, when retained, audio at every edit.
                </small>
              </label>
            </>
          )}

          <StructuralAction
            label={
              concatMode === 'fast'
                ? 'Export fast concat'
                : 'Export normalized concat'
            }
            reason={concatReason}
            onClick={() => {
              if (!timeline) {
                return;
              }
              const clipIds = timeline.clips.map((clip) => clip.id);
              if (concatMode === 'fast') {
                run(
                  onConcat,
                  {
                    operation: 'concat',
                    mode: 'fast',
                    clipIds,
                    targetExtension: normalizeExtension(
                      timeline.fastTargetExtension
                    ),
                    targetMuxer: timeline.fastTargetMuxer.trim(),
                  },
                  'Fast concat queued.'
                );
                return;
              }
              if (!concatPreset || !missingAudioPolicy || !subtitlePolicy) {
                return;
              }
              run(
                onConcat,
                {
                  operation: 'concat',
                  mode: 'normalized',
                  clipIds,
                  presetId: concatPreset.id,
                  missingAudioPolicy,
                  subtitlePolicy,
                  crossfadeSeconds: Number(crossfadeSeconds),
                },
                'Normalized concat queued.'
              );
            }}
          />
        </section>
      </div>

      <p
        className="structural-operations__status"
        role="status"
        aria-live="polite"
      >
        {pending ? 'Queueing local operation…' : status}
      </p>
    </section>
  );
}

function TimelineSummary({
  clips,
}: {
  readonly clips: readonly StructuralTimelineClip[];
}) {
  const audioCount = clips.filter((clip) => clip.hasAudio).length;
  const subtitleCount = clips.filter((clip) => clip.hasSubtitles).length;
  const missingAudio = clips.filter((clip) => !clip.hasAudio);
  return (
    <div className="structural-timeline-summary">
      <p>
        <strong>{clips.length}</strong> clips in sequence ·{' '}
        <strong>{audioCount}</strong> with audio ·{' '}
        <strong>{subtitleCount}</strong> with subtitles
      </p>
      {missingAudio.length > 0 ? (
        <p>No audio: {missingAudio.map((clip) => clip.label).join(', ')}</p>
      ) : (
        <p>Every clip has an audio stream.</p>
      )}
    </div>
  );
}

function CompatibilityReport({
  diagnostics,
  clipCount,
}: {
  readonly diagnostics:
    readonly StructuralCompatibilityDiagnostic[] | undefined;
  readonly clipCount: number;
}) {
  if (diagnostics === undefined) {
    return (
      <div className="structural-compatibility structural-compatibility--error">
        <h4>Fast compatibility not checked</h4>
        <p>
          Inspect every input stream before enabling stream-copy concatenation.
        </p>
      </div>
    );
  }
  if (diagnostics.length === 0) {
    return (
      <div className="structural-compatibility structural-compatibility--ok">
        <h4>Fast compatibility passed</h4>
        <p>
          All {clipCount} clips have matching stream properties for stream copy.
        </p>
      </div>
    );
  }
  const hasErrors = diagnostics.some(
    (diagnostic) => diagnostic.severity === 'error'
  );
  return (
    <div
      className={`structural-compatibility ${
        hasErrors
          ? 'structural-compatibility--error'
          : 'structural-compatibility--warning'
      }`}
    >
      <h4>
        {hasErrors ? 'Fast compatibility failed' : 'Fast compatibility notes'}
      </h4>
      <ul>
        {diagnostics.map((diagnostic, index) => (
          <li key={`${diagnostic.code}-${diagnostic.path ?? index}`}>
            <span>{diagnostic.severity}</span>
            <span>
              {diagnostic.message}
              {diagnostic.path ? (
                <small>Checked field: {diagnostic.path}</small>
              ) : null}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

function PresetField({
  id,
  label,
  value,
  presets,
  suitable,
  disabled,
  onChange,
}: {
  readonly id: string;
  readonly label: string;
  readonly value: string;
  readonly presets: readonly StructuralExportPresetOption[];
  readonly suitable: (preset: StructuralExportPresetOption) => boolean;
  readonly disabled: boolean;
  readonly onChange: (value: string) => void;
}) {
  const selectableCount = presets.filter(
    (preset) => suitable(preset) && !preset.disabledReason
  ).length;
  return (
    <label className="structural-field" htmlFor={id}>
      <span>{label}</span>
      <select
        id={id}
        aria-label={label}
        value={value}
        disabled={disabled}
        onChange={(event) => onChange(event.currentTarget.value)}
      >
        <option value="">Choose an available preset…</option>
        {presets.map((preset) => {
          const unsuitable = !suitable(preset);
          const reason =
            preset.disabledReason ??
            (unsuitable ? 'wrong media type for this operation' : undefined);
          return (
            <option
              key={preset.id}
              value={preset.id}
              disabled={Boolean(reason)}
            >
              {preset.name} · .{normalizeExtension(preset.fileExtension)}
              {reason ? ` — unavailable: ${reason}` : ''}
            </option>
          );
        })}
      </select>
      <small>
        {selectableCount > 0
          ? `${selectableCount} verified preset${
              selectableCount === 1 ? '' : 's'
            } available.`
          : 'No suitable preset has passed the current FFmpeg capability check.'}
      </small>
    </label>
  );
}

function MissingAudioPolicyField({
  componentId,
  value,
  disabled,
  onChange,
}: {
  readonly componentId: string;
  readonly value: MissingAudioPolicy | '';
  readonly disabled: boolean;
  readonly onChange: (value: MissingAudioPolicy) => void;
}) {
  const options: readonly {
    readonly value: MissingAudioPolicy;
    readonly title: string;
    readonly detail: string;
  }[] = [
    {
      value: 'insert-silence',
      title: 'Insert silence',
      detail: 'Keep an audio track and fill each silent clip to its duration.',
    },
    {
      value: 'drop-all',
      title: 'Drop all audio',
      detail: 'Export one video-only sequence, even from clips with audio.',
    },
    {
      value: 'reject',
      title: 'Reject mixed audio',
      detail: 'Stop if some clips have audio and others do not.',
    },
  ];
  return (
    <fieldset
      className="structural-policy"
      disabled={disabled}
      aria-describedby={`${componentId}-audio-policy-hint`}
    >
      <legend>Missing-audio policy</legend>
      <p id={`${componentId}-audio-policy-hint`}>
        Required: choose how a normalized sequence handles clips without audio.
      </p>
      <div>
        {options.map((option) => (
          <label key={option.value}>
            <input
              type="radio"
              name={`${componentId}-audio-policy`}
              value={option.value}
              checked={value === option.value}
              onChange={() => onChange(option.value)}
            />
            <span>
              <strong>{option.title}</strong>
              <small>{option.detail}</small>
            </span>
          </label>
        ))}
      </div>
    </fieldset>
  );
}

function SubtitlePolicyField({
  componentId,
  value,
  disabled,
  hasSubtitles,
  onChange,
}: {
  readonly componentId: string;
  readonly value: NormalizedSubtitlePolicy | '';
  readonly disabled: boolean;
  readonly hasSubtitles: boolean;
  readonly onChange: (value: NormalizedSubtitlePolicy) => void;
}) {
  return (
    <fieldset
      className="structural-policy"
      disabled={disabled}
      aria-describedby={`${componentId}-subtitle-policy-hint`}
    >
      <legend>Subtitle policy</legend>
      <p id={`${componentId}-subtitle-policy-hint`}>
        Required: normalized concat has no reviewed, container-safe subtitle
        retiming path. The selected preset and loaded FFmpeg capabilities still
        gate the final export.
      </p>
      <div>
        <label>
          <input
            type="radio"
            name={`${componentId}-subtitle-policy`}
            value="reject"
            checked={value === 'reject'}
            onChange={() => onChange('reject')}
          />
          <span>
            <strong>Reject subtitle input</strong>
            <small>
              {hasSubtitles
                ? 'Keep the operation blocked because at least one clip contains subtitles.'
                : 'Proceed only while every clip remains subtitle-free.'}
            </small>
          </span>
        </label>
        <label>
          <input
            type="radio"
            name={`${componentId}-subtitle-policy`}
            value="drop-all"
            checked={value === 'drop-all'}
            onChange={() => onChange('drop-all')}
          />
          <span>
            <strong>Drop all subtitles</strong>
            <small>
              Explicitly omit subtitle streams from every normalized input. This
              cannot be undone in the output.
            </small>
          </span>
        </label>
      </div>
    </fieldset>
  );
}

function StructuralAction({
  label,
  reason,
  onClick,
}: {
  readonly label: string;
  readonly reason?: string;
  readonly onClick: () => void;
}) {
  const reasonId = useId();
  return (
    <div className="structural-action">
      <button
        type="button"
        disabled={Boolean(reason)}
        aria-describedby={reason ? reasonId : undefined}
        onClick={onClick}
      >
        {label}
      </button>
      {reason ? <small id={reasonId}>{reason}</small> : null}
    </div>
  );
}

function reasonForTrim({
  mode,
  selectedClip,
  preset,
  availability,
  connected,
  busy,
}: {
  readonly mode: TrimMode;
  readonly selectedClip: StructuralSelectedClip | undefined;
  readonly preset: StructuralExportPresetOption | undefined;
  readonly availability: StructuralOperationsProps['availability'];
  readonly connected: boolean;
  readonly busy: boolean;
}): string | undefined {
  if (busy) {
    return 'Another local operation is currently running.';
  }
  if (!selectedClip) {
    return 'Select a timeline clip first.';
  }
  if (!validRange(selectedClip)) {
    return 'The selected clip must have a finite in/out range inside its source.';
  }
  if (mode === 'fast' && !normalizeExtension(selectedClip.sourceExtension)) {
    return 'The selected source has no safe output extension.';
  }
  if (mode === 'accurate') {
    if (!preset) {
      return 'Choose an available typed preset for the accurate trim.';
    }
    if (preset.disabledReason) {
      return preset.disabledReason;
    }
    if (
      (selectedClip.hasVideo && !preset.supportsVideo) ||
      (!selectedClip.hasVideo && selectedClip.hasAudio && !preset.supportsAudio)
    ) {
      return 'The selected preset does not encode this clip type.';
    }
  }
  if (!connected) {
    return 'This action has not been connected to the job runner.';
  }
  return capabilityReason(
    availability?.[mode === 'fast' ? 'trim-fast' : 'trim-accurate']
  );
}

function reasonForConcat({
  mode,
  timeline,
  preset,
  missingAudioPolicy,
  subtitlePolicy,
  crossfadeSeconds,
  availability,
  connected,
  busy,
}: {
  readonly mode: ConcatMode;
  readonly timeline: StructuralTimeline | undefined;
  readonly preset: StructuralExportPresetOption | undefined;
  readonly missingAudioPolicy: MissingAudioPolicy | '';
  readonly subtitlePolicy: NormalizedSubtitlePolicy | '';
  readonly crossfadeSeconds: string;
  readonly availability: StructuralOperationsProps['availability'];
  readonly connected: boolean;
  readonly busy: boolean;
}): string | undefined {
  if (busy) {
    return 'Another local operation is currently running.';
  }
  if (!timeline || timeline.clips.length < 2) {
    return 'Add at least two timeline clips in the order to be joined.';
  }
  const clipIds = timeline.clips.map((clip) => clip.id);
  if (
    clipIds.some((id) => id.trim() === '') ||
    new Set(clipIds).size !== clipIds.length
  ) {
    return 'Every timeline clip must have a unique nonempty ID.';
  }
  if (mode === 'fast') {
    if (!normalizeExtension(timeline.fastTargetExtension)) {
      return 'The sequence has no safe fast-concat output extension.';
    }
    if (timeline.fastCompatibilityDiagnostics === undefined) {
      return 'Fast concat remains disabled until every input stream is checked.';
    }
    if (
      timeline.fastCompatibilityDiagnostics.some(
        (diagnostic) => diagnostic.severity === 'error'
      )
    ) {
      return 'Resolve the reported stream incompatibilities or use normalized concat.';
    }
    if (!/^[a-z0-9][a-z0-9_-]{0,31}$/u.test(timeline.fastTargetMuxer.trim())) {
      return 'The fast-concat output muxer is unknown.';
    }
  } else {
    if (timeline.clips.some((clip) => !clip.hasVideo)) {
      return 'Normalized concat currently requires video in every clip.';
    }
    if (!preset) {
      return 'Choose an available typed video preset for normalized concat.';
    }
    if (preset.disabledReason) {
      return preset.disabledReason;
    }
    if (!preset.supportsVideo) {
      return 'Normalized concat requires a video preset.';
    }
    if (!missingAudioPolicy) {
      return 'Choose an explicit missing-audio policy.';
    }
    if (!subtitlePolicy) {
      return 'Choose an explicit subtitle policy.';
    }
    if (
      subtitlePolicy === 'reject' &&
      timeline.clips.some((clip) => clip.hasSubtitles)
    ) {
      return 'This sequence contains subtitles. Explicitly drop all subtitles, or remove them before normalized concat.';
    }
    const crossfade = Number(crossfadeSeconds);
    if (
      !Number.isFinite(crossfade) ||
      crossfade < 0 ||
      (crossfade > 0 && (crossfade < 0.04 || crossfade > 10))
    ) {
      return 'Crossfade duration must be 0, or a value from 0.04–10 seconds.';
    }
    if (crossfade > 0) {
      for (const [index, clip] of timeline.clips.entries()) {
        if (clip.durationSeconds === undefined) {
          continue;
        }
        const required =
          index === 0 || index === timeline.clips.length - 1
            ? crossfade
            : crossfade * 2;
        if (clip.durationSeconds < required) {
          return `Clip ${index + 1} is too short for ${crossfade} second crossfades at its edit boundaries.`;
        }
      }
    }
  }
  if (!connected) {
    return 'This action has not been connected to the job runner.';
  }
  const baseReason = capabilityReason(
    availability?.[mode === 'fast' ? 'concat-fast' : 'concat-normalized']
  );
  if (baseReason || mode === 'fast' || Number(crossfadeSeconds) === 0) {
    return baseReason;
  }
  return capabilityReason(availability?.['concat-crossfade']);
}

function capabilityReason(
  status: StructuralCapabilityStatus | undefined
): string | undefined {
  if (!status?.available) {
    return (
      status?.reason ??
      'The required browser and FFmpeg capabilities have not been verified.'
    );
  }
  return undefined;
}

function validRange(clip: StructuralSelectedClip): boolean {
  if (
    !Number.isFinite(clip.sourceInSeconds) ||
    !Number.isFinite(clip.sourceOutSeconds) ||
    clip.sourceInSeconds < 0 ||
    clip.sourceOutSeconds <= clip.sourceInSeconds
  ) {
    return false;
  }
  return (
    clip.sourceDurationSeconds === undefined ||
    (Number.isFinite(clip.sourceDurationSeconds) &&
      clip.sourceDurationSeconds > 0 &&
      clip.sourceOutSeconds <= clip.sourceDurationSeconds)
  );
}

function normalizeExtension(extension: string | undefined): string {
  const normalized = (extension ?? '')
    .trim()
    .replace(/^\.+/u, '')
    .toLowerCase();
  return /^[a-z0-9]{1,10}$/u.test(normalized) ? normalized : '';
}

function formatDuration(seconds: number): string {
  if (!Number.isFinite(seconds) || seconds < 0) {
    return 'Invalid range';
  }
  return `${seconds.toFixed(3)} s`;
}

function formatTime(seconds: number): string {
  if (!Number.isFinite(seconds) || seconds < 0) {
    return '—';
  }
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const remainder = seconds % 60;
  return `${hours.toString().padStart(2, '0')}:${minutes
    .toString()
    .padStart(2, '0')}:${remainder.toFixed(3).padStart(6, '0')}`;
}
