// audioManager.js - Procedural bass rhythm + speakAI
//
// API:
//   unlockBackgroundAudio()  — call on click/key (starts beat)
//   updateAudio()            — call every frame (bassPulse + speakAI)
//   playSceneWhoosh(options) — scene transition whistle (overlaps other audio)
//   playLongSceneWhoosh()    — single extended whoosh for key moments
//   audioState.bassPulse     — 0..1 rhythm signal for visuals

let audioContext;
let analyser;
let masterGain;
let kickBuffer;
let beatTimerId;
let droneOsc;
let droneGain;
let droneHarmOsc;
let droneHarmGain;
let kickGain;
let sceneVoiceSuppressed = false;
let frequencyData;
let isAudioInitialized = false;
let isAudioRunning = false;
let rhythmStartTime = 0;

let lastVoiceTriggerTime = 0;
let nextAmbientVoiceTime = 0;
const BPM_FAST = 90;
const BPM_SLOW = 52;
const VOICE_COOLDOWN_MS = 7000;
const AMBIENT_VOICE_MIN_MS = 11000;
const AMBIENT_VOICE_MAX_MS = 22000;
const KICK_MUTE_DURING_VOICE_MS = 1400;

let currentBpm = BPM_FAST;
let slowBeatMode = false;
let beatActive = true;
let kickMutedUntil = 0;
let audioEndFading = false;
const MASTER_GAIN_LEVEL = 0.55;

function getBeatMs(bpm) {
  return (60 / bpm) * 1000;
}

const audioState = {
  bassPulse: 0.0,
  rawBassEnergy: 0.0,
  currentTime: 0.0,
};

function isBeatPlaying() {
  return isAudioInitialized && isAudioRunning && audioContext.state === 'running';
}

function scheduleNextAmbientVoice() {
  const delay =
    AMBIENT_VOICE_MIN_MS +
    Math.random() * (AMBIENT_VOICE_MAX_MS - AMBIENT_VOICE_MIN_MS);
  nextAmbientVoiceTime = performance.now() + delay;
}

function preloadVoices() {
  if (!('speechSynthesis' in window)) return;
  window.speechSynthesis.getVoices();
  window.speechSynthesis.addEventListener('voiceschanged', () => {}, { once: true });
}

function playVoiceAccent() {
  if (!audioContext || audioContext.state !== 'running') return;

  const now = audioContext.currentTime;
  const freqs = [82, 110, 147];

  freqs.forEach((freq, i) => {
    const osc = audioContext.createOscillator();
    const gain = audioContext.createGain();

    osc.type = 'sine';
    osc.frequency.setValueAtTime(freq, now + i * 0.06);

    const t = now + i * 0.06;
    gain.gain.setValueAtTime(0.0001, t);
    gain.gain.exponentialRampToValueAtTime(0.007, t + 0.02);
    gain.gain.exponentialRampToValueAtTime(0.0001, t + 0.35);

    osc.connect(gain);
    gain.connect(masterGain);
    osc.start(t);
    osc.stop(t + 0.35);
  });
}

function speakAI() {
  if (!('speechSynthesis' in window)) return;

  const utterance = new SpeechSynthesisUtterance('I');
  utterance.pitch = 0.32 + Math.random() * 0.15;
  utterance.rate = 0.24 + Math.random() * 0.08;
  utterance.volume = 0.1;

  const voices = window.speechSynthesis.getVoices();
  const englishVoice = voices.find((v) => v.lang.startsWith('en'));
  if (englishVoice) utterance.voice = englishVoice;

  window.speechSynthesis.speak(utterance);
  playVoiceAccent();
}

function trySpeakAI(probability) {
  if (audioEndFading || sceneVoiceSuppressed) return false;
  const now = performance.now();
  if (now - lastVoiceTriggerTime < VOICE_COOLDOWN_MS) return false;
  if (Math.random() >= probability) return false;

  speakAI();
  lastVoiceTriggerTime = now;
  kickMutedUntil = now + KICK_MUTE_DURING_VOICE_MS;
  return true;
}

