import { FormEvent, useEffect, 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 } from "../api/client";
import type { AudioAsset, AudioClip } from "../api/types";
import { useWalkupContext } from "../hooks/useWalkupContext";
import { queryClient } from "../lib/queryClient";
import { formatClipRange, formatPlaybackPosition } from "../lib/media";
import { formatMemberName } from "../lib/teamsnapHelpers";

const MEDIA_ACCEPT =
  ".mp3,.m4a,.aac,.wav,.ogg,.oga,.flac,.mp4,.m4v,.mov,audio/*,video/*,application/octet-stream";
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
const DEFAULT_CLIP_LENGTH_MS = 30_000;
const TRIM_NUDGE_MS = 100;
const TRIM_STEP_MS = 100;
const TRIM_ZOOM_WINDOW_MS = 3_000;
const TRIM_ZOOM_SLIDER_MAX = 100;

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

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

type BootstrapIconName = "play" | "stop" | "three-dots-vertical" | "pencil-square" | "plus-lg" | "x-lg";
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 [manageMediaOpen, setManageMediaOpen] = useState(false);
  const audioRef = useRef<HTMLAudioElement | null>(null);
  const previewClipIdRef = useRef<number | null>(null);
  const previewRangeRef = useRef<{ startMs: number; endMs: number } | null>(null);
  const [previewClipId, setPreviewClipId] = useState<number | null>(null);
  const [previewTimeMs, setPreviewTimeMs] = useState<number | null>(null);

  const assetsQuery = useQuery({
    queryKey: ["assets", teamId, playerId],
    queryFn: () => api.listAssets(teamId, playerId),
    enabled: Boolean(teamId && playerId),
  });
  const clipsQuery = useQuery({
    queryKey: ["clips", teamId, playerId],
    queryFn: () => api.listClips(teamId, playerId),
    enabled: Boolean(teamId && playerId),
  });

  useEffect(() => {
    return () => {
      stopPreview();
    };
  }, []);

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

  function getAudio() {
    const audio = audioRef.current ?? new Audio();
    if (!audioRef.current) {
      audio.preload = "auto";
      audio.onended = () => {
        stopPreview();
      };
      audio.ontimeupdate = () => {
        const range = previewRangeRef.current;
        if (!range) {
          return;
        }

        setPreviewTimeMs(Math.round(audio.currentTime * 1000));

        if (audio.currentTime >= range.endMs / 1000) {
          stopPreview();
        }
      };
    }
    audioRef.current = audio;
    return audio;
  }

  function stopPreview() {
    previewClipIdRef.current = null;
    previewRangeRef.current = null;
    setPreviewClipId(null);
    setPreviewTimeMs(null);

    const audio = audioRef.current;
    if (!audio) {
      return;
    }

    audio.pause();
    audio.currentTime = 0;
    audio.removeAttribute("src");
    audio.load();
  }

  async function playPreview(
    clip: AudioClip,
    startMsOverride?: number,
    endMsOverride?: number,
  ) {
    if (!clip.normalized_url) {
      return;
    }

    const audio = getAudio();
    const startMs = startMsOverride ?? clip.start_ms;
    const endMs = endMsOverride ?? clip.end_ms;
    const startSeconds = startMs / 1000;

    if (previewClipIdRef.current === clip.id && !audio.paused) {
      stopPreview();
      return;
    }

    stopPreview();
    const nextAudio = getAudio();
    previewClipIdRef.current = clip.id;
    previewRangeRef.current = { startMs, endMs };
    setPreviewClipId(clip.id);
    setPreviewTimeMs(startMs);
    nextAudio.pause();
    const metadataReady = new Promise<void>((resolve) => {
      nextAudio.onloadedmetadata = () => {
        if (previewClipIdRef.current === clip.id) {
          nextAudio.currentTime = startSeconds;
          setPreviewTimeMs(startMs);
        }
        resolve();
      };
    });
    nextAudio.src = `${API_BASE}${clip.normalized_url}`;
    await metadataReady;

    try {
      await nextAudio.play();
    } catch (error) {
      stopPreview();
      throw error;
    }
  }

  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 (!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">
        <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">
          {clipsQuery.data?.map((clip) => (
            <WalkupClipCard
              key={clip.id}
              clip={clip}
              isPreviewing={previewClipId === clip.id}
              onPreview={() => void playPreview(clip)}
              onEdit={() => openEditWalkupClip(clip)}
              onStopPreview={stopPreview}
            />
          ))}
          {!clipsQuery.isLoading && !clipsQuery.data?.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}
          playPreview={playPreview}
          onClose={closeCreateWalkupClip}
          stopPreview={stopPreview}
          onDeleteClip={async (clipId: number) => {
            stopPreview();
            await deleteClipMutation.mutateAsync(clipId);
          }}
          isDeletingClip={deleteClipMutation.isPending}
        />
      ) : null}
      {manageMediaOpen ? (
        <ManageUploadedMediaModal
          assets={assetsQuery.data ?? []}
          teamId={teamId}
          playerId={playerId}
          onClose={closeManageMedia}
          stopPreview={stopPreview}
        />
      ) : null}
    </section>
  );
}

