import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import WaveSurfer from "wavesurfer.js";
import RegionsPlugin, { type Region } from "wavesurfer.js/plugins/regions";

import { API_BASE, api } from "../api/client";
import type { AudioAsset, AudioClip, TeamSnapEvent } from "../api/types";
import { ClipSummaryRow } from "../components/ClipSummaryRow";
import { useClipPlayback } from "../hooks/useClipPlayback";
import { useWalkupContext } from "../hooks/useWalkupContext";
import { queryClient } from "../lib/queryClient";
import { formatClipRange, formatPlaybackPosition } from "../lib/media";
import { formatGameTitle, formatMemberName } from "../lib/teamsnapHelpers";

const MEDIA_ACCEPT =
  ".mp3,.m4a,.aac,.wav,.ogg,.oga,.flac,.mp4,.m4v,.mov,audio/*,video/*,application/octet-stream";
const DEFAULT_CLIP_LENGTH_MS = 30_000;
const END_SHORTCUT_LENGTH_MS = 90_000;
const SAVE_FADE_OUT_MS = 1000;
const TRIM_NUDGE_MS = 100;
const TRIM_STEP_MS = 100;
const TRIM_ZOOM_WINDOW_MS = 3_000;
const TRIM_ZOOM_SLIDER_MAX = 100;

function clipsQueryKey(teamId: string | null, playerId: string | null, includeHidden: boolean) {
  return ["clips", teamId, playerId, includeHidden ? "all" : "visible"] as const;
}

function clipsQueryPrefix(teamId: string | null, playerId: string | null) {
  return ["clips", teamId, playerId] as const;
}

type WalkupClipSourceMode = "upload" | "url" | "existing";

type WalkupClipModalState =
  | { mode: "create" }
  | { mode: "edit"; clip: AudioClip };

type BootstrapIconName =
  | "play"
  | "stop"
  | "three-dots"
  | "pencil-square"
  | "plus-lg"
  | "x-lg"
  | "chevron-up"
  | "chevron-down"
  | "pin-fill"
  | "eye"
  | "eye-slash";
type TrimFocusEdge = "start" | "end";
type SourceCreationProgress = {
  label: string;
  detail: string;
  percent: number | null;
};

