/**
 * WebAudio с тремя шинами (master/music/sfx): громкости по шинам,
 * кроссфейд музыки, кэш сэмплов. Игры подключают Settings.onChange
 * и вызывают setBusVolume.
 */

export interface AudioBuses {
    master: GainNode;
    music: GainNode;
    sfx: GainNode;
}

export interface MusicHandle {
    stop(fadeSeconds?: number): void;
}

export class AudioManager {
    private ctx: AudioContext | null = null;
    private buffers = new Map<string, AudioBuffer>();
    private urls = new Map<string, string>();
    private buses: AudioBuses | null = null;
    private currentMusic: { source: AudioBufferSourceNode; gain: GainNode } | null = null;

    constructor(private resolveUrl: (key: string) => string) {}

    /** Расблокировать аудио — вызвать из обработчика пользовательского ввода. */
    async unlock(): Promise<void> {
        if (!this.ctx) {
            this.ctx = new AudioContext();
            const master = this.ctx.createGain();
            const music = this.ctx.createGain();
            const sfx = this.ctx.createGain();
            music.connect(master);
            sfx.connect(master);
            master.connect(this.ctx.destination);
            this.buses = { master, music, sfx };
        }
        if (this.ctx.state === 'suspended') {
            await this.ctx.resume();
        }
    }

    /** Громкость шины 0..1 (без unlock просто запоминается на будущее). */
    setBusVolume(bus: 'master' | 'music' | 'sfx', volume: number): void {
        if (this.buses) {
            this.buses[bus].gain.value = Math.max(0, Math.min(1, volume));
        }
    }

    async load(key: string): Promise<void> {
        this.urls.set(key, this.resolveUrl(key));
    }

    /**
     * Декодировать заранее (после unlock, например в сцене загрузки),
     * чтобы первый play не тратил время на fetch+decode.
     */
    async preload(key: string): Promise<void> {
        const ctx = await this.ensureContext();
        if (!ctx) return;
        await this.decode(key);
    }

    /**
     * Проиграть sfx (если звук не загружен — тихо ничего не делает).
     * volume — относительная громкость в шине sfx.
     */
    async play(key: string, volume = 1): Promise<void> {
        const ctx = await this.ensureContext();
        if (!ctx) return;
        const buf = await this.decode(key);
        if (!buf) return;

        const source = ctx.createBufferSource();
        source.buffer = buf;
        const gain = ctx.createGain();
        gain.gain.value = volume;
        source.connect(gain).connect(this.buses!.sfx);
        source.start();
    }

    /**
     * Играть музыку в цикле с кроссфейдом: текущая трек затухает,
     * новый нарастает за fadeSeconds.
     */
    async playMusic(key: string, { loop = true, fade = 1, volume = 1 }: { loop?: boolean; fade?: number; volume?: number } = {}): Promise<MusicHandle | null> {
        const ctx = await this.ensureContext();
        if (!ctx) return null;
        const buf = await this.decode(key);
        if (!buf) return null;

        // Заглушить текущий трек
        this.stopMusic(fade);

        const source = ctx.createBufferSource();
        source.buffer = buf;
        source.loop = loop;
        const gain = ctx.createGain();
        gain.gain.setValueAtTime(0.0001, ctx.currentTime);
        gain.gain.linearRampToValueAtTime(volume, ctx.currentTime + fade);
        source.connect(gain).connect(this.buses!.music);
        source.start();

        this.currentMusic = { source, gain };
        return {
            stop: (fadeSeconds = fade) => this.stopMusic(fadeSeconds)
        };
    }

    /** Остановить музыку (с затуханием). */
    stopMusic(fadeSeconds = 0.5): void {
        const current = this.currentMusic;
        if (!current || !this.ctx) return;
        this.currentMusic = null;
        const { source, gain } = current;
        const t = this.ctx!.currentTime;
        gain.gain.cancelScheduledValues(t);
        gain.gain.setValueAtTime(gain.gain.value, t);
        gain.gain.linearRampToValueAtTime(0.0001, t + fadeSeconds);
        window.setTimeout(() => {
            try {
                source.stop();
            } catch {
                // уже остановлен
            }
        }, fadeSeconds * 1000 + 50);
    }

    private async ensureContext(): Promise<AudioContext | null> {
        await this.unlock();
        return this.ctx;
    }

    private async decode(key: string): Promise<AudioBuffer | null> {
        if (!this.ctx) return null;
        let buf = this.buffers.get(key);
        const url = this.urls.get(key);
        if (!buf && url) {
            try {
                const res = await fetch(url);
                buf = await this.ctx.decodeAudioData(await res.arrayBuffer());
                this.buffers.set(key, buf);
            } catch (err) {
                console.warn(`[audio] не удалось декодировать ${key}:`, err);
                return null;
            }
        }
        return buf ?? null;
    }
}