import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { getPttSettings } from '../lib/pttSettings';
import { codeToShortcut } from '../lib/globalShortcut';
import { invalidate as invalidateSoundCache, preload } from '../lib/soundboardPlayback';
import {
  addSound,
  deleteSound,
  getSoundBlob,
  listSounds,
  MAX_SOUND_BYTES,
  reorderCategory,
  type SoundboardEntry,
  subscribeSoundboardChanges,
  updateSound,
} from '../lib/soundboardStorage';
import { Modal } from './Modal';
import {
  AlertIcon,
  ChevronDownIcon,
  PencilIcon,
  PlusIcon,
  SpinnerIcon,
  TrashIcon,
} from './icons';

interface Props {
  open: boolean;
  onClose: () => void;
}

const BYTES_PER_MB = 1024 * 1024;
const CATEGORY_LIST_ID = 'sb-category-list';

// Admin UI for the soundboard. Users add, rename, categorise, reorder,
// assign hotkeys, adjust per-sound volume, preview and delete clips here.
// In-call panel only reads the resulting manifest.
export function SoundboardManagerDialog({ open, onClose }: Props) {
  const { t } = useTranslation(['app']);
  const fileRef = useRef<HTMLInputElement | null>(null);
  const previewRef = useRef<HTMLAudioElement | null>(null);
  const previewUrlRef = useRef<string | null>(null);
  const [entries, setEntries] = useState<SoundboardEntry[]>([]);
  const [loading, setLoading] = useState(false);
  const [busyId, setBusyId] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [previewingId, setPreviewingId] = useState<string | null>(null);

  const refresh = useCallback(async () => {
    setLoading(true);
    try {
      setEntries(await listSounds());
    } catch (err: unknown) {
      console.error('listSounds failed', err);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    if (!open) return;
    void refresh();
    // External mutations (hotkey fires, multi-tab edits) should reflect
    // immediately while the dialog is open.
    const unsub = subscribeSoundboardChanges(() => {
      void refresh();
    });
    return () => {
      unsub();
      stopPreview();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [open, refresh]);

  useEffect(() => {
    return () => stopPreview();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const categories = useMemo(() => {
    const set = new Set<string>();
    for (const e of entries) if (e.category) set.add(e.category);
    return Array.from(set).sort((a, b) => a.localeCompare(b));
  }, [entries]);

  const grouped = useMemo(() => {
    const map = new Map<string, SoundboardEntry[]>();
    for (const e of entries) {
      const key = e.category ?? '';
      const arr = map.get(key);
      if (arr) arr.push(e);
      else map.set(key, [e]);
    }
    return map;
  }, [entries]);

  function stopPreview(): void {
    const el = previewRef.current;
    if (el) {
      try {
        el.pause();
      } catch {
        /* ignore */
      }
      el.src = '';
    }
    previewRef.current = null;
    if (previewUrlRef.current) {
      URL.revokeObjectURL(previewUrlRef.current);
      previewUrlRef.current = null;
    }
    setPreviewingId(null);
  }

  async function handleAdd(file: File): Promise<void> {
    setError(null);
    setBusyId('__add');
    try {
      await addSound({ file });
      await refresh();
    } catch (err: unknown) {
      const code = err instanceof Error ? err.message : 'add_failed';
      if (code === 'sound_too_large') {
        setError(
          t('app:soundboard.error_too_large', {
            defaultValue: 'Datei zu groß (max {{max}} MB).',
            max: MAX_SOUND_BYTES / BYTES_PER_MB,
          }),
        );
      } else if (code === 'sound_not_audio') {
        setError(
          t('app:soundboard.error_not_audio', {
            defaultValue: 'Nur Audio-Dateien werden unterstützt.',
          }),
        );
      } else {
        setError(
          t('app:soundboard.error_generic', {
            defaultValue: 'Sound konnte nicht gespeichert werden.',
          }),
        );
      }
    } finally {
      setBusyId(null);
      if (fileRef.current) fileRef.current.value = '';
    }
  }

  async function handlePatch(
    id: string,
    patch: Parameters<typeof updateSound>[1],
  ): Promise<void> {
    setBusyId(id);
    try {
      await updateSound(id, patch);
    } catch (err: unknown) {
      console.error('updateSound failed', err);
    } finally {
      setBusyId(null);
    }
  }

  async function handleDelete(id: string): Promise<void> {
    if (!window.confirm(t('app:soundboard.delete_confirm', { defaultValue: 'Sound löschen?' }))) {
      return;
    }
    setBusyId(id);
    try {
      await deleteSound(id);
      invalidateSoundCache(id);
      if (previewingId === id) stopPreview();
    } catch (err: unknown) {
      console.error('deleteSound failed', err);
    } finally {
      setBusyId(null);
    }
  }

  async function handlePreview(entry: SoundboardEntry): Promise<void> {
    if (previewingId === entry.id) {
      stopPreview();
      return;
    }
    stopPreview();
    const blob = await getSoundBlob(entry.id);
    if (!blob) return;
    const url = URL.createObjectURL(blob);
    const el = new Audio(url);
    el.volume = entry.gain;
    el.onended = () => stopPreview();
    el.onerror = () => stopPreview();
    el.play().catch(() => stopPreview());
    previewRef.current = el;
    previewUrlRef.current = url;
    setPreviewingId(entry.id);
    // Opportunistic warm-up of the AudioBuffer cache so the first in-call
    // playback doesn't pause on decode.
    void preload(entry.id);
  }

  async function handleReorder(
    category: string | null,
    idx: number,
    dir: -1 | 1,
  ): Promise<void> {
    const bucket = grouped.get(category ?? '') ?? [];
    const next = idx + dir;
    if (next < 0 || next >= bucket.length) return;
    const reordered = bucket.slice();
    const tmp = reordered[idx]!;
    reordered[idx] = reordered[next]!;
    reordered[next] = tmp;
    setBusyId('__reorder:' + (category ?? ''));
    try {
      await reorderCategory(
        category,
        reordered.map((e) => e.id),
      );
    } catch (err: unknown) {
      console.error('reorderCategory failed', err);
    } finally {
      setBusyId(null);
    }
  }

  if (!open) return null;

  return (
    <Modal
      open={open}
      onClose={onClose}
      title={t('app:soundboard.manager_title', { defaultValue: 'Soundboard verwalten' })}
      size="lg"
    >
      <div className="flex flex-col gap-4">
        <div className="flex items-center justify-between gap-3">
          <p className="text-xs text-fg-muted">
            {t('app:soundboard.manager_hint', {
              defaultValue:
                'Beliebig viele Sounds, kein Hotkey nötig. Hotkeys feuern nur während eines Anrufs.',
            })}
          </p>
          <input
            ref={fileRef}
            type="file"
            accept="audio/*"
            className="hidden"
            onChange={(e) => {
              const f = e.target.files?.[0];
              if (f) void handleAdd(f);
            }}
          />
          <button
            type="button"
            onClick={() => fileRef.current?.click()}
            disabled={busyId !== null}
            className="inline-flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md bg-accent px-3 py-1.5 text-xs font-semibold text-accent-fg transition hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
          >
            {busyId === '__add' ? (
              <SpinnerIcon className="h-3.5 w-3.5" />
            ) : (
              <PlusIcon className="h-3.5 w-3.5" />
            )}
            <span>{t('app:soundboard.add', { defaultValue: 'Sound hinzufügen' })}</span>
          </button>
        </div>

        {error && (
          <div className="flex items-start gap-2 rounded-md border border-rose-500/30 bg-rose-500/10 px-3 py-2 text-xs text-rose-600 dark:text-rose-300">
            <AlertIcon className="mt-0.5 h-4 w-4 shrink-0" />
            <p className="min-w-0 flex-1 break-words">{error}</p>
          </div>
        )}

        <datalist id={CATEGORY_LIST_ID}>
          {categories.map((c) => (
            <option key={c} value={c} />
          ))}
        </datalist>

        {loading ? (
          <div className="flex items-center gap-2 text-xs text-fg-muted">
            <SpinnerIcon className="h-3.5 w-3.5 text-accent" />
          </div>
        ) : entries.length === 0 ? (
          <p className="rounded-lg border border-line bg-surface-2 px-4 py-6 text-center text-sm text-fg-muted">
            {t('app:soundboard.empty', {
              defaultValue: 'Noch keine Sounds. Lade oben welche hoch.',
            })}
          </p>
        ) : (
          <div className="flex flex-col gap-4">
            {Array.from(grouped.entries()).map(([categoryKey, bucket]) => {
              const category = categoryKey === '' ? null : categoryKey;
              return (
                <SoundboardCategoryGroup
                  key={categoryKey || '__uncat'}
                  category={category}
                  entries={bucket}
                  entriesTotal={entries}
                  busyId={busyId}
                  previewingId={previewingId}
                  onPatch={handlePatch}
                  onDelete={handleDelete}
                  onPreview={handlePreview}
                  onReorder={handleReorder}
                />
              );
            })}
          </div>
        )}
      </div>
    </Modal>
  );
}

// ---------------------------------------------------------------------------

interface GroupProps {
  category: string | null;
  entries: SoundboardEntry[];
  entriesTotal: SoundboardEntry[];
  busyId: string | null;
  previewingId: string | null;
  onPatch: (id: string, patch: Parameters<typeof updateSound>[1]) => Promise<void>;
  onDelete: (id: string) => Promise<void>;
  onPreview: (entry: SoundboardEntry) => Promise<void>;
  onReorder: (category: string | null, idx: number, dir: -1 | 1) => Promise<void>;
}

function SoundboardCategoryGroup({
  category,
  entries,
  entriesTotal,
  busyId,
  previewingId,
  onPatch,
  onDelete,
  onPreview,
  onReorder,
}: GroupProps) {
  const { t } = useTranslation(['app']);
  const [open, setOpen] = useState(true);
  const label =
    category ??
    t('app:soundboard.category_uncategorized', { defaultValue: '(Ohne Kategorie)' });
  return (
    <section className="rounded-lg border border-line bg-surface-2">
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        className="flex w-full cursor-pointer items-center justify-between gap-3 px-4 py-2.5 text-left"
      >
        <span className="text-xs font-semibold uppercase tracking-[0.1em] text-fg-muted">
          {label} · {entries.length}
        </span>
        <ChevronDownIcon
          className={'h-4 w-4 text-fg-muted transition ' + (open ? '' : '-rotate-90')}
        />
      </button>
      {open && (
        <ul className="flex flex-col gap-2 border-t border-line p-3">
          {entries.map((entry, idx) => (
            <SoundboardRow
              key={entry.id}
              entry={entry}
              entriesTotal={entriesTotal}
              isFirst={idx === 0}
              isLast={idx === entries.length - 1}
              busy={busyId === entry.id}
              previewing={previewingId === entry.id}
              onPatch={onPatch}
              onDelete={onDelete}
              onPreview={onPreview}
              onReorderUp={() => onReorder(category, idx, -1)}
              onReorderDown={() => onReorder(category, idx, 1)}
            />
          ))}
        </ul>
      )}
    </section>
  );
}

// ---------------------------------------------------------------------------

interface RowProps {
  entry: SoundboardEntry;
  entriesTotal: SoundboardEntry[];
  isFirst: boolean;
  isLast: boolean;
  busy: boolean;
  previewing: boolean;
  onPatch: (id: string, patch: Parameters<typeof updateSound>[1]) => Promise<void>;
  onDelete: (id: string) => Promise<void>;
  onPreview: (entry: SoundboardEntry) => Promise<void>;
  onReorderUp: () => Promise<void>;
  onReorderDown: () => Promise<void>;
}

function SoundboardRow({
  entry,
  entriesTotal,
  isFirst,
  isLast,
  busy,
  previewing,
  onPatch,
  onDelete,
  onPreview,
  onReorderUp,
  onReorderDown,
}: RowProps) {
  const { t } = useTranslation(['app']);
  const [editingName, setEditingName] = useState(false);
  const [nameDraft, setNameDraft] = useState(entry.name);
  const [categoryDraft, setCategoryDraft] = useState(entry.category ?? '');
  const [capturingHotkey, setCapturingHotkey] = useState(false);
  const [hotkeyError, setHotkeyError] = useState<string | null>(null);

  useEffect(() => {
    setNameDraft(entry.name);
    setCategoryDraft(entry.category ?? '');
  }, [entry.name, entry.category]);

  useEffect(() => {
    if (!capturingHotkey) return;
    const onKey = (e: KeyboardEvent) => {
      e.preventDefault();
      e.stopPropagation();
      if (e.code === 'Escape') {
        setCapturingHotkey(false);
        setHotkeyError(null);
        return;
      }
      // Conflict checks: PTT + other soundboard entries with this code.
      const ptt = getPttSettings();
      if (ptt.enabled && ptt.key === e.code) {
        setHotkeyError(
          t('app:soundboard.hotkey_conflict_ptt', {
            defaultValue: 'Konflikt mit Push-to-Talk.',
          }),
        );
        return;
      }
      const taken = entriesTotal.find((s) => s.id !== entry.id && s.hotkey === e.code);
      if (taken) {
        setHotkeyError(
          t('app:soundboard.hotkey_conflict_sound', {
            defaultValue: 'Bereits von "{{name}}" belegt.',
            name: taken.name,
          }),
        );
        return;
      }
      setHotkeyError(null);
      setCapturingHotkey(false);
      void onPatch(entry.id, { hotkey: e.code });
    };
    window.addEventListener('keydown', onKey, { capture: true });
    return () => window.removeEventListener('keydown', onKey, { capture: true });
  }, [capturingHotkey, entriesTotal, entry.id, onPatch, t]);

  const hotkeyLabel = entry.hotkey ? codeToShortcut(entry.hotkey) : null;

  return (
    <li className="flex flex-wrap items-center gap-3 rounded-md border border-line bg-surface-3 px-3 py-2">
      <div className="flex min-w-0 flex-1 flex-col gap-0.5">
        {editingName ? (
          <input
            autoFocus
            value={nameDraft}
            onChange={(e) => setNameDraft(e.target.value)}
            onBlur={() => {
              setEditingName(false);
              if (nameDraft.trim() && nameDraft !== entry.name) {
                void onPatch(entry.id, { name: nameDraft });
              } else {
                setNameDraft(entry.name);
              }
            }}
            onKeyDown={(e) => {
              if (e.key === 'Enter') {
                (e.target as HTMLInputElement).blur();
              }
              if (e.key === 'Escape') {
                setNameDraft(entry.name);
                setEditingName(false);
              }
            }}
            className="w-full rounded border border-line bg-surface-2 px-2 py-1 text-sm text-fg focus:border-accent focus:outline-none"
          />
        ) : (
          <button
            type="button"
            onClick={() => setEditingName(true)}
            className="flex cursor-pointer items-center gap-1.5 self-start text-sm font-semibold text-fg hover:text-accent"
          >
            {entry.name}
            <PencilIcon className="h-3 w-3 opacity-50" />
          </button>
        )}
        <p className="text-[10px] text-fg-muted">
          {(entry.size / BYTES_PER_MB).toFixed(2)} MB · {entry.mime}
        </p>
      </div>

      <input
        type="text"
        value={categoryDraft}
        onChange={(e) => setCategoryDraft(e.target.value)}
        onBlur={() => {
          const next = categoryDraft.trim() || null;
          if (next !== (entry.category ?? null)) {
            void onPatch(entry.id, { category: next });
          }
        }}
        list={CATEGORY_LIST_ID}
        placeholder={t('app:soundboard.category_placeholder', {
          defaultValue: 'Kategorie…',
        })}
        className="w-32 shrink-0 rounded border border-line bg-surface-2 px-2 py-1 text-xs text-fg placeholder-fg-muted focus:border-accent focus:outline-none"
      />

      <div className="flex shrink-0 items-center gap-1">
        <button
          type="button"
          onClick={() => {
            setHotkeyError(null);
            setCapturingHotkey((v) => !v);
          }}
          className={
            'inline-flex min-w-[5rem] cursor-pointer items-center justify-center rounded border px-2 py-1 text-[11px] font-mono font-semibold transition ' +
            (capturingHotkey
              ? 'animate-pulse border-accent bg-accent/20 text-fg'
              : hotkeyLabel
                ? 'border-accent/40 bg-accent/10 text-accent'
                : 'border-line bg-surface-2 text-fg-muted hover:bg-surface')
          }
          title={t('app:soundboard.hotkey_capture', {
            defaultValue: 'Hotkey binden (Esc = abbrechen)',
          })}
        >
          {capturingHotkey
            ? t('app:soundboard.hotkey_press', { defaultValue: 'Drücke…' })
            : hotkeyLabel ??
              t('app:soundboard.hotkey_none', { defaultValue: 'Kein Hotkey' })}
        </button>
        {entry.hotkey && !capturingHotkey && (
          <button
            type="button"
            onClick={() => void onPatch(entry.id, { hotkey: null })}
            aria-label={t('app:soundboard.hotkey_clear', { defaultValue: 'Hotkey entfernen' })}
            title={t('app:soundboard.hotkey_clear', { defaultValue: 'Hotkey entfernen' })}
            className="flex h-6 w-6 cursor-pointer items-center justify-center rounded text-fg-muted hover:bg-surface-2 hover:text-fg"
          >
            ×
          </button>
        )}
      </div>

      <div className="flex shrink-0 items-center gap-2">
        <input
          type="range"
          min={0}
          max={1}
          step={0.05}
          value={entry.gain}
          onChange={(e) => void onPatch(entry.id, { gain: Number(e.target.value) })}
          className="accent-accent w-20"
          title={t('app:soundboard.gain_title', {
            defaultValue: 'Lautstärke',
          })}
        />
        <button
          type="button"
          onClick={() => void onPreview(entry)}
          disabled={busy}
          className="inline-flex cursor-pointer items-center gap-1 rounded border border-line bg-surface-2 px-2 py-1 text-[11px] font-medium text-fg transition hover:bg-surface disabled:opacity-50"
        >
          {previewing
            ? t('app:soundboard.preview_stop', { defaultValue: 'Stop' })
            : t('app:soundboard.preview', { defaultValue: 'Vorhören' })}
        </button>
        <button
          type="button"
          onClick={onReorderUp}
          disabled={busy || isFirst}
          aria-label={t('app:soundboard.move_up', { defaultValue: 'Nach oben' })}
          title={t('app:soundboard.move_up', { defaultValue: 'Nach oben' })}
          className="flex h-6 w-6 cursor-pointer items-center justify-center rounded text-fg-muted hover:bg-surface-2 hover:text-fg disabled:opacity-40"
        >
          ↑
        </button>
        <button
          type="button"
          onClick={onReorderDown}
          disabled={busy || isLast}
          aria-label={t('app:soundboard.move_down', { defaultValue: 'Nach unten' })}
          title={t('app:soundboard.move_down', { defaultValue: 'Nach unten' })}
          className="flex h-6 w-6 cursor-pointer items-center justify-center rounded text-fg-muted hover:bg-surface-2 hover:text-fg disabled:opacity-40"
        >
          ↓
        </button>
        <button
          type="button"
          onClick={() => void onDelete(entry.id)}
          disabled={busy}
          aria-label={t('app:soundboard.delete', { defaultValue: 'Löschen' })}
          title={t('app:soundboard.delete', { defaultValue: 'Löschen' })}
          className="flex h-6 w-6 cursor-pointer items-center justify-center rounded border border-rose-500/30 bg-rose-500/10 text-rose-600 hover:bg-rose-500/20 disabled:opacity-50 dark:text-rose-300"
        >
          <TrashIcon className="h-3 w-3" />
        </button>
      </div>

      {hotkeyError && capturingHotkey && (
        <p className="basis-full text-[11px] text-rose-600 dark:text-rose-300">
          {hotkeyError}
        </p>
      )}
    </li>
  );
}