export function LibraryPage() {
  const walkup = useWalkupContext();
  const teamId = walkup.selectedTeamId;
  const playerId = walkup.currentPlayerId;
  const [walkupClipModal, setWalkupClipModal] = useState<WalkupClipModalState | null>(null);
  const [clipPinModalClipId, setClipPinModalClipId] = useState<number | null>(null);
  const [manageMediaOpen, setManageMediaOpen] = useState(false);
  const {
    activeKey: previewKey,
    currentTimeMs: previewTimeMs,
    fadeOutClip,
    playClip: playClipPreview,
    stopClip: stopPreview,
  } = useClipPlayback();

  const assetsQuery = useQuery({
    queryKey: ["assets", teamId, playerId],
    queryFn: () => api.listAssets(teamId, playerId),
    enabled: Boolean(teamId && playerId),
    networkMode: "always",
    retry: 0,
  });
  const clipsQuery = useQuery({
    queryKey: clipsQueryKey(teamId, playerId, true),
    queryFn: () => api.listClips(teamId, playerId, true),
    enabled: Boolean(teamId && playerId),
    networkMode: "always",
    retry: 0,
  });
  const pinsQuery = useQuery({
    queryKey: ["pins", teamId, playerId],
    queryFn: () => api.listPins(playerId),
    enabled: Boolean(playerId),
    networkMode: "always",
    retry: 0,
  });
  const orderedClips = useMemo(
    () =>
      [...(clipsQuery.data ?? [])].sort((left, right) => {
        if (left.sort_order !== right.sort_order) {
          return left.sort_order - right.sort_order;
        }
        return right.created_at.localeCompare(left.created_at);
      }),
    [clipsQuery.data],
  );
  const pinnedAssignmentsByClipAndGame = useMemo(() => {
    return new Map((pinsQuery.data ?? []).map((assignment) => [`${assignment.external_game_id}:${assignment.clip_id}`, assignment]));
  }, [pinsQuery.data]);
  const pinCountByClipId = useMemo(() => {
    const counts = new Map<number, number>();
    for (const assignment of pinsQuery.data ?? []) {
      counts.set(assignment.clip_id, (counts.get(assignment.clip_id) ?? 0) + 1);
    }
    return counts;
  }, [pinsQuery.data]);
  const isLoadingLibraryData =
    walkup.sessionQuery.isLoading ||
    walkup.teamsQuery.isLoading ||
    walkup.membersQuery.isLoading ||
    walkup.eventsQuery.isLoading ||
    (walkup.isTeamSnap && Boolean(teamId && playerId) && (assetsQuery.isLoading || clipsQuery.isLoading || pinsQuery.isLoading));

  const deleteClipMutation = useMutation({
    mutationFn: (clipId: number) => api.deleteClip(clipId, playerId),
    onSuccess: async () => {
      stopPreview();
      await queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) });
    },
  });

  const reorderClipsMutation = useMutation({
    mutationFn: (clipIds: number[]) =>
      api.reorderClips({
        external_team_id: teamId,
        owner_external_player_id: playerId,
        clip_ids: clipIds,
      }),
    onSuccess: async () => {
      await queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) });
    },
  });

  const toggleHiddenMutation = useMutation({
    mutationFn: async ({ clip, hidden }: { clip: AudioClip; hidden: boolean }) =>
      api.updateClip(
        clip.id,
        {
          label: clip.label,
          start_ms: clip.start_ms,
          end_ms: clip.end_ms,
          sort_order: clip.sort_order,
          hidden,
        },
        playerId,
      ),
    onSuccess: async () => {
      await Promise.all([
        queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
        queryClient.invalidateQueries({ queryKey: ["assignments"] }),
        queryClient.invalidateQueries({ queryKey: ["prep"] }),
      ]);
    },
  });

  const togglePinMutation = useMutation({
    mutationFn: async ({ clipId, gameId }: { clipId: number; gameId: string }) => {
      const assignment = (pinsQuery.data ?? []).find(
        (entry) => entry.clip_id === clipId && entry.external_game_id === gameId,
      );
      if (assignment) {
        await api.deleteAssignment(gameId, assignment.id, playerId);
        return;
      }

      await api.createAssignment(gameId, {
        external_team_id: teamId,
        external_player_id: playerId,
        clip_id: clipId,
        batting_slot: null,
        status: "ready",
      });
    },
    onSuccess: async () => {
      await Promise.all([
        queryClient.invalidateQueries({ queryKey: ["pins", teamId, playerId] }),
        queryClient.invalidateQueries({ queryKey: ["assignments"] }),
        queryClient.invalidateQueries({ queryKey: ["prep"] }),
      ]);
    },
  });

  async function playPreview(clip: AudioClip, startMsOverride?: number, endMsOverride?: number) {
    await playClipPreview({
      key: String(clip.id),
      url: clip.normalized_url,
      startMs: startMsOverride ?? clip.start_ms,
      endMs: endMsOverride ?? clip.end_ms,
    });
  }

  function openCreateWalkupClip() {
    setWalkupClipModal({ mode: "create" });
  }

  function closeCreateWalkupClip() {
    stopPreview();
    setWalkupClipModal(null);
  }

  function openEditWalkupClip(clip: AudioClip) {
    setWalkupClipModal({ mode: "edit", clip });
  }

  function openManageMedia() {
    setManageMediaOpen(true);
  }

  function closeManageMedia() {
    stopPreview();
    setManageMediaOpen(false);
  }

  if (isLoadingLibraryData) {
    return (
      <section className="page-grid">
        <div className="panel gameday-loading-panel" role="status" aria-live="polite">
          <div className="gameday-loading-spinner spinner-border text-primary" aria-hidden="true" />
          <div className="gameday-loading-copy">
            <span className="gameday-loading-label">Waiting for TeamSnap</span>
            <strong>Loading WalkUp clips</strong>
            <span className="muted">Teams, roster details, and clip data are being fetched.</span>
          </div>
        </div>
      </section>
    );
  }

  if (!walkup.isTeamSnap) {
    return <section className="page-grid"><div className="panel">Reconnect with TeamSnap to manage WalkUp clips.</div></section>;
  }

  if (!teamId || !playerId) {
    return (
      <section className="page-grid">
        <div className="panel">No player record was found on the selected team, so this account cannot add WalkUp clips yet.</div>
      </section>
    );
  }

  return (
    <section className="page-grid">
      <div className="panel walkup-library-panel">
        <div className="section-header clip-list-header">
          <div>
            <h2>My Clips</h2>
            <div className="panel-subtitle">{walkup.currentPlayer ? formatMemberName(walkup.currentPlayer) : "Selected Player"}</div>
          </div>
          <button
            type="button"
            className="btn btn-primary clip-list-add-button"
            onClick={() => openCreateWalkupClip()}
            aria-label="Create walk up clip"
            title="Create walk up clip"
          >
            <BootstrapIcon name="plus-lg" />
          </button>
        </div>
        <div className="stack">
          {orderedClips.map((clip, index) => (
            <WalkupClipCard
              key={clip.id}
              clip={clip}
              isPlaying={previewKey === String(clip.id)}
              onTogglePlayback={() => void playPreview(clip)}
              onEdit={() => openEditWalkupClip(clip)}
              onMoveUp={() => {
                const nextOrder = [...orderedClips];
                if (index <= 0) {
                  return;
                }
                const [movedClip] = nextOrder.splice(index, 1);
                nextOrder.splice(index - 1, 0, movedClip);
                void reorderClipsMutation.mutateAsync(nextOrder.map((item) => item.id));
              }}
              onMoveDown={() => {
                const nextOrder = [...orderedClips];
                if (index >= nextOrder.length - 1) {
                  return;
                }
                const [movedClip] = nextOrder.splice(index, 1);
                nextOrder.splice(index + 1, 0, movedClip);
                void reorderClipsMutation.mutateAsync(nextOrder.map((item) => item.id));
              }}
              canMoveUp={index > 0}
              canMoveDown={index < orderedClips.length - 1}
              pinCount={pinCountByClipId.get(clip.id) ?? 0}
              onOpenPinModal={() => setClipPinModalClipId(clip.id)}
              onToggleHidden={() => {
                void toggleHiddenMutation.mutateAsync({ clip, hidden: !clip.hidden });
              }}
              isHidden={clip.hidden}
            />
          ))}
          {!clipsQuery.isLoading && !orderedClips.length ? (
            <div className="muted">No WalkUp clips created yet. Open the modal to make the first one.</div>
          ) : null}
          {deleteClipMutation.error instanceof Error ? <div className="muted">{deleteClipMutation.error.message}</div> : null}
        </div>
      </div>
      <div className="panel">
        <div className="section-header">
          <h2>Uploaded media</h2>
          <div className="muted">Review the source files behind your WalkUp clips. You can rename or delete uploads here.</div>
        </div>
        <div className="row walkup-panel-actions">
          <button type="button" className="btn btn-outline-secondary" onClick={openManageMedia}>
            Manage uploaded media
          </button>
        </div>
      </div>
      {walkupClipModal ? (
        <WalkupClipModal
          state={walkupClipModal}
          assets={assetsQuery.data ?? []}
          teamId={teamId}
          playerId={playerId}
          previewTimeMs={previewTimeMs}
          fadeOutPreview={fadeOutClip}
          playPreview={playPreview}
          onClose={closeCreateWalkupClip}
          stopPreview={stopPreview}
          onDeleteClip={async (clipId: number) => {
            stopPreview();
            await deleteClipMutation.mutateAsync(clipId);
          }}
          isDeletingClip={deleteClipMutation.isPending}
        />
      ) : null}
      {clipPinModalClipId !== null ? (
        <PinToGameModal
          clip={orderedClips.find((item) => item.id === clipPinModalClipId) ?? null}
          games={walkup.games}
          pinnedAssignmentsByClipAndGame={pinnedAssignmentsByClipAndGame}
          onClose={() => setClipPinModalClipId(null)}
          onTogglePin={(gameId, clipId) => {
            void togglePinMutation.mutateAsync({ clipId, gameId });
          }}
          isTogglingPin={togglePinMutation.isPending}
        />
      ) : null}
      {manageMediaOpen ? (
        <ManageUploadedMediaModal
          assets={assetsQuery.data ?? []}
          teamId={teamId}
          playerId={playerId}
          onClose={closeManageMedia}
          stopPreview={stopPreview}
        />
      ) : null}
    </section>
  );
}

function BootstrapIcon({ name }: { name: BootstrapIconName }) {
  const iconNameMap: Record<BootstrapIconName, string> = {
    play: "play-fill",
    stop: "stop-fill",
    "three-dots": "three-dots",
    "pencil-square": "pencil-square",
    "plus-lg": "plus-lg",
    "x-lg": "x-lg",
    "chevron-up": "chevron-up",
    "chevron-down": "chevron-down",
    "pin-fill": "pin-fill",
    eye: "eye",
    "eye-slash": "eye-slash",
  };

  return <i className={`bi bi-${iconNameMap[name]} bootstrap-icon`} aria-hidden="true" />;
}