function createKickBuffer(ctx) {
  const sampleRate = ctx.sampleRate;
  const duration = 0.35;
  const length = Math.floor(sampleRate * duration);
  const buffer = ctx.createBuffer(1, length, sampleRate);
  const data = buffer.getChannelData(0);

  for (let i = 0; i < length; i++) {
    const t = i / sampleRate;
    const envelope = Math.exp(-t * 10);
    const pitch = 95 * Math.exp(-t * 9) + 42;
    data[i] = Math.sin(2 * Math.PI * pitch * t) * envelope * 0.9;
  }

  return buffer;
}

function playKick() {
  if (!audioContext || !kickBuffer || audioContext.state !== 'running') return;
  if (performance.now() < kickMutedUntil) return;
  if (trySpeakAI(0.035)) return;

  const source = audioContext.createBufferSource();
  source.buffer = kickBuffer;
  source.connect(kickGain);
  source.start();

  audioState.bassPulse = Math.max(audioState.bassPulse, 0.85);
}

function playSingleWhoosh(startTime, duration, pitchScale = 1) {
  const p = pitchScale;
  const osc = audioContext.createOscillator();
  const breathOsc = audioContext.createOscillator();
  const whooshGain = audioContext.createGain();
  const breathGain = audioContext.createGain();
  const filter = audioContext.createBiquadFilter();

  osc.type = 'triangle';
  osc.frequency.setValueAtTime(320 * p, startTime);
  osc.frequency.exponentialRampToValueAtTime(1450 * p, startTime + duration * 0.28);
  osc.frequency.exponentialRampToValueAtTime(380 * p, startTime + duration);

  breathOsc.type = 'sine';
  breathOsc.frequency.setValueAtTime(520 * p, startTime);
  breathOsc.frequency.exponentialRampToValueAtTime(980 * p, startTime + duration * 0.24);
  breathOsc.frequency.exponentialRampToValueAtTime(260 * p, startTime + duration * 0.85);

  filter.type = 'bandpass';
  filter.frequency.setValueAtTime(650 * p, startTime);
  filter.frequency.exponentialRampToValueAtTime(1800 * p, startTime + duration * 0.22);
  filter.frequency.exponentialRampToValueAtTime(500 * p, startTime + duration);
  filter.Q.value = 3.2;

  whooshGain.gain.setValueAtTime(0.0001, startTime);
  whooshGain.gain.exponentialRampToValueAtTime(0.2, startTime + duration * 0.06);
  whooshGain.gain.exponentialRampToValueAtTime(0.0001, startTime + duration);

  breathGain.gain.setValueAtTime(0.0001, startTime);
  breathGain.gain.exponentialRampToValueAtTime(0.08, startTime + duration * 0.04);
  breathGain.gain.exponentialRampToValueAtTime(0.0001, startTime + duration * 0.75);

  osc.connect(filter);
  filter.connect(whooshGain);
  whooshGain.connect(masterGain);
  breathOsc.connect(breathGain);
  breathGain.connect(masterGain);

  osc.start(startTime);
  osc.stop(startTime + duration);
  breathOsc.start(startTime);
  breathOsc.stop(startTime + duration);
}

function playSceneWhoosh(options = {}) {
  if (!audioContext || audioContext.state !== 'running') return;

  const repeats = options.repeats ?? 3;
  const duration = options.duration ?? 1.85;
  const gap = options.gap ?? 0.3;
  const now = audioContext.currentTime;

  for (let i = 0; i < repeats; i += 1) {
    playSingleWhoosh(now + i * (duration + gap), duration);
  }
}

function playLongSceneWhoosh() {
  if (!audioContext || audioContext.state !== 'running') return;
  playSingleWhoosh(audioContext.currentTime, 5.2, 0.78);
}

