// Looping ringtones. Two patterns:
//   - outgoing: long calling tone, 3s cycle (oscillator only)
//   - incoming: classic "ring ring" double beep (oscillator), optionally
//     upgraded to a user-supplied audio file stored in ringtoneStorage
//
// The custom file plays immediately if we can load it; otherwise we fall
// back to the generated oscillator pattern so ringing never misses an
// incoming call due to an IO failure.

import { getAudioSettings, subscribeAudioSettings } from './audioSettings';
import { getIncomingRingtone } from './ringtoneStorage';

type Pattern = 'outgoing' | 'incoming';

class Ringtone {
  private ctx: AudioContext | null = null;
  private interval: number | null = null;
  private pattern: Pattern | null = null;

  // Custom-file playback path (incoming only).
  private audioEl: HTMLAudioElement | null = null;
  private customUrl: string | null = null;
  // Sequence token to ignore slow IO completing after user changed state.
  private startSeq = 0;
  // Live-subscribe so settings-slider changes reflect while the ringtone
  // is playing (user can hear the effect of their slider immediately).
  private unsubVolume: (() => void) | null = null;

  private get volume(): number {
    const v = getAudioSettings().ringtoneVolume;
    if (!Number.isFinite(v)) return 0.9;
    return Math.min(1, Math.max(0, v));
  }

  start(pattern: Pattern): void {
    if (this.pattern === pattern) return; // already playing this pattern
    this.stop();
    this.pattern = pattern;
    const seq = ++this.startSeq;

    // Track live slider moves so the user can dial in the volume while a
    // call is ringing and hear the change immediately.
    this.unsubVolume = subscribeAudioSettings(() => {
      if (this.audioEl) this.audioEl.volume = this.volume;
    });

    if (pattern === 'incoming') {
      // Kick off oscillator immediately so we never miss ringing feedback
      // while the custom file (if any) loads asynchronously. Once the blob
      // is ready we hand playback over to the <audio> element.
      this.startOscillator(pattern);
      void this.tryUpgradeToCustom(seq);
    } else {
      this.startOscillator(pattern);
    }
  }

  stop(): void {
    this.startSeq++;
    this.stopOscillator();
    this.stopCustom();
    this.pattern = null;
    if (this.unsubVolume) {
      this.unsubVolume();
      this.unsubVolume = null;
    }
  }

  // --- Custom file path (incoming only) ----------------------------------

  private async tryUpgradeToCustom(seq: number): Promise<void> {
    let stored;
    try {
      stored = await getIncomingRingtone();
    } catch {
      return; // keep oscillator
    }
    // User stopped or switched patterns while we were loading.
    if (seq !== this.startSeq || this.pattern !== 'incoming' || !stored) return;

    const url = URL.createObjectURL(stored.blob);
    const el = new Audio(url);
    el.loop = true;
    el.volume = this.volume;
    // Chrome/WKWebView autoplay policy: muted playback is always allowed,
    // but ringtones must be audible, so play() may reject the first time
    // before the user interacted. If it rejects, we keep the oscillator.
    el.play().catch(() => {
      URL.revokeObjectURL(url);
    });

    this.audioEl = el;
    this.customUrl = url;
    // Only swap off the oscillator once the custom element is actually
    // wired — avoids a silent gap on transition.
    this.stopOscillator();
  }

  private stopCustom(): void {
    if (this.audioEl) {
      try {
        this.audioEl.pause();
      } catch {
        /* ignore */
      }
      this.audioEl.src = '';
      this.audioEl = null;
    }
    if (this.customUrl) {
      URL.revokeObjectURL(this.customUrl);
      this.customUrl = null;
    }
  }

  // --- Oscillator fallback ----------------------------------------------

  private startOscillator(pattern: Pattern): void {
    const AudioCtx =
      window.AudioContext ??
      (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
    if (!AudioCtx) return;
    this.ctx = new AudioCtx();

    const play = pattern === 'outgoing' ? this.playOutgoing : this.playIncoming;
    play.call(this);
    this.interval = window.setInterval(
      () => play.call(this),
      pattern === 'outgoing' ? 3000 : 2000,
    );
  }

  private stopOscillator(): void {
    if (this.interval !== null) {
      window.clearInterval(this.interval);
      this.interval = null;
    }
    if (this.ctx) {
      void this.ctx.close().catch(() => {
        /* ignore */
      });
      this.ctx = null;
    }
  }

  private beep(freq: number, durationSec: number, delaySec: number, gain = 0.18): void {
    const ctx = this.ctx;
    if (!ctx) return;
    const osc = ctx.createOscillator();
    const g = ctx.createGain();
    osc.type = 'sine';
    osc.frequency.value = freq;
    osc.connect(g);
    g.connect(ctx.destination);
    const t0 = ctx.currentTime + delaySec;
    // Volume slider multiplies the base gain so the fallback tone tracks
    // the user's preference. A flat user-setting of 0 keeps the pattern
    // running visually (oscillator nodes alive) but inaudible.
    const effectiveGain = gain * this.volume;
    g.gain.setValueAtTime(0, t0);
    g.gain.linearRampToValueAtTime(effectiveGain, t0 + 0.02);
    g.gain.exponentialRampToValueAtTime(0.001, t0 + durationSec);
    osc.start(t0);
    osc.stop(t0 + durationSec + 0.02);
  }

  private playOutgoing(): void {
    // Soft calling tone — single warm note. Slightly bumped from 0.14 so
    // it's audible on laptop speakers without blasting.
    this.beep(440, 0.4, 0, 0.22);
    this.beep(440, 0.4, 0.6, 0.22);
  }

  private playIncoming(): void {
    // Classic double-ring "ring ring". Bumped from 0.22 → 0.4 so it's
    // unmissable through music / background noise.
    this.beep(880, 0.18, 0, 0.4);
    this.beep(660, 0.18, 0.22, 0.4);
    this.beep(880, 0.18, 0.6, 0.4);
    this.beep(660, 0.18, 0.82, 0.4);
  }
}

export const ringtone = new Ringtone();