function WalkupClipModal({
  state,
  assets,
  teamId,
  playerId,
  previewTimeMs,
  fadeOutPreview,
  playPreview,
  onClose,
  stopPreview,
  onDeleteClip,
  isDeletingClip,
}: {
  state: WalkupClipModalState;
  assets: AudioAsset[];
  teamId: string;
  playerId: string;
  previewTimeMs: number | null;
  fadeOutPreview: (durationMs?: number) => void;
  playPreview: (clip: AudioClip, startMs?: number, endMs?: number) => Promise<void>;
  onClose: () => void;
  stopPreview: () => void;
  onDeleteClip: (clipId: number) => Promise<void>;
  isDeletingClip: boolean;
}) {
  const isCreateMode = state.mode === "create";
  const [step, setStep] = useState<"source" | "editor">(isCreateMode ? "source" : "editor");
  const [sourceMode, setSourceMode] = useState<WalkupClipSourceMode>("upload");
  const [sourceTitle, setSourceTitle] = useState("");
  const [draftLabel, setDraftLabel] = useState(state.mode === "edit" ? state.clip.label ?? "" : "");
  const [file, setFile] = useState<File | null>(null);
  const [fileInputKey, setFileInputKey] = useState(0);
  const [importUrl, setImportUrl] = useState("");
  const [existingAssetId, setExistingAssetId] = useState("");
  const [draftClip, setDraftClip] = useState<AudioClip | null>(state.mode === "edit" ? state.clip : null);
  const [draftStartMs, setDraftStartMs] = useState(state.mode === "edit" ? state.clip.start_ms : 0);
  const [draftEndMs, setDraftEndMs] = useState(state.mode === "edit" ? state.clip.end_ms : DEFAULT_CLIP_LENGTH_MS);
  const [sourceProgress, setSourceProgress] = useState<SourceCreationProgress | null>(null);

  useEffect(() => {
    const previousOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.body.style.overflow = previousOverflow;
    };
  }, []);

  useEffect(() => {
    if (state.mode === "edit") {
      setStep("editor");
      setDraftClip(state.clip);
      setDraftStartMs(state.clip.start_ms);
      setDraftEndMs(state.clip.end_ms);
      setDraftLabel(state.clip.label ?? "");
    }
  }, [state]);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") {
        stopPreview();
        onClose();
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, [onClose, stopPreview]);

  const resolveCreatedClip = async (assetId: number) => {
    setSourceProgress({
      label: "Loading clip",
      detail: "Refreshing the library with the generated WalkUp clip.",
      percent: null,
    });
    await Promise.all([
      queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }),
      queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
    ]);
    const refreshedClips = await queryClient.fetchQuery({
      queryKey: clipsQueryKey(teamId, playerId, true),
      queryFn: () => api.listClips(teamId, playerId, true),
    });
    const createdClip = refreshedClips.find((clip) => clip.asset_id === assetId);
    if (!createdClip) {
      throw new Error("The new walk up clip could not be found after saving the media.");
    }
    return createdClip;
  };

  const createSourceMutation = useMutation({
    mutationFn: async () => {
      if (sourceMode === "upload") {
        if (!file) {
          throw new Error("Select a media file first");
        }
        setSourceProgress({
          label: "Uploading file",
          detail: "Sending the media file to the server.",
          percent: 0,
        });
        const asset = await api.uploadAsset({
          teamId,
          playerId,
          title: sourceTitle.trim() || file.name,
          file,
          onUploadProgress: (percent) => {
            setSourceProgress({
              label: "Uploading file",
              detail: "Sending the media file to the server.",
              percent,
            });
          },
          onProcessingStart: () => {
            setSourceProgress({
              label: "Generating waveform",
              detail: "Upload complete. Normalizing audio and building the waveform preview.",
              percent: null,
            });
          },
        });
        const clip = await resolveCreatedClip(asset.id);
        return clip;
      }

      if (sourceMode === "url") {
        if (!importUrl.trim()) {
          throw new Error("Paste a media URL first");
        }
        setSourceProgress({
          label: "Importing media",
          detail: "Downloading the media, normalizing audio, and generating the waveform.",
          percent: null,
        });
        const asset = await api.importAssetFromUrl({
          external_team_id: teamId,
          owner_external_player_id: playerId,
          url: importUrl.trim(),
          title: sourceTitle.trim() || undefined,
        });
        const clip = await resolveCreatedClip(asset.id);
        return clip;
      }

      if (!existingAssetId) {
        throw new Error("Choose an existing media file first");
      }

      setSourceProgress({
        label: "Creating clip",
        detail: "Creating a WalkUp clip from the selected media.",
        percent: null,
      });
      const clip = await api.createClip({
        asset_id: Number(existingAssetId),
        external_team_id: teamId,
        owner_external_player_id: playerId,
        label: draftLabel.trim() || "WalkUp clip",
        start_ms: 0,
        end_ms: DEFAULT_CLIP_LENGTH_MS,
      });
      await queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) });
      return clip;
    },
    onSuccess: (clip) => {
      setSourceProgress(null);
      setDraftClip(clip);
      setDraftStartMs(clip.start_ms);
      setDraftEndMs(clip.end_ms);
      setDraftLabel(clip.label || "WalkUp clip");
      setStep("editor");
    },
    onError: () => {
      setSourceProgress(null);
    },
  });

  function resetToSource() {
    stopPreview();
    setSourceProgress(null);
    setStep("source");
    setDraftClip(null);
    setFileInputKey((current) => current + 1);
  }

  function handleClose() {
    stopPreview();
    onClose();
  }

  function handleSourceSubmit(event: FormEvent) {
    event.preventDefault();
    void createSourceMutation.mutateAsync();
  }

  return (
    <div
      className="walkup-modal-backdrop"
      role="presentation"
      onClick={handleClose}
    >
      <section
        className="walkup-modal card shadow-lg border-0 w-100"
        role="dialog"
        aria-modal="true"
        aria-labelledby="walkup-clip-create-title"
        onClick={(event) => event.stopPropagation()}
      >
        <div className="walkup-modal-header">
          <div>
            <p className="eyebrow mb-1">{isCreateMode ? "Create walk up clip" : "Edit walk up clip"}</p>
            <h2 id="walkup-clip-create-title" className="h3 mb-0">
              {step === "source" ? "Choose a source" : isCreateMode ? "Trim and name the clip" : "Edit metadata"}
            </h2>
          </div>
          <button
            type="button"
            className="btn btn-outline-secondary btn-sm icon-button"
            onClick={handleClose}
            aria-label="Close modal"
            title="Close modal"
          >
            <BootstrapIcon name="x-lg" />
          </button>
        </div>
        <div className="walkup-modal-body">
          <nav aria-label="WalkUp clip steps">
            <ol className="breadcrumb walkup-step-breadcrumb mb-0">
              <li className={`breadcrumb-item${step === "source" ? " active" : ""}`} aria-current={step === "source" ? "page" : undefined}>
                Source
              </li>
              <li className={`breadcrumb-item${step === "editor" ? " active" : ""}`} aria-current={step === "editor" ? "page" : undefined}>
                Trim and metadata
              </li>
            </ol>
          </nav>

          {step === "source" ? (
            <form className="stack" onSubmit={handleSourceSubmit} aria-busy={createSourceMutation.isPending}>
              <ul className="nav nav-tabs" role="tablist" aria-label="WalkUp clip source">
                <li className="nav-item" role="presentation">
                  <button
                    id="walkup-upload-tab"
                    type="button"
                    role="tab"
                    className={`nav-link${sourceMode === "upload" ? " active" : ""}`}
                    aria-selected={sourceMode === "upload"}
                    aria-controls="walkup-upload-panel"
                    disabled={createSourceMutation.isPending}
                    onClick={() => setSourceMode("upload")}
                  >
                    File
                  </button>
                </li>
                <li className="nav-item" role="presentation">
                  <button
                    id="walkup-url-tab"
                    type="button"
                    role="tab"
                    className={`nav-link${sourceMode === "url" ? " active" : ""}`}
                    aria-selected={sourceMode === "url"}
                    aria-controls="walkup-url-panel"
                    disabled={createSourceMutation.isPending}
                    onClick={() => setSourceMode("url")}
                  >
                    URL
                  </button>
                </li>
                <li className="nav-item" role="presentation">
                  <button
                    id="walkup-existing-tab"
                    type="button"
                    role="tab"
                    className={`nav-link${sourceMode === "existing" ? " active" : ""}`}
                    aria-selected={sourceMode === "existing"}
                    aria-controls="walkup-existing-panel"
                    disabled={createSourceMutation.isPending}
                    onClick={() => setSourceMode("existing")}
                  >
                    Existing
                  </button>
                </li>
              </ul>
              <div className="tab-content pt-2">
                <div
                  id="walkup-upload-panel"
                  role="tabpanel"
                  aria-labelledby="walkup-upload-tab"
                  className={`tab-pane fade${sourceMode === "upload" ? " show active" : ""}`}
                >
                  <div className="stack">
                    <div className="muted">Upload a local audio file to create a new WalkUp clip.</div>
                    <label className="field">
                      Media title
                      <input
                        value={sourceTitle}
                        onChange={(event) => setSourceTitle(event.target.value)}
                        placeholder="Song or media title"
                        disabled={createSourceMutation.isPending}
                      />
                    </label>
                    <label className="field">
                      Media file
                      <input
                        key={fileInputKey}
                        type="file"
                        accept={MEDIA_ACCEPT}
                        onChange={(event) => setFile(event.target.files?.[0] ?? null)}
                        disabled={createSourceMutation.isPending}
                      />
                    </label>
                    <div className="muted">Upload a file and the backend will create a walk up clip from it.</div>
                  </div>
                </div>
                <div
                  id="walkup-url-panel"
                  role="tabpanel"
                  aria-labelledby="walkup-url-tab"
                  className={`tab-pane fade${sourceMode === "url" ? " show active" : ""}`}
                >
                  <div className="stack">
                    <div className="muted">Paste a link, such as a YouTube URL, and we will download the audio for clip creation.</div>
                    <label className="field">
                      Media title
                      <input
                        value={sourceTitle}
                        onChange={(event) => setSourceTitle(event.target.value)}
                        placeholder="Optional media title"
                        disabled={createSourceMutation.isPending}
                      />
                    </label>
                    <label className="field">
                      Media URL
                      <input
                        value={importUrl}
                        onChange={(event) => setImportUrl(event.target.value)}
                        placeholder="https://..."
                        autoCapitalize="none"
                        autoComplete="off"
                        spellCheck={false}
                        disabled={createSourceMutation.isPending}
                      />
                    </label>
                    <div className="muted">Import a URL and the backend will download and normalize it for the clip flow.</div>
                  </div>
                </div>
                <div
                  id="walkup-existing-panel"
                  role="tabpanel"
                  aria-labelledby="walkup-existing-tab"
                  className={`tab-pane fade${sourceMode === "existing" ? " show active" : ""}`}
                >
                  <div className="stack">
                    <div className="muted">Pick an existing media file to turn into a WalkUp clip.</div>
                    <label className="field">
                      Existing media file
                      <select
                        value={existingAssetId}
                        onChange={(event) => setExistingAssetId(event.target.value)}
                        disabled={createSourceMutation.isPending}
                      >
                        <option value="">Choose a file</option>
                        {assets.map((asset) => (
                          <option key={asset.id} value={String(asset.id)}>
                            {asset.title || asset.original_filename}
                          </option>
                        ))}
                      </select>
                    </label>
                    {existingAssetId ? (
                      <div className="panel-note">The clip will be created from the selected existing media file.</div>
                    ) : (
                      <div className="muted">Choose one of the existing media files in this WalkUp media library.</div>
                    )}
                  </div>
                </div>
              </div>
              {sourceProgress ? <SourceProgressPanel progress={sourceProgress} /> : null}
              <div className="row walkup-modal-actions">
                <button type="submit" className="btn btn-primary" disabled={createSourceMutation.isPending}>
                  {createSourceMutation.isPending ? sourceProgress?.label ?? "Creating..." : "Create walk up clip"}
                </button>
                <button type="button" className="btn btn-outline-secondary" onClick={handleClose}>
                  Cancel
                </button>
              </div>
              {createSourceMutation.error instanceof Error ? (
                <div className="muted">{createSourceMutation.error.message}</div>
              ) : null}
            </form>
          ) : null}

          {step === "editor" && draftClip ? (
            <WalkupClipEditorPanel
              clip={draftClip}
              label={draftLabel}
              setLabel={setDraftLabel}
              startMs={draftStartMs}
              setStartMs={setDraftStartMs}
              endMs={draftEndMs}
              setEndMs={setDraftEndMs}
              onPreview={(startMs, endMs) => void playPreview(draftClip, startMs, endMs)}
              onStopPreview={stopPreview}
              previewTimeMs={previewTimeMs}
              playerId={playerId}
              onSaveComplete={async () => {
                if (previewTimeMs !== null) {
                  fadeOutPreview(SAVE_FADE_OUT_MS);
                  await new Promise<void>((resolve) => {
                    window.setTimeout(resolve, SAVE_FADE_OUT_MS);
                  });
                }
                await Promise.all([
                  queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }),
                  queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
                ]);
                onClose();
              }}
              saveButtonLabel={isCreateMode ? "Save walk up clip" : "Save changes"}
              introText={
                isCreateMode
                  ? `Source: ${draftClip.asset_title}. Use the controls below to trim the clip and update its metadata.`
                  : `Edit ${draftClip.asset_title}. Update the clip range and metadata below.`
              }
              onChangeSource={isCreateMode ? resetToSource : undefined}
              closeLabel={isCreateMode ? "Cancel" : "Close"}
              onClose={handleClose}
              onDelete={async () => onDeleteClip(draftClip.id)}
              isDeleting={isDeletingClip}
            />
          ) : null}
        </div>
      </section>
    </div>
  );
}

