import { buildMarkerSchedule } from './markerSchedule';
import {
  resolvePressSampleKey,
  resolveReleaseSampleKey,
  type SoundPhase,
} from './keySoundMap';
import { createSampleCache, type SampleCache } from './sampleCache';
import type { Marker } from '../types';
import { app } from '../store.svelte';

export type AudioEngine = {
  /** Wire video element into the Web Audio graph and preload the active switch. */
  init: (video: HTMLVideoElement) => Promise<void>;
  /**
   * Schedule upcoming press/release samples synced to video playback.
   * Delays are divided by playbackRate so SFX stays aligned during speed-adjusted preview.
   */
  reschedule: (
    currentTime: number,
    markers: Marker[],
    isPlaying: boolean,
    playbackRate: number,
  ) => void;
  /** Immediate press/release feedback during live annotation (not rate-scaled). */
  playKey: (code: string, keyLabel: string, phase: SoundPhase) => void;
  preloadActiveSwitch: () => Promise<void>;
  /** Return the shared sample cache when initialized, otherwise null. */
  getSampleCache: () => SampleCache | null;
  destroy: () => void;
};

export function createAudioEngine(): AudioEngine {
  let ctx: AudioContext | null = null;
  let videoSource: MediaElementAudioSourceNode | null = null;
  let videoGain: GainNode | null = null;
  let sfxGain: GainNode | null = null;
  let cache: SampleCache | null = null;
  const scheduledSources: AudioBufferSourceNode[] = [];

  async function ensureContext(): Promise<AudioContext> {
    if (!ctx) {
      ctx = new AudioContext();
      cache = createSampleCache(ctx);
      sfxGain = ctx.createGain();
      sfxGain.connect(ctx.destination);
    }
    if (ctx.state === 'suspended') await ctx.resume();
    return ctx;
  }

  function getCache(): SampleCache {
    if (!cache) throw new Error('Audio engine not initialized');
    return cache;
  }

  function clearScheduled(): void {
    for (const source of scheduledSources) {
      try {
        source.stop();
      } catch {
        // already stopped
      }
    }
    scheduledSources.length = 0;
  }

  function playBuffer(buffer: AudioBuffer, when = 0): AudioBufferSourceNode | undefined {
    if (!ctx || !sfxGain) return;
    const source = ctx.createBufferSource();
    source.buffer = buffer;
    source.connect(sfxGain);
    source.start(when);
    return source;
  }

  async function resolveAndPlay(
    code: string,
    keyLabel: string,
    phase: SoundPhase,
    when = 0,
  ): Promise<AudioBufferSourceNode | null | undefined> {
    const sampleCache = getCache();
    const switchType = app.activeSwitch;
    const manifest = await sampleCache.ensureManifest();
    const entry = manifest[switchType];
    if (!entry) return null;

    const sampleKey =
      phase === 'press'
        ? resolvePressSampleKey(keyLabel, code, entry.press)
        : resolveReleaseSampleKey(keyLabel, entry.release);

    if (sampleCache.getLoadedSwitch() !== switchType) {
      await sampleCache.preloadSwitch(switchType);
    }

    const buffer = await sampleCache.loadBuffer(switchType, phase, sampleKey);
    return playBuffer(buffer, when);
  }

  return {
    async init(video: HTMLVideoElement) {
      const audioCtx = await ensureContext();
      if (!videoSource) {
        videoSource = audioCtx.createMediaElementSource(video);
        videoGain = audioCtx.createGain();
        videoSource.connect(videoGain);
        videoGain.connect(audioCtx.destination);
      }
      await getCache().preloadSwitch(app.activeSwitch);
    },

    reschedule(time: number, markers: Marker[], playing: boolean, playbackRate: number) {
      if (!ctx || !playing) {
        clearScheduled();
        return;
      }

      clearScheduled();
      const audioCtx = ctx;
      const startAt = audioCtx.currentTime + 0.02;
      const rate = playbackRate > 0 ? playbackRate : 1;
      const switchType = app.activeSwitch;

      void (async () => {
        const sampleCache = getCache();
        const manifest = await sampleCache.ensureManifest();
        const entry = manifest[switchType];
        if (!entry) return;

        if (sampleCache.getLoadedSwitch() !== switchType) {
          await sampleCache.preloadSwitch(switchType);
        }

        const schedule = buildMarkerSchedule(markers, entry);
        const windowStart = time - 0.05;

        for (const event of schedule) {
          if (event.time < windowStart) continue;
          if (event.phase === 'release' && event.sampleKey === 'GENERIC_long') continue;

          const delay = (event.time - time) / rate;
          const buffer = await sampleCache.loadBuffer(switchType, event.phase, event.sampleKey);
          const source = playBuffer(buffer, startAt + Math.max(0, delay));
          if (source) scheduledSources.push(source);
        }
      })();
    },

    playKey(code: string, keyLabel: string, phase: SoundPhase) {
      void resolveAndPlay(code, keyLabel, phase);
    },

    async preloadActiveSwitch() {
      await ensureContext();
      await getCache().preloadSwitch(app.activeSwitch);
    },

    getSampleCache() {
      return cache;
    },

    destroy() {
      clearScheduled();
      videoSource?.disconnect();
      videoGain?.disconnect();
      sfxGain?.disconnect();
      void ctx?.close();
      ctx = null;
      videoSource = null;
      videoGain = null;
      sfxGain = null;
      cache = null;
    },
  };
}
