// Notification chime. Plays either a user-uploaded custom sound (if one
// exists in IndexedDB) or the built-in two-tone synth chime. Throttled
// so a burst of messages doesn't turn into a machine gun.
//
// Uses a single persistent AudioContext — a fresh one per notification
// lands in the `suspended` state under Chromium's autoplay policy
// whenever the user hasn't interacted recently, so the tone silently
// never plays.
//
// The custom AudioBuffer is decoded eagerly: on module init and on any
// upload/reset, kick the load so it's ready before the next incoming
// message. Playback always awaits `ctx.resume()` before starting the
// source so we don't race an async resume in a realtime-event context
// that has no prior user gesture.

import {
  getCustomNotificationSound,
  subscribeNotificationSoundChanges,
} from './notificationSoundStorage';

let ctx: AudioContext | null = null;
let lastPlay = 0;

// Cached decoded custom sound. `null` = no custom upload (fall back to
// synth). `undefined` = not yet loaded. `false` = decode failed.
let customBuffer: AudioBuffer | null | undefined | false = undefined;
let customLoadInFlight: Promise<void> | null = null;

function getCtx(): AudioContext | null {
  if (ctx) return ctx;
  const AudioCtx =
    window.AudioContext ??
    (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
  if (!AudioCtx) return null;
  ctx = new AudioCtx();
  return ctx;
}

async function loadCustomBuffer(c: AudioContext): Promise<void> {
  try {
    const record = await getCustomNotificationSound();
    if (!record) {
      customBuffer = null;
      return;
    }
    const arr = await record.blob.arrayBuffer();
    const buf = await c.decodeAudioData(arr.slice(0));
    customBuffer = buf;
  } catch (err: unknown) {
    console.warn('notification: custom sound decode failed', err);
    customBuffer = false;
  }
}

function kickLoad(): void {
  if (customLoadInFlight) return;
  const c = getCtx();
  if (!c) return;
  customLoadInFlight = loadCustomBuffer(c).finally(() => {
    customLoadInFlight = null;
  });
}

subscribeNotificationSoundChanges(() => {
  customBuffer = undefined;
  customLoadInFlight = null;
  kickLoad();
});

// Eager-load at module init so the buffer is ready before the first
// notification fires. Safe to call at top level — decodeAudioData
// doesn't need the context running.
kickLoad();

function playCustom(c: AudioContext, buffer: AudioBuffer): void {
  const src = c.createBufferSource();
  src.buffer = buffer;
  const gain = c.createGain();
  gain.gain.value = 1;
  src.connect(gain);
  gain.connect(c.destination);
  src.start();
}

function playSynthTone(c: AudioContext): void {
  const master = c.createGain();
  master.connect(c.destination);
  master.gain.value = 0.12;

  const tones: { freq: number; delay: number }[] = [
    { freq: 880, delay: 0 },
    { freq: 1320, delay: 0.08 },
  ];
  for (const { freq, delay } of tones) {
    const osc = c.createOscillator();
    const gain = c.createGain();
    osc.type = 'sine';
    osc.frequency.value = freq;
    osc.connect(gain);
    gain.connect(master);
    const t0 = c.currentTime + delay;
    gain.gain.setValueAtTime(0, t0);
    gain.gain.linearRampToValueAtTime(1, t0 + 0.015);
    gain.gain.exponentialRampToValueAtTime(0.001, t0 + 0.28);
    osc.start(t0);
    osc.stop(t0 + 0.3);
  }
}

export function playNotificationTone(): void {
  const now = Date.now();
  if (now - lastPlay < 800) return;
  lastPlay = now;

  void (async () => {
    const c = getCtx();
    if (!c) return;
    if (c.state === 'suspended') {
      try {
        await c.resume();
      } catch {
        /* autoplay denial — silent this round */
        return;
      }
    }

    // If the load hasn't finished yet (first notification after a cold
    // start, before the eager load completed) wait briefly for it so
    // we don't emit the synth tone over a user-configured custom clip.
    // Cap the wait so a never-resolving decode doesn't block a message.
    if (customBuffer === undefined && customLoadInFlight) {
      const timeout = new Promise<void>((r) => window.setTimeout(r, 500));
      await Promise.race([customLoadInFlight, timeout]);
    }

    if (customBuffer && typeof customBuffer !== 'boolean') {
      playCustom(c, customBuffer);
      return;
    }
    playSynthTone(c);
  })();
}