function ManageUploadedMediaModal({
  assets,
  teamId,
  playerId,
  onClose,
  stopPreview,
}: {
  assets: AudioAsset[];
  teamId: string;
  playerId: string;
  onClose: () => void;
  stopPreview: () => void;
}) {
  useEffect(() => {
    const previousOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.body.style.overflow = previousOverflow;
    };
  }, []);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") {
        stopPreview();
        onClose();
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, [onClose, stopPreview]);

  const deleteAssetMutation = useMutation({
    mutationFn: (assetId: number) => api.deleteAsset(assetId, playerId),
    onSuccess: async () => {
      stopPreview();
      await Promise.all([
        queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }),
        queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
      ]);
    },
  });

  function handleClose() {
    stopPreview();
    onClose();
  }

  return (
    <div className="walkup-modal-backdrop" role="presentation" onClick={handleClose}>
      <section
        className="walkup-modal card shadow-lg border-0 w-100"
        role="dialog"
        aria-modal="true"
        aria-labelledby="walkup-media-title"
        onClick={(event) => event.stopPropagation()}
      >
        <div className="walkup-modal-header">
          <div>
            <p className="eyebrow mb-1">WalkUp clips</p>
            <h2 id="walkup-media-title" className="h3 mb-0">
              Manage uploaded media
            </h2>
          </div>
          <button
            type="button"
            className="btn-close"
            onClick={handleClose}
            aria-label="Close modal"
            title="Close modal"
          >
          </button>
        </div>
        <div className="walkup-modal-body">
          <div className="panel-note">
            Rename or delete the uploaded media that backs your WalkUp clips. Existing clip edits stay in the clip view.
          </div>
          <div className="stack">
            {assets.map((asset) => (
              <UploadedMediaCard
                key={asset.id}
                asset={asset}
                onDelete={() => void deleteAssetMutation.mutateAsync(asset.id)}
                isDeleting={deleteAssetMutation.isPending}
                teamId={teamId}
                playerId={playerId}
              />
            ))}
            {!assets.length ? <div className="muted">No uploaded media has been added yet.</div> : null}
            {deleteAssetMutation.error instanceof Error ? <div className="muted">{deleteAssetMutation.error.message}</div> : null}
          </div>
        </div>
      </section>
    </div>
  );
}