function playThankYouBlims() {
  if (!audioContext || audioContext.state !== 'running') return;

  const now = audioContext.currentTime;
  const freqs = [392, 494, 587, 698, 784, 880, 988, 1175];

  freqs.forEach((freq, i) => {
    const osc = audioContext.createOscillator();
    const gain = audioContext.createGain();
    const filter = audioContext.createBiquadFilter();

    osc.type = 'sine';
    osc.frequency.setValueAtTime(freq * (0.99 + Math.random() * 0.02), now + i * 0.13);

    filter.type = 'highpass';
    filter.frequency.value = 280;

    const t = now + i * 0.13;
    gain.gain.setValueAtTime(0.0001, t);
    gain.gain.exponentialRampToValueAtTime(0.055, t + 0.01);
    gain.gain.exponentialRampToValueAtTime(0.0001, t + 0.22);

    osc.connect(filter);
    filter.connect(gain);
    gain.connect(masterGain);
    osc.start(t);
    osc.stop(t + 0.24);
  });
}

function startSubDrone() {
  if (droneOsc) return;

  kickGain = audioContext.createGain();
  kickGain.gain.value = 1;
  kickGain.connect(masterGain);

  droneOsc = audioContext.createOscillator();
  droneGain = audioContext.createGain();
  droneOsc.type = 'sine';
  droneOsc.frequency.value = 48;
  droneGain.gain.value = 0.12;
  droneOsc.connect(droneGain);
  droneGain.connect(masterGain);
  droneOsc.start();

  droneHarmOsc = audioContext.createOscillator();
  droneHarmGain = audioContext.createGain();
  droneHarmOsc.type = 'sine';
  droneHarmOsc.frequency.value = 132;
  droneHarmGain.gain.value = 0;
  droneHarmOsc.connect(droneHarmGain);
  droneHarmGain.connect(masterGain);
  droneHarmOsc.start();
}

function updateSceneAudio(sceneName, sceneTime, sceneDuration) {
  if (!audioContext || !isAudioRunning || audioEndFading || !droneOsc) return;

  const now = audioContext.currentTime;
  const progress = sceneDuration > 0 ? Math.min(Math.max(sceneTime / sceneDuration, 0), 1) : 0;

  if (sceneName === 'Quantum Cathedral — Ascent') {
    sceneVoiceSuppressed = true;
    droneOsc.frequency.setTargetAtTime(44 + progress * 10, now, 0.12);
    droneGain.gain.setTargetAtTime(0.1 + progress * 0.045, now, 0.12);
    droneHarmOsc.frequency.setTargetAtTime(130 + progress * 28, now, 0.12);
    droneHarmGain.gain.setTargetAtTime(0.018 + progress * 0.038, now, 0.12);
    if (kickGain) {
      kickGain.gain.setTargetAtTime(0.5 + progress * 0.28, now, 0.12);
    }
    return;
  }

  sceneVoiceSuppressed = false;
  droneOsc.frequency.setTargetAtTime(48, now, 0.2);
  droneGain.gain.setTargetAtTime(0.12, now, 0.2);
  if (droneHarmGain) {
    droneHarmGain.gain.setTargetAtTime(0, now, 0.2);
  }
  if (kickGain) {
    kickGain.gain.setTargetAtTime(1, now, 0.2);
  }
}

function startBeatTimer() {
  if (beatTimerId) return;

  playKick();
  beatTimerId = setInterval(playKick, getBeatMs(currentBpm));
}

function setBeatActive(active) {
  const wantActive = !!active;
  if (wantActive === beatActive) return;

  beatActive = wantActive;
  if (!isAudioRunning) return;

  stopBeatTimer();
  if (beatActive) {
    startBeatTimer();
  }
}

function setBeatPace(slow) {
  const wantSlow = !!slow;
  if (wantSlow === slowBeatMode) return;

  slowBeatMode = wantSlow;
  currentBpm = wantSlow ? BPM_SLOW : BPM_FAST;

  if (!isAudioRunning || !beatActive) return;

  stopBeatTimer();
  startBeatTimer();
}

function stopBeatTimer() {
  if (beatTimerId) {
    clearInterval(beatTimerId);
    beatTimerId = null;
  }
}