function BootstrapIcon({ name }: { name: BootstrapIconName }) {
  if (name === "play") {
    return (
      <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
        <path d="M10.804 8 5 4.633v6.734zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696z" />
      </svg>
    );
  }

  if (name === "stop") {
    return (
      <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
        <path d="M3.5 5A1.5 1.5 0 0 1 5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11zM5 4.5a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5V5a.5.5 0 0 0-.5-.5z" />
      </svg>
    );
  }

  if (name === "three-dots-vertical") {
    return (
      <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
        <path d="M9.5 1a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0m0 7a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0m0 7a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0" />
      </svg>
    );
  }

  if (name === "plus-lg") {
    return (
      <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
        <path d="M8 2a.75.75 0 0 1 .75.75v4.5h4.5a.75.75 0 0 1 0 1.5h-4.5v4.5a.75.75 0 0 1-1.5 0v-4.5h-4.5a.75.75 0 0 1 0-1.5h4.5v-4.5A.75.75 0 0 1 8 2" />
      </svg>
    );
  }

  if (name === "x-lg") {
    return (
      <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
        <path d="M2.146 2.146a.5.5 0 0 1 .708 0L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854a.5.5 0 0 1 0-.708" />
      </svg>
    );
  }

  return (
    <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
      <path d="M15.502 1.94a.5.5 0 0 1 0 .706l-.853.853-2-2 .853-.853a.5.5 0 0 1 .707 0l1.293 1.294zm-1.75 1.457-2-2L4.939 8.21a.5.5 0 0 0-.11.168l-1 3a.5.5 0 0 0 .64.64l3-1a.5.5 0 0 0 .168-.11l6.115-6.51zM4.257 11.518l.924-.308 6.563-6.992-1.5-1.5L3.25 9.7l-.308.924 1.315.894z" />
    </svg>
  );
}

function WalkupClipModal({
  state,
  assets,
  teamId,
  playerId,
  previewTimeMs,
  playPreview,
  onClose,
  stopPreview,
  onDeleteClip,
  isDeletingClip,
}: {
  state: WalkupClipModalState;
  assets: AudioAsset[];
  teamId: string;
  playerId: string;
  previewTimeMs: number | null;
  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: ["clips", teamId, playerId] }),
    ]);
    const refreshedClips = await queryClient.fetchQuery({
      queryKey: ["clips", teamId, playerId],
      queryFn: () => api.listClips(teamId, playerId),
    });
    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: ["clips", 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">
          <div className="walkup-stepper">
            <div className={`walkup-step${step === "source" ? " is-active" : " is-complete"}`}>1. Source</div>
            <div className={`walkup-step${step === "editor" ? " is-active" : ""}`}>2. Trim and metadata</div>
          </div>

          {step === "source" ? (
            <form className="stack" onSubmit={handleSourceSubmit} aria-busy={createSourceMutation.isPending}>
              <label className="field">
                Walkup clip name
                <input
                  value={draftLabel}
                  onChange={(event) => setDraftLabel(event.target.value)}
                  placeholder="Optional clip name"
                  autoComplete="off"
                  disabled={createSourceMutation.isPending}
                />
              </label>
              <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")}
                  >
                    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")}
                  >
                    Import 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 media
                  </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">
                    <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">
                    <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">
                    <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 () => {
                await Promise.all([
                  queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }),
                  queryClient.invalidateQueries({ queryKey: ["clips", teamId, playerId] }),
                ]);
                handleClose();
              }}
              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: ["clips", 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 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">
          <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,
  isPreviewing,
  onPreview,
  onEdit,
  onStopPreview,
}: {
  clip: AudioClip;
  isPreviewing: boolean;
  onPreview: () => void;
  onEdit: () => void;
  onStopPreview: () => void;
}) {
  const [menuOpen, setMenuOpen] = useState(false);

  return (
    <div className="clip-summary">
      <div className="clip-summary-header">
        <button
          type="button"
          className={`btn btn-sm icon-button icon-button-circle${isPreviewing ? " btn-success" : " btn-outline-secondary"}`}
          onClick={isPreviewing ? onStopPreview : onPreview}
          disabled={!clip.normalized_url}
          aria-label={isPreviewing ? "Stop preview" : "Preview clip"}
          title={isPreviewing ? "Stop preview" : "Preview clip"}
        >
          <BootstrapIcon name={isPreviewing ? "stop" : "play"} />
        </button>
        <div className="clip-summary-title-row">
          <strong>{clip.label}</strong>
        </div>
        <div className="clip-summary-menu-wrap">
          <button
            type="button"
            className="icon-button-bare icon-button-menu"
            onClick={() => setMenuOpen((current) => !current)}
            aria-label="Clip menu"
            aria-expanded={menuOpen}
            aria-haspopup="menu"
            title="Clip menu"
          >
            <BootstrapIcon name="three-dots-vertical" />
          </button>
          {menuOpen ? (
            <div className="clip-summary-menu" role="menu">
              <button type="button" className="clip-summary-menu-item" role="menuitem" onClick={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>
      </div>
    </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 useThirtySecondLength() {
    setEndMs(startMs + DEFAULT_CLIP_LENGTH_MS);
  }

  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 nudgeEnd(deltaMs: number) {
    const nextEndMs = Math.max(safeStartMs + 1, safeEndMs + deltaMs);
    setFocusEdge("end");
    onEndChange(nextEndMs);
  }

  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="clip-waveform-nudges">
              <button type="button" className="btn btn-outline-secondary btn-sm" onClick={() => nudgeEnd(-TRIM_NUDGE_MS)}>
                -{TRIM_NUDGE_MS}ms
              </button>
              <button type="button" className="btn btn-outline-secondary btn-sm" onClick={() => nudgeEnd(TRIM_NUDGE_MS)}>
                +{TRIM_NUDGE_MS}ms
              </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: ["clips", 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>
  );
}