function WalkupClipCard({
  clip,
  isPlaying,
  onTogglePlayback,
  onEdit,
  onMoveUp,
  onMoveDown,
  canMoveUp,
  canMoveDown,
  pinCount,
  onOpenPinModal,
  onToggleHidden,
  isHidden,
}: {
  clip: AudioClip;
  isPlaying: boolean;
  onTogglePlayback: () => void;
  onEdit: () => void;
  onMoveUp: () => void;
  onMoveDown: () => void;
  canMoveUp: boolean;
  canMoveDown: boolean;
  pinCount: number;
  onOpenPinModal: () => void;
  onToggleHidden: () => void;
  isHidden: boolean;
}) {
  const [menuOpen, setMenuOpen] = useState(false);
  const [menuDirection, setMenuDirection] = useState<"down" | "up">("down");
  const menuRef = useRef<HTMLDivElement | null>(null);
  const menuButtonRef = useRef<HTMLButtonElement | null>(null);
  const menuPanelRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    if (!menuOpen) {
      return;
    }

    const frame = window.requestAnimationFrame(() => {
      const buttonRect = menuButtonRef.current?.getBoundingClientRect();
      const menuHeight = menuPanelRef.current?.getBoundingClientRect().height ?? 0;
      if (!buttonRect || !menuHeight) {
        setMenuDirection("down");
        return;
      }

      const openDownSpace = window.innerHeight - buttonRect.bottom;
      const openUpSpace = buttonRect.top;
      setMenuDirection(openDownSpace < menuHeight + 16 && openUpSpace > menuHeight ? "up" : "down");
    });

    function handlePointerDown(event: PointerEvent) {
      if (!menuRef.current?.contains(event.target as Node)) {
        setMenuOpen(false);
      }
    }

    function handleKeyDown(event: KeyboardEvent) {
      if (event.key === "Escape") {
        setMenuOpen(false);
      }
    }

    document.addEventListener("pointerdown", handlePointerDown);
    document.addEventListener("keydown", handleKeyDown);
    return () => {
      window.cancelAnimationFrame(frame);
      document.removeEventListener("pointerdown", handlePointerDown);
      document.removeEventListener("keydown", handleKeyDown);
    };
  }, [menuOpen]);

  return (
    <ClipSummaryRow
      clip={clip}
      isPlaying={isPlaying}
      onTogglePlayback={onTogglePlayback}
      isPlaybackAvailable={Boolean(clip.normalized_url)}
      titleExtras={isHidden ? <span className="pill">Hidden</span> : null}
      actions={
        <>
          <div className="btn-group btn-group-sm clip-summary-order-controls" role="group" aria-label="Clip order controls">
            <button
              type="button"
              className="btn btn-outline-secondary"
              onClick={onMoveUp}
              disabled={!canMoveUp}
              aria-label="Move clip up"
              title="Move clip up"
            >
              <BootstrapIcon name="chevron-up" />
            </button>
            <button
              type="button"
              className="btn btn-outline-secondary"
              onClick={onMoveDown}
              disabled={!canMoveDown}
              aria-label="Move clip down"
              title="Move clip down"
            >
              <BootstrapIcon name="chevron-down" />
            </button>
          </div>
          <div className="clip-summary-menu-wrap" ref={menuRef}>
            <button
              type="button"
              className="btn btn-sm btn-outline-secondary d-inline-flex align-items-center justify-content-center px-2"
              ref={menuButtonRef}
              onClick={() => setMenuOpen((current) => !current)}
              aria-label="Clip menu"
              aria-expanded={menuOpen}
              aria-haspopup="menu"
              title="Clip menu"
            >
              <BootstrapIcon name="three-dots" />
            </button>
            {menuOpen ? (
              <div ref={menuPanelRef} className={`clip-summary-menu${menuDirection === "up" ? " is-up" : ""}`} role="menu">
                <div className="clip-summary-menu-label">
                  Pinned to {pinCount} game{pinCount === 1 ? "" : "s"}
                </div>
                <button
                  type="button"
                  className="btn btn-sm btn-outline-secondary clip-summary-menu-action"
                  role="menuitem"
                  onClick={() => {
                    setMenuOpen(false);
                    onOpenPinModal();
                  }}
                >
                  <span className="clip-summary-menu-icon">
                    <BootstrapIcon name="pin-fill" />
                  </span>
                  <span>Pin to game</span>
                </button>
                <button
                  type="button"
                  className="btn btn-sm btn-outline-secondary clip-summary-menu-action"
                  role="menuitem"
                  onClick={() => {
                    setMenuOpen(false);
                    onToggleHidden();
                  }}
                >
                  <span className="clip-summary-menu-icon">
                    <BootstrapIcon name={isHidden ? "eye" : "eye-slash"} />
                  </span>
                  <span>{isHidden ? "Show in gameday" : "Hide from gameday"}</span>
                </button>
                <button
                  type="button"
                  className="btn btn-sm btn-outline-secondary clip-summary-menu-action"
                  role="menuitem"
                  onClick={() => {
                    setMenuOpen(false);
                    onEdit();
                  }}
                >
                  <span className="clip-summary-menu-icon">
                    <BootstrapIcon name="pencil-square" />
                  </span>
                  <span>Edit clip</span>
                </button>
                <div className="clip-summary-menu-label">Source: {clip.asset_title}</div>
              </div>
            ) : null}
          </div>
        </>
      }
    />
  );
}