function initAudioEngine() {
  if (isAudioInitialized) return;

  const AudioContext = window.AudioContext || window.webkitAudioContext;
  audioContext = new AudioContext();

  masterGain = audioContext.createGain();
  masterGain.gain.value = MASTER_GAIN_LEVEL;

  analyser = audioContext.createAnalyser();
  analyser.fftSize = 512;
  analyser.smoothingTimeConstant = 0.75;

  masterGain.connect(analyser);
  analyser.connect(audioContext.destination);

  kickBuffer = createKickBuffer(audioContext);
  frequencyData = new Uint8Array(analyser.frequencyBinCount);
  isAudioInitialized = true;

  preloadVoices();
}

async function unlockBackgroundAudio() {
  initAudioEngine();

  if (audioContext.state === 'suspended') {
    try {
      await audioContext.resume();
    } catch (error) {
      isAudioRunning = false;
      return false;
    }
  }

  if (audioContext.state !== 'running') {
    isAudioRunning = false;
    return false;
  }

  rhythmStartTime = performance.now();
  scheduleNextAmbientVoice();
  startSubDrone();
  startBeatTimer();

  isAudioRunning = true;
  return true;
}

async function startBackgroundAudio() {
  return unlockBackgroundAudio();
}

function updateAudio() {
  if (!isAudioRunning || audioEndFading) return;

  audioState.currentTime = (performance.now() - rhythmStartTime) / 1000;

  if (audioState.bassPulse > 0.01) {
    audioState.bassPulse *= 0.9;
  }

  const now = performance.now();
  if (now >= nextAmbientVoiceTime) {
    trySpeakAI(0.1);
    scheduleNextAmbientVoice();
  }

  if (!analyser) return;

  analyser.getByteFrequencyData(frequencyData);

  let bassSum = 0;
  for (let i = 1; i <= 8; i++) {
    bassSum += frequencyData[i];
  }

  const averageBass = bassSum / (8 * 255);
  audioState.rawBassEnergy = averageBass;

  if (averageBass > 0.15) {
    audioState.bassPulse = Math.max(audioState.bassPulse, averageBass);
  }

  audioState.bassPulse = Math.min(Math.max(audioState.bassPulse, 0.0), 1.0);
}

function fadeOutBackgroundAudio(durationSeconds) {
  if (!audioContext || !masterGain || audioEndFading) return;

  audioEndFading = true;
  stopBeatTimer();

  if ('speechSynthesis' in window) {
    window.speechSynthesis.cancel();
  }

  const now = audioContext.currentTime;
  masterGain.gain.cancelScheduledValues(now);
  masterGain.gain.setValueAtTime(masterGain.gain.value, now);
  masterGain.gain.linearRampToValueAtTime(0.0001, now + durationSeconds);
}

function stopBackgroundAudio() {
  isAudioRunning = false;
  audioEndFading = false;
  stopBeatTimer();

  if (droneOsc) {
    try {
      droneOsc.stop();
    } catch (error) {
      // Already stopped.
    }
    droneOsc.disconnect();
    droneGain.disconnect();
    droneOsc = null;
    droneGain = null;
  }

  if (droneHarmOsc) {
    try {
      droneHarmOsc.stop();
    } catch (error) {
      // Already stopped.
    }
    droneHarmOsc.disconnect();
    droneHarmGain.disconnect();
    droneHarmOsc = null;
    droneHarmGain = null;
  }

  kickGain = null;
  sceneVoiceSuppressed = false;

  if (audioContext && audioContext.state === 'running') {
    audioContext.suspend();
  }

  if (masterGain) {
    masterGain.gain.value = MASTER_GAIN_LEVEL;
  }
}

window.audioState = audioState;
window.isBeatPlaying = isBeatPlaying;
window.initAudio = initAudioEngine;
window.updateAudio = updateAudio;
window.startBackgroundAudio = startBackgroundAudio;
window.unlockBackgroundAudio = unlockBackgroundAudio;
window.stopBackgroundAudio = stopBackgroundAudio;
window.setBeatPace = setBeatPace;
window.setBeatActive = setBeatActive;
window.playSceneWhoosh = playSceneWhoosh;
window.playLongSceneWhoosh = playLongSceneWhoosh;
window.playThankYouBlims = playThankYouBlims;
window.updateSceneAudio = updateSceneAudio;
window.fadeOutBackgroundAudio = fadeOutBackgroundAudio;
window.speakAI = speakAI;
