import CallSound from '../../../public/sound/call.ogg';
import { RingtoneId } from '../state/settings';

export const RINGTONE_OPTIONS: { value: RingtoneId; label: string }[] = [
  { value: 'classic', label: 'Classic' },
  { value: 'chime', label: 'Chime' },
  { value: 'soft', label: 'Soft' },
  { value: 'retro', label: 'Retro' },
  { value: 'none', label: 'Silent' },
];

export const isRingtoneId = (v: unknown): v is RingtoneId =>
  v === 'classic' || v === 'chime' || v === 'soft' || v === 'retro' || v === 'none';

type SynthStyle = 'chime' | 'soft' | 'retro';

const clamp01 = (n: number): number => Math.max(0, Math.min(1, n));

// Shared WebAudio context for synthesized ringtones. Kept separate from the
// join/leave-sound context (callSounds.ts) to keep blast radius small.
let sharedCtx: AudioContext | undefined;
const getCtx = (): AudioContext | undefined => {
  try {
    if (!sharedCtx || sharedCtx.state === 'closed') sharedCtx = new AudioContext();
    if (sharedCtx.state === 'suspended') sharedCtx.resume().catch(() => undefined);
    return sharedCtx;
  } catch {
    return undefined;
  }
};

/**
 * C-L3 — prime (create + resume) the shared ringtone AudioContext from within a
 * user gesture. Browsers keep a fresh context suspended until a gesture, and an
 * incoming-call ring fires with no gesture of its own, so the first ring after a
 * cold page load could be silent (resume() is async and may not finish before
 * the notes are scheduled). Call this on any early app gesture so a later ring
 * plays through an already-running context. Mirrors `unlockCallSounds`.
 */
export const unlockRingtoneAudio = (): void => {
  getCtx();
};

type Note = {
  freq: number;
  /** Offset from phrase start, in seconds */
  at: number;
  /** Duration in seconds */
  dur: number;
};

// One looping phrase per synth style + the period before it repeats.
const PHRASES: Record<
  SynthStyle,
  { type: OscillatorType; gain: number; period: number; notes: Note[] }
> = {
  // Two-tone "ring … ring" telephone cadence.
  chime: {
    type: 'sine',
    gain: 0.3,
    period: 3,
    notes: [
      { freq: 587.33, at: 0, dur: 0.35 },
      { freq: 880, at: 0.4, dur: 0.35 },
      { freq: 587.33, at: 1.0, dur: 0.35 },
      { freq: 880, at: 1.4, dur: 0.35 },
    ],
  },
  // Gentle rising triangle pair.
  soft: {
    type: 'triangle',
    gain: 0.24,
    period: 3.2,
    notes: [
      { freq: 523.25, at: 0, dur: 0.5 },
      { freq: 659.25, at: 0.55, dur: 0.7 },
    ],
  },
  // Retro arpeggio sweep.
  retro: {
    type: 'square',
    gain: 0.12,
    period: 2.4,
    notes: [
      { freq: 440, at: 0, dur: 0.12 },
      { freq: 554.37, at: 0.13, dur: 0.12 },
      { freq: 659.25, at: 0.26, dur: 0.12 },
      { freq: 880, at: 0.39, dur: 0.22 },
    ],
  },
};

const playPhrase = (style: SynthStyle, volume: number, destination: AudioNode): void => {
  const ctx = getCtx();
  if (!ctx) return;
  const { type, gain: peak, notes } = PHRASES[style];
  const scaledPeak = peak * clamp01(volume);
  if (scaledPeak <= 0) return;
  const now = ctx.currentTime;
  notes.forEach(({ freq, at, dur }) => {
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = type;
    osc.frequency.value = freq;
    const start = now + at;
    // Short attack + exponential decay to avoid clicks.
    gain.gain.setValueAtTime(0, start);
    gain.gain.linearRampToValueAtTime(scaledPeak, start + 0.015);
    gain.gain.exponentialRampToValueAtTime(0.0001, start + dur);
    osc.connect(gain);
    gain.connect(destination);
    osc.start(start);
    osc.stop(start + dur + 0.02);
  });
};