function PinToGameModal({
  clip,
  games,
  pinnedAssignmentsByClipAndGame,
  onClose,
  onTogglePin,
  isTogglingPin,
}: {
  clip: AudioClip | null;
  games: TeamSnapEvent[];
  pinnedAssignmentsByClipAndGame: Map<string, { id: number }>;
  onClose: () => void;
  onTogglePin: (gameId: string, clipId: number) => void;
  isTogglingPin: boolean;
}) {
  useEffect(() => {
    const previousOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.body.style.overflow = previousOverflow;
    };
  }, []);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") {
        onClose();
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, [onClose]);

  if (!clip) {
    return null;
  }

  return (
    <div className="walkup-modal-backdrop" role="presentation" onClick={onClose}>
      <section
        className="walkup-modal card shadow-lg border-0 w-100"
        role="dialog"
        aria-modal="true"
        aria-labelledby="pin-to-game-modal-title"
        onClick={(event) => event.stopPropagation()}
      >
        <div className="walkup-modal-header">
          <div>
            <p className="eyebrow mb-1">Pin to game</p>
            <h2 id="pin-to-game-modal-title" className="h3 mb-0">
              {clip.label}
            </h2>
          </div>
          <button
            type="button"
            className="btn btn-outline-secondary btn-sm icon-button"
            onClick={onClose}
            aria-label="Close modal"
            title="Close modal"
          >
            <BootstrapIcon name="x-lg" />
          </button>
        </div>
        <div className="walkup-modal-body stack">
          <div className="panel-note">Choose which games should receive this clip. The clip can be pinned to multiple games or none at all.</div>
          <div className="clip-pin-game-list">
            {games.map((game) => {
              const gameId = String(game.id);
              const isPinned = pinnedAssignmentsByClipAndGame.has(`${gameId}:${clip.id}`);
              return (
                <button
                  type="button"
                  key={gameId}
                  className={`clip-pin-game-row btn btn-outline-secondary${isPinned ? " is-active" : ""}`}
                  onClick={() => onTogglePin(gameId, clip.id)}
                  disabled={isTogglingPin}
                >
                  <div className="clip-pin-game-copy">
                    <strong>{formatGameTitle(game)}</strong>
                    <span className="muted">{isPinned ? "Pinned to this game" : "Not pinned"}</span>
                  </div>
                  <span className={`pill${isPinned ? " is-active" : ""}`}>{isPinned ? "Pinned" : "Pin"}</span>
                </button>
              );
            })}
            {!games.length ? <div className="muted">No games are available for this team yet.</div> : null}
          </div>
          <div className="row walkup-modal-actions">
            <button type="button" className="btn btn-outline-secondary" onClick={onClose}>
              Close
            </button>
          </div>
        </div>
      </section>
    </div>
  );
}

function SourceProgressPanel({ progress }: { progress: SourceCreationProgress }) {
  const progressValue = progress.percent ?? 100;
  return (
    <div className="source-progress-panel" role="status" aria-live="polite">
      <div className="source-progress-header">
        <strong>{progress.label}</strong>
        {progress.percent !== null ? <span>{progress.percent}%</span> : <span>Working...</span>}
      </div>
      <div className={`source-progress-bar${progress.percent === null ? " is-indeterminate" : ""}`}>
        <div className="source-progress-bar-fill" style={{ width: `${progressValue}%` }} />
      </div>
      <div className="muted">{progress.detail}</div>
    </div>
  );
}

function WalkupClipEditorPanel({
  clip,
  label,
  setLabel,
  startMs,
  setStartMs,
  endMs,
  setEndMs,
  previewTimeMs,
  onPreview,
  onStopPreview,
  onSaveComplete,
  saveButtonLabel,
  introText,
  onChangeSource,
  closeLabel,
  onClose,
  onDelete,
  isDeleting,
  playerId,
}: {
  clip: AudioClip;
  label: string;
  setLabel: (value: string) => void;
  startMs: number;
  setStartMs: (value: number) => void;
  endMs: number;
  setEndMs: (value: number) => void;
  previewTimeMs: number | null;
  onPreview: (startMs: number, endMs: number) => void;
  onStopPreview: () => void;
  onSaveComplete: () => Promise<void>;
  saveButtonLabel: string;
  introText: string;
  onChangeSource?: () => void;
  closeLabel: string;
  onClose: () => void;
  onDelete: () => Promise<void>;
  isDeleting: boolean;
  playerId: string;
}) {
  useEffect(() => {
    setLabel(clip.label ?? "");
    setStartMs(clip.start_ms);
    setEndMs(clip.end_ms);
  }, [clip.end_ms, clip.id, clip.label, clip.start_ms, setEndMs, setLabel, setStartMs]);

  const previewClip = {
    ...clip,
    start_ms: startMs,
    end_ms: endMs,
    label,
  };

  const saveMutation = useMutation({
    mutationFn: async () => {
      const trimmedLabel = label.trim();
      if (!trimmedLabel) {
        throw new Error("WalkUp clip name cannot be blank");
      }
      if (endMs <= startMs) {
        throw new Error("Clip end must be greater than start");
      }
      return api.updateClip(clip.id, {
        label: trimmedLabel !== clip.label ? trimmedLabel : undefined,
        start_ms: startMs,
        end_ms: endMs,
      }, playerId);
    },
  });

  async function handleSave() {
    try {
      await saveMutation.mutateAsync();
      await onSaveComplete();
    } catch {
      // Mutation state already captures the error for display in the modal.
    }
  }

  async function handleSubmit(event: FormEvent) {
    event.preventDefault();
    await handleSave();
  }

  function handleStartChange(nextStart: number) {
    setStartMs(nextStart);
    if (nextStart >= endMs) {
      setEndMs(nextStart + DEFAULT_CLIP_LENGTH_MS);
    }
  }

  function handleEndChange(nextEnd: number) {
    setEndMs(nextEnd);
    if (nextEnd <= startMs) {
      setStartMs(Math.max(0, nextEnd - DEFAULT_CLIP_LENGTH_MS));
    }
  }

  const trimmedLabel = label.trim();
  const canSave = trimmedLabel.length > 0 && endMs > startMs && !saveMutation.isPending;

  return (
    <form className="stack" onSubmit={handleSubmit}>
      <div className="panel-note">{introText}</div>
      <label className="field">
        WalkUp clip name
        <input
          value={label}
          onChange={(event) => setLabel(event.target.value)}
          placeholder="WalkUp clip name"
          autoComplete="off"
        />
      </label>
      <ClipTrimScrubber
        clip={clip}
        durationMs={clip.waveform_duration_ms ?? Math.max(endMs, startMs + DEFAULT_CLIP_LENGTH_MS, DEFAULT_CLIP_LENGTH_MS)}
        waveformPeaks={clip.waveform_peaks ?? []}
        startMs={startMs}
        endMs={endMs}
        previewTimeMs={previewTimeMs}
        onStartChange={handleStartChange}
        onEndChange={handleEndChange}
        onPreview={() => void onPreview(startMs, endMs)}
        onStopPreview={onStopPreview}
      />
      {previewTimeMs !== null ? (
        <div className="panel-note">
          Preview range: {formatClipRange(previewClip.start_ms, previewClip.end_ms)}
          {" "}
          Current time: {formatPlaybackPosition(previewTimeMs)} / {formatPlaybackPosition(previewClip.end_ms)}
        </div>
      ) : null}
      <div className="row walkup-modal-actions">
        <button type="button" className="btn btn-primary" onClick={() => void handleSave()} disabled={!canSave}>
          {saveMutation.isPending ? "Saving..." : saveButtonLabel}
        </button>
        <button
          type="button"
          className="btn btn-outline-danger"
          onClick={() => void onDelete()}
          disabled={isDeleting}
        >
          {isDeleting ? "Deleting..." : "Delete"}
        </button>
        {onChangeSource ? (
          <button type="button" className="btn btn-outline-secondary" onClick={onChangeSource}>
            Change source
          </button>
        ) : null}
        <button type="button" className="btn btn-outline-secondary" onClick={onClose}>
          {closeLabel}
        </button>
      </div>
      {saveMutation.error instanceof Error ? <div className="muted">{saveMutation.error.message}</div> : null}
    </form>
  );
}

