// IndexedDB-backed custom notification sound. Single-row object store —
// user either has one uploaded clip overriding the built-in two-tone
// chime, or doesn't and we fall back to the synthesized tone.
//
// Separate from `soundboardStorage.ts` because the soundboard is a
// multi-entry user library with categories + hotkeys + per-clip gain;
// this module only needs "one blob, replace-on-upload, wipe-on-reset".

export interface NotificationSoundEntry {
  filename: string;
  mime: string;
  size: number;
  uploadedAt: number;
}

interface StoredNotificationSound extends NotificationSoundEntry {
  blob: Blob;
}

// 1 MB cap — notification sounds should be short (<5s), and we don't
// want an IDB quota bust from a 200MB MP3.
export const MAX_NOTIFICATION_SOUND_BYTES = 1 * 1024 * 1024;

// --- Change observer — the notificationSound module subscribes to
// invalidate its cached AudioBuffer when upload/reset happens, so the
// next ping uses the fresh sound without an app restart.

type Listener = () => void;
const listeners = new Set<Listener>();

export function subscribeNotificationSoundChanges(l: Listener): () => void {
  listeners.add(l);
  return () => {
    listeners.delete(l);
  };
}

function notifyChange(): void {
  for (const l of listeners) {
    try {
      l();
    } catch (err: unknown) {
      console.warn('notification-sound change listener threw', err);
    }
  }
}

const DB_NAME = 'netralax-notification';
const DB_VERSION = 1;
const STORE = 'sound';
const KEY = 'current';

let dbPromise: Promise<IDBDatabase> | null = null;

function openDb(): Promise<IDBDatabase> {
  if (dbPromise) return dbPromise;
  dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, DB_VERSION);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains(STORE)) {
        db.createObjectStore(STORE);
      }
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error ?? new Error('indexedDB open failed'));
  });
  return dbPromise;
}

export async function getCustomNotificationSound(): Promise<
  { blob: Blob; entry: NotificationSoundEntry } | null
> {
  const db = await openDb();
  return new Promise((resolve, reject) => {
    const t = db.transaction(STORE, 'readonly');
    const req = t.objectStore(STORE).get(KEY);
    req.onsuccess = () => {
      const stored = req.result as StoredNotificationSound | undefined;
      if (!stored) {
        resolve(null);
        return;
      }
      const { blob, ...entry } = stored;
      resolve({ blob, entry });
    };
    req.onerror = () => reject(req.error);
    t.onerror = () => reject(t.error);
  });
}

export async function getCustomNotificationSoundMeta(): Promise<NotificationSoundEntry | null> {
  const res = await getCustomNotificationSound();
  return res ? res.entry : null;
}

export async function setCustomNotificationSound(file: File): Promise<NotificationSoundEntry> {
  if (file.size === 0) throw new Error('empty_file');
  if (file.size > MAX_NOTIFICATION_SOUND_BYTES) throw new Error('sound_too_large');
  const mime = file.type || 'application/octet-stream';
  if (!mime.startsWith('audio/')) throw new Error('sound_not_audio');

  const stored: StoredNotificationSound = {
    filename: file.name || 'notification.audio',
    mime,
    size: file.size,
    uploadedAt: Date.now(),
    blob: file,
  };

  const db = await openDb();
  await new Promise<void>((resolve, reject) => {
    const t = db.transaction(STORE, 'readwrite');
    const req = t.objectStore(STORE).put(stored, KEY);
    req.onsuccess = () => resolve();
    req.onerror = () => reject(req.error);
    t.onerror = () => reject(t.error);
  });

  notifyChange();
  const { blob: _blob, ...entry } = stored;
  return entry;
}

export async function clearCustomNotificationSound(): Promise<void> {
  const db = await openDb();
  await new Promise<void>((resolve, reject) => {
    const t = db.transaction(STORE, 'readwrite');
    const req = t.objectStore(STORE).delete(KEY);
    req.onsuccess = () => resolve();
    req.onerror = () => reject(req.error);
    t.onerror = () => reject(t.error);
  });
  notifyChange();
}