// The bundled call.ogg is mastered near full scale, so at equal `volume` it is
// perceptibly much louder than the synthesized styles (which peak at ~0.12–0.3).
// Attenuate it so all ringtones sit at a comparable loudness.
const CLASSIC_GAIN = 0.45;

const startClassic = (volume: number, loop: boolean): (() => void) => {
  let audio: HTMLAudioElement | undefined;
  try {
    audio = new Audio(CallSound);
    audio.loop = loop;
    audio.volume = clamp01(volume) * CLASSIC_GAIN;
    audio.play().catch(() => undefined);
  } catch {
    audio = undefined;
  }
  return () => {
    if (!audio) return;
    audio.pause();
    audio.currentTime = 0;
    audio = undefined;
  };
};

const startSynth = (style: SynthStyle, volume: number, loop: boolean): (() => void) => {
  const ctx = getCtx();
  if (!ctx) return () => undefined;
  // All notes route through a per-session master gain so stop() can silence
  // everything instantly — including notes already scheduled slightly in the
  // future — instead of letting the last phrase ring out after the user answers.
  const master = ctx.createGain();
  master.gain.value = 1;
  master.connect(ctx.destination);

  playPhrase(style, volume, master);
  const id = loop
    ? window.setInterval(() => playPhrase(style, volume, master), PHRASES[style].period * 1000)
    : 0;

  let stopped = false;
  return () => {
    if (stopped) return;
    stopped = true;
    if (id) window.clearInterval(id);
    try {
      const now = ctx.currentTime;
      master.gain.cancelScheduledValues(now);
      master.gain.setValueAtTime(master.gain.value, now);
      master.gain.linearRampToValueAtTime(0, now + 0.03);
    } catch {
      /* context may be closed */
    }
    window.setTimeout(() => {
      try {
        master.disconnect();
      } catch {
        /* already disconnected */
      }
    }, 100);
  };
};

/**
 * Start an incoming-call ringtone, looping until the returned stop fn is
 * called. `volume` is 0..1. Returns a no-op stop fn for 'none'.
 *
 * Synthesized styles share the WebAudio autoplay limitation of the bundled
 * 'classic' file: until the page has had a user gesture the browser may keep
 * audio suspended, so the very first ring after a cold page load can be
 * silent. This matches the pre-existing behaviour of the classic ringtone.
 */
export const startRingtone = (id: RingtoneId, volume: number): (() => void) => {
  // C-L2 — a real incoming ring supersedes any lingering Settings preview so the
  // two don't overlap (the preview is otherwise only cleared by its own timer).
  activePreviewStop?.();
  activePreviewStop = null;
  if (id === 'none') return () => undefined;
  if (id === 'classic') return startClassic(volume, true);
  return startSynth(id, volume, true);
};

// Only one preview may sound at a time; starting a new one cancels the last.
let activePreviewStop: (() => void) | null = null;

/**
 * Play a single, non-looping preview of a ringtone (used by Settings).
 * Auto-stops the bundled 'classic' clip after a few seconds and cancels any
 * previously-playing preview. Returns a stop fn for early cancellation.
 */
export const previewRingtone = (id: RingtoneId, volume: number): (() => void) => {
  activePreviewStop?.();
  activePreviewStop = null;
  if (id === 'none') return () => undefined;

  const stop = id === 'classic' ? startClassic(volume, false) : startSynth(id, volume, false);
  let timer = 0;
  const wrapped = () => {
    window.clearTimeout(timer);
    stop();
    if (activePreviewStop === wrapped) activePreviewStop = null;
  };
  timer = window.setTimeout(wrapped, 4000);
  activePreviewStop = wrapped;
  return wrapped;
};