function snapTrimMs(value: number, stepMs: number): number {
  return Math.round(value / stepMs) * stepMs;
}

function peaksForWaveSurfer(peaks: number[]): number[][] | undefined {
  if (!peaks.length) {
    return undefined;
  }

  return [peaks.map((peak) => Math.max(0, Math.min(1, peak / 100)))];
}

function ClipTrimScrubber({
  clip,
  durationMs,
  waveformPeaks,
  startMs,
  endMs,
  previewTimeMs,
  onStartChange,
  onEndChange,
  onPreview,
  onStopPreview,
}: {
  clip: AudioClip;
  durationMs: number;
  waveformPeaks: number[];
  startMs: number;
  endMs: number;
  previewTimeMs: number | null;
  onStartChange: (value: number) => void;
  onEndChange: (value: number) => void;
  onPreview: () => void;
  onStopPreview: () => void;
}) {
  const waveformContainerRef = useRef<HTMLDivElement | null>(null);
  const waveSurferRef = useRef<WaveSurfer | null>(null);
  const regionsPluginRef = useRef<RegionsPlugin | null>(null);
  const trimRegionRef = useRef<Region | null>(null);
  const suppressRegionUpdateRef = useRef(false);
  const latestTrimRef = useRef({
    onStartChange,
    onEndChange,
    stepMs: TRIM_STEP_MS,
    scrubMaxMs: durationMs,
  });
  const scrubMaxMs = Math.max(durationMs, endMs, startMs + 1);
  const safeStartMs = Math.min(startMs, scrubMaxMs - 1);
  const safeEndMs = Math.max(endMs, safeStartMs + 1);
  const [zoomAmount, setZoomAmount] = useState(55);
  const [focusEdge, setFocusEdge] = useState<TrimFocusEdge>("start");
  const [waveformReady, setWaveformReady] = useState(false);
  const zoomRatio = Math.pow(Math.max(0, Math.min(1, zoomAmount / TRIM_ZOOM_SLIDER_MAX)), 1.8);
  const closeWindowMs = Math.min(scrubMaxMs, TRIM_ZOOM_WINDOW_MS);
  const targetViewDurationMs = zoomAmount <= 0 ? scrubMaxMs : scrubMaxMs - (scrubMaxMs - closeWindowMs) * zoomRatio;
  const viewCenterMs = focusEdge === "end" ? safeEndMs : safeStartMs;
  const halfWindowMs = targetViewDurationMs / 2;
  let viewStartMs = zoomAmount <= 0 ? 0 : Math.max(0, viewCenterMs - halfWindowMs);
  let viewEndMs = zoomAmount <= 0 ? scrubMaxMs : Math.min(scrubMaxMs, viewCenterMs + halfWindowMs);
  if (zoomAmount > 0 && viewEndMs - viewStartMs < targetViewDurationMs) {
    if (viewStartMs === 0) {
      viewEndMs = Math.min(scrubMaxMs, targetViewDurationMs);
    } else {
      viewStartMs = Math.max(0, scrubMaxMs - targetViewDurationMs);
    }
  }
  const viewDurationMs = Math.max(1, viewEndMs - viewStartMs);
  const sliderStepMs = TRIM_STEP_MS;

  useEffect(() => {
    latestTrimRef.current = {
      onStartChange,
      onEndChange,
      stepMs: sliderStepMs,
      scrubMaxMs,
    };
  }, [onEndChange, onStartChange, scrubMaxMs, sliderStepMs]);

  useEffect(() => {
    const container = waveformContainerRef.current;
    if (!container || !clip.normalized_url) {
      return;
    }

    setWaveformReady(false);
    trimRegionRef.current = null;
    const regions = RegionsPlugin.create();
    const wavesurfer = WaveSurfer.create({
      container,
      url: `${API_BASE}${clip.normalized_url}`,
      peaks: peaksForWaveSurfer(waveformPeaks),
      duration: scrubMaxMs / 1000,
      height: 110,
      waveColor: "rgba(19, 34, 56, 0.28)",
      progressColor: "rgba(217, 79, 4, 0.8)",
      cursorColor: "rgba(19, 34, 56, 0.9)",
      cursorWidth: 2,
      minPxPerSec: 1,
      fillParent: true,
      normalize: true,
      dragToSeek: false,
      interact: false,
      hideScrollbar: false,
      plugins: [regions],
    });

    waveSurferRef.current = wavesurfer;
    regionsPluginRef.current = regions;

    const unsubscribeReady = wavesurfer.on("ready", () => {
      const region = regions.addRegion({
        id: "walkup-trim-region",
        start: safeStartMs / 1000,
        end: safeEndMs / 1000,
        color: "rgba(217, 79, 4, 0.18)",
        drag: false,
        resize: true,
        resizeStart: true,
        resizeEnd: true,
        minLength: 0.05,
      });
      trimRegionRef.current = region;
      setWaveformReady(true);
    });

    const unsubscribeRegionUpdate = regions.on("region-update", (region, side) => {
      if (region.id !== "walkup-trim-region" || suppressRegionUpdateRef.current) {
        return;
      }

      const { onStartChange: updateStart, onEndChange: updateEnd, stepMs, scrubMaxMs: maxMs } = latestTrimRef.current;
      const nextStartMs = Math.max(0, Math.min(maxMs - 1, snapTrimMs(region.start * 1000, stepMs)));
      const nextEndMs = Math.max(nextStartMs + 1, Math.min(maxMs, snapTrimMs(region.end * 1000, stepMs)));
      if (side !== "end") {
        setFocusEdge("start");
        updateStart(nextStartMs);
      }
      if (side !== "start") {
        setFocusEdge("end");
        updateEnd(nextEndMs);
      }
    });

    return () => {
      unsubscribeRegionUpdate();
      unsubscribeReady();
      trimRegionRef.current = null;
      regionsPluginRef.current = null;
      waveSurferRef.current = null;
      setWaveformReady(false);
      wavesurfer.destroy();
    };
  }, [clip.id, clip.normalized_url, durationMs, waveformPeaks]);

  useEffect(() => {
    const region = trimRegionRef.current;
    if (!region) {
      return;
    }

    suppressRegionUpdateRef.current = true;
    region.setOptions({
      start: safeStartMs / 1000,
      end: safeEndMs / 1000,
    });
    window.setTimeout(() => {
      suppressRegionUpdateRef.current = false;
    }, 0);
  }, [safeEndMs, safeStartMs]);

  useEffect(() => {
    const wavesurfer = waveSurferRef.current;
    const container = waveformContainerRef.current;
    if (!wavesurfer || !container || !waveformReady) {
      return;
    }

    const viewDurationSeconds = Math.max(0.1, viewDurationMs / 1000);
    const minPxPerSec = zoomAmount <= 0 ? 1 : Math.max(1, container.clientWidth / viewDurationSeconds);
    wavesurfer.zoom(minPxPerSec);
    wavesurfer.setScrollTime(zoomAmount <= 0 ? 0 : viewStartMs / 1000);
  }, [viewDurationMs, viewStartMs, waveformReady, zoomAmount]);

  useEffect(() => {
    const wavesurfer = waveSurferRef.current;
    if (!wavesurfer || previewTimeMs === null) {
      return;
    }

    wavesurfer.setTime(previewTimeMs / 1000);
  }, [previewTimeMs]);

  function handleZoomAmountChange(value: number) {
    setZoomAmount(Math.max(0, Math.min(TRIM_ZOOM_SLIDER_MAX, value)));
  }

  function nudgeStart(deltaMs: number) {
    const nextStartMs = Math.max(0, safeStartMs + deltaMs);
    setFocusEdge("start");
    onStartChange(nextStartMs);
  }

  function setEndToDefaultLength() {
    onEndChange(Math.min(Math.max(1, durationMs), safeStartMs + END_SHORTCUT_LENGTH_MS));
    setFocusEdge("end");
  }

  function setEndToSourceEnd() {
    onEndChange(Math.max(1, durationMs));
    setFocusEdge("end");
  }

  return (
    <div className="clip-waveform-shell">
      <div className="clip-waveform-header">
        <div>
          <strong>Trim waveform</strong>
          <div className="muted">
            {formatClipRange(safeStartMs, safeEndMs)} | Step {sliderStepMs}ms
          </div>
        </div>
        <div className="clip-waveform-meta">
          <span className="pill">Source: {formatPlaybackPosition(durationMs)}</span>
        </div>
      </div>
      <div className="clip-wavesurfer" ref={waveformContainerRef} aria-label="Trim waveform region" />
      <label className="clip-zoom-scrubber">
        <span className="clip-zoom-scrubber-header">
          <span>Zoom scrubber</span>
          <span className="muted">{zoomAmount === 0 ? "Full file" : `${zoomAmount}%`}</span>
        </span>
        <input
          type="range"
          min={0}
          max={TRIM_ZOOM_SLIDER_MAX}
          step={1}
          value={zoomAmount}
          onChange={(event) => handleZoomAmountChange(Number(event.target.value))}
          disabled={!clip.normalized_url || !waveformReady}
        />
      </label>
      <div className="clip-waveform-controls">
        <div className="clip-waveform-control">
          <div className="clip-waveform-control-header">
            <span>Start</span>
            <span className="muted">{formatPlaybackPosition(safeStartMs)}</span>
            <div className="clip-waveform-nudges">
              <button type="button" className="btn btn-outline-secondary btn-sm" onClick={() => nudgeStart(-TRIM_NUDGE_MS)}>
                -{TRIM_NUDGE_MS}ms
              </button>
              <button type="button" className="btn btn-outline-secondary btn-sm" onClick={() => nudgeStart(TRIM_NUDGE_MS)}>
                +{TRIM_NUDGE_MS}ms
              </button>
            </div>
          </div>
        </div>
        <div className="clip-waveform-control">
          <div className="clip-waveform-control-header">
            <span>End</span>
            <span className="muted">{formatPlaybackPosition(safeEndMs)}</span>
            <div className="btn-group btn-group-sm" role="group" aria-label="End clip shortcuts">
              <button type="button" className="btn btn-outline-secondary" onClick={setEndToDefaultLength}>
                1:30
              </button>
              <button type="button" className="btn btn-outline-secondary" onClick={setEndToSourceEnd}>
                EOF
              </button>
            </div>
          </div>
        </div>
      </div>
      <div className="clip-waveform-preview-action">
        <button
          type="button"
          className={`btn clip-waveform-preview-button${previewTimeMs !== null ? " btn-danger" : " btn-outline-secondary"}`}
          onClick={previewTimeMs !== null ? onStopPreview : onPreview}
          disabled={!clip.normalized_url}
          aria-label={previewTimeMs !== null ? "Stop preview" : "Preview clip"}
          title={previewTimeMs !== null ? "Stop preview" : "Preview clip"}
        >
          <BootstrapIcon name={previewTimeMs !== null ? "stop" : "play"} />
          <span>{previewTimeMs !== null ? "Stop preview" : "Preview clip"}</span>
        </button>
      </div>
    </div>
  );
}

function UploadedMediaCard({
  asset,
  onDelete,
  isDeleting,
  teamId,
  playerId,
}: {
  asset: AudioAsset;
  onDelete: () => void;
  isDeleting: boolean;
  teamId: string;
  playerId: string;
}) {
  const [isEditing, setIsEditing] = useState(false);
  const [title, setTitle] = useState(asset.title ?? "");

  useEffect(() => {
    setIsEditing(false);
    setTitle(asset.title ?? "");
  }, [asset.id, asset.title]);

  const updateMutation = useMutation({
    mutationFn: () => api.updateAsset(asset.id, { title: title.trim() }, playerId),
    onSuccess: async () => {
      setIsEditing(false);
      await Promise.all([
        queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }),
        queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
      ]);
    },
  });

  function handleSubmit(event: FormEvent) {
    event.preventDefault();
    void updateMutation.mutateAsync();
  }

  const trimmedTitle = title.trim();
  const titleChanged = trimmedTitle !== asset.title;
  const canSave = isEditing && trimmedTitle.length > 0 && titleChanged && !updateMutation.isPending;

  return (
    <form className="asset-card" onSubmit={handleSubmit}>
      <div className="asset-card-header">
        <div className="asset-card-copy">
          <strong>{isEditing ? "Editing media title" : asset.title || "Untitled media"}</strong>
          <div className="muted asset-card-filename">Uploaded as: {asset.original_filename || "Unknown file"}</div>
        </div>
        <div className="row asset-card-actions">
          <span className="pill">{Math.round(asset.size_bytes / 1024)} KB</span>
          <button
            type="button"
            className="btn btn-outline-secondary btn-sm"
            onClick={() => setIsEditing((current) => !current)}
            aria-pressed={isEditing}
          >
            {isEditing ? "Done" : "Edit title"}
          </button>
          <button type="button" className="btn btn-outline-danger btn-sm" onClick={onDelete} disabled={isDeleting}>
            Delete
          </button>
        </div>
      </div>
      {isEditing ? (
        <label className="field">
          Media title
          <input
            value={title}
            onChange={(event) => setTitle(event.target.value)}
            placeholder="Media title"
            autoComplete="off"
          />
        </label>
      ) : null}
      <div className="asset-card-meta">
        {isEditing && trimmedTitle.length === 0 ? <div className="muted">Media title cannot be blank.</div> : null}
        {updateMutation.error instanceof Error ? <div className="muted">{updateMutation.error.message}</div> : null}
        {!isEditing ? <div className="muted">Open Edit title to rename this uploaded media item.</div> : null}
      </div>
      {isEditing ? (
        <div className="row asset-card-footer">
          <button type="submit" className="btn btn-primary btn-sm" disabled={!canSave}>
            {updateMutation.isPending ? "Saving..." : "Save title"}
          </button>
        </div>
      ) : null}
    </form>
  );
}
