/**
 * 文件功能：聊天室特效音效引擎（Web Audio API 实时合成）
 *
 * 所有音效通过 Web Audio API 实时合成，无需外部音频文件。
 *
 * 对外 API：
 *   EffectSounds.play(type)  播放指定特效的背景音效
 *   EffectSounds.stop()      停止并释放当前音效资源
 *   EffectSounds.ding()      播放简短叮咚通知音（大/小卡片弹出时使用）
 *
 * 支持的 type：
 *   lightning  雷鸣闪电（三层合成：放电啪声 + 低频轰鸣 + 极低频滚动余韵）
 *   fireworks  烟花（发射滑音 + 高频爆炸噪声）
 *   rain       下雨（双层带通白噪声，低音量，与视觉 8s 同步）
 *   snow       下雪（仅五声音阶铃音，无风声）
 *   sakura     樱花（轻柔风铃 + 微风扫过）
 *   meteors    流星（高速掠空呼啸）
 *   gold-rain  金币雨（金属叮当）
 *   hearts     爱心飘落（温暖双音）
 *   confetti   彩带庆典（礼炮碎响 + 清亮点缀）
 *   fireflies  萤火虫（稀疏微光铃音）
 */

const EffectSounds = (() => {
    /** @type {AudioContext|null} */
    let _ctx = null;
    /** @type {Function|null} 当前音效的停止函数 */
    let _stopFn = null;

    // ─── 工具方法 ──────────────────────────────────────────────────

    /**
     * 懒加载并返回 AudioContext。
     * 浏览器要求首次创建必须在用户手势后，聊天室点击任何按钮即可解锁。
     *
     * @returns {AudioContext}
     */
    function _getCtx() {
        if (!_ctx || _ctx.state === "closed") {
            _ctx = new (window.AudioContext || window.webkitAudioContext)();
        }
        if (_ctx.state === "suspended") {
            _ctx.resume();
        }
        return _ctx;
    }

    /**
     * 创建包含白噪声的 AudioBuffer。
     *
     * @param  {AudioContext}  ctx
     * @param  {number}        duration  时长（秒）
     * @returns {AudioBuffer}
     */
    function _makeNoise(ctx, duration) {
        const len = Math.ceil(ctx.sampleRate * duration);
        const buf = ctx.createBuffer(1, len, ctx.sampleRate);
        const data = buf.getChannelData(0);
        for (let i = 0; i < len; i++) {
            data[i] = Math.random() * 2 - 1;
        }
        return buf;
    }

    /**
     * 调度一个带包络的简单音符。
     *
     * @param  {AudioContext}  ctx
     * @param  {GainNode}      masterGain
     * @param  {{delay?:number, duration?:number, freq:number, endFreq?:number, volume?:number, type?:OscillatorType}} options
     */
    function _scheduleTone(ctx, masterGain, options) {
        const {
            delay = 0,
            duration = 0.8,
            freq,
            endFreq = freq,
            volume = 0.18,
            type = "sine",
        } = options;

        const t0 = ctx.currentTime + delay;
        const osc = ctx.createOscillator();
        const env = ctx.createGain();
        osc.type = type;
        osc.frequency.setValueAtTime(freq, t0);
        osc.frequency.exponentialRampToValueAtTime(
            Math.max(20, endFreq),
            t0 + duration,
        );
        env.gain.setValueAtTime(Math.max(0.0001, volume), t0);
        env.gain.exponentialRampToValueAtTime(0.001, t0 + duration);
        osc.connect(env);
        env.connect(masterGain);
        osc.start(t0);
        osc.stop(t0 + duration + 0.03);
    }

    /**
     * 调度一段带滤波扫频的噪声音色，用于呼啸、爆裂等环境声。
     *
     * @param  {AudioContext}  ctx
     * @param  {GainNode}      masterGain
     * @param  {{delay?:number, duration?:number, startFreq?:number, endFreq?:number, volume?:number, q?:number, filterType?:BiquadFilterType}} options
     */
    function _scheduleNoiseSweep(ctx, masterGain, options) {
        const {
            delay = 0,
            duration = 0.6,
            startFreq = 2200,
            endFreq = 500,
            volume = 0.16,
            q = 0.8,
            filterType = "bandpass",
        } = options;

        const t0 = ctx.currentTime + delay;
        const src = ctx.createBufferSource();
        src.buffer = _makeNoise(ctx, duration + 0.15);
        const filter = ctx.createBiquadFilter();
        filter.type = filterType;
        filter.Q.value = q;
        filter.frequency.setValueAtTime(startFreq, t0);
        filter.frequency.exponentialRampToValueAtTime(
            Math.max(60, endFreq),
            t0 + duration,
        );

        const env = ctx.createGain();
        env.gain.setValueAtTime(0.001, t0);
        env.gain.linearRampToValueAtTime(volume, t0 + duration * 0.18);
        env.gain.exponentialRampToValueAtTime(0.001, t0 + duration);

        src.connect(filter);
        filter.connect(env);
        env.connect(masterGain);
        src.start(t0);
        src.stop(t0 + duration + 0.08);
    }

    // ─── 雷电音效（三层合成，贴近真实雷声）────────────────────────

    /**
     * 单次真实雷声 = 放电啪声 + 低频轰鸣 + 极低频滚动余韵。
     *
     * 物理模型：
     *   crack  — 闪电通道瞬间放电产生的高频尖啪（30~50ms）
     *   boom   — 空气急速膨胀产生的低频冲击（40~120Hz，~150ms 达峰值，衰减 2s）
     *   rumble — 回声与地面反射产生的极低频滚动（20~60Hz，缓慢衰减 3.5s）
     *
     * @param  {AudioContext}  ctx
     * @param  {GainNode}      masterGain
     * @param  {number}        delay  延迟（秒）
     */
    function _thunderCrack(ctx, masterGain, delay) {
        const t0 = ctx.currentTime + delay;

        // ① 放电啪声（crack）：带通白噪声，极短 ~50ms
        const snap = ctx.createBufferSource();
        snap.buffer = _makeNoise(ctx, 0.12);
        const snapBpf = ctx.createBiquadFilter();
        snapBpf.type = "bandpass";
        snapBpf.frequency.value = 2800 + Math.random() * 800;
        snapBpf.Q.value = 0.7;
        const snapEnv = ctx.createGain();
        snapEnv.gain.setValueAtTime(0.55, t0);
        snapEnv.gain.exponentialRampToValueAtTime(0.001, t0 + 0.055);
        snap.connect(snapBpf);
        snapBpf.connect(snapEnv);
        snapEnv.connect(masterGain);
        snap.start(t0);
        snap.stop(t0 + 0.12);

        // ② 主轰鸣（boom）：低通白噪声，120→40Hz 扫频，快冲击慢衰减 ~2s
        const boom = ctx.createBufferSource();
        boom.buffer = _makeNoise(ctx, 2.2);
        const boomLpf = ctx.createBiquadFilter();
        boomLpf.type = "lowpass";
        boomLpf.frequency.setValueAtTime(130, t0 + 0.03);
        boomLpf.frequency.exponentialRampToValueAtTime(38, t0 + 2.0);
        const boomEnv = ctx.createGain();
        boomEnv.gain.setValueAtTime(0, t0 + 0.03);
        boomEnv.gain.linearRampToValueAtTime(1.0, t0 + 0.12); // 快速冲击
        boomEnv.gain.exponentialRampToValueAtTime(0.001, t0 + 2.1);
        boom.connect(boomLpf);
        boomLpf.connect(boomEnv);
        boomEnv.connect(masterGain);
        boom.start(t0 + 0.03);
        boom.stop(t0 + 2.2);

        // ③ 滚动余韵（rumble）：极低频，缓慢堆积后长衰减 ~3.5s
        const rumble = ctx.createBufferSource();
        rumble.buffer = _makeNoise(ctx, 3.8);
        const rumbleLpf = ctx.createBiquadFilter();
        rumbleLpf.type = "lowpass";
        rumbleLpf.frequency.setValueAtTime(55, t0);
        rumbleLpf.frequency.exponentialRampToValueAtTime(22, t0 + 3.5);
        const rumbleEnv = ctx.createGain();
        rumbleEnv.gain.setValueAtTime(0, t0 + 0.05);
        rumbleEnv.gain.linearRampToValueAtTime(0.65, t0 + 0.35); // 缓慢堆积
        rumbleEnv.gain.exponentialRampToValueAtTime(0.001, t0 + 3.6);
        rumble.connect(rumbleLpf);
        rumbleLpf.connect(rumbleEnv);
        rumbleEnv.connect(masterGain);
        rumble.start(t0 + 0.05);
        rumble.stop(t0 + 3.8);
    }

    /**
     * 启动雷电音效：与视觉特效同步触发 10 次雷声，总时长约 7 秒。
     *
     * @returns {Function}  停止函数
     */
    function _startLightning() {
        const ctx = _getCtx();
        const master = ctx.createGain();
        master.gain.value = 0.8;
        master.connect(ctx.destination);

        // 10 次闪电，间隔 400~800ms，雷声滞后闪电 50~300ms（光速>声速）
        let t = 0.3;
        for (let i = 0; i < 10; i++) {
            _thunderCrack(ctx, master, t + 0.06 + Math.random() * 0.25);
            t += 0.4 + Math.random() * 0.4;
        }

        // 视觉特效 ~7 秒，预留尾声淡出
        const timer = setTimeout(() => {
            master.gain.linearRampToValueAtTime(0, ctx.currentTime + 1.0);
            setTimeout(() => {
                try {
                    master.disconnect();
                } catch (_) {}
            }, 1100);
        }, 8000);

        return () => {
            clearTimeout(timer);
            try {
                master.gain.setValueAtTime(0, ctx.currentTime);
                master.disconnect();
            } catch (_) {}
        };
    }

    // ─── 烟花音效 ──────────────────────────────────────────────────

    /**
     * 播放单颗烟花：发射滑音 + 空中爆炸噪声。
     *
     * @param  {AudioContext}  ctx
     * @param  {GainNode}      masterGain
     * @param  {number}        delay  延迟（秒）
     */
    function _fireworkPop(ctx, masterGain, delay) {
        const t0 = ctx.currentTime + delay;

        // 发射音：200→700Hz 上升滑音
        const osc = ctx.createOscillator();
        osc.type = "sine";
        osc.frequency.setValueAtTime(200, t0);
        osc.frequency.exponentialRampToValueAtTime(700, t0 + 0.18);
        const launchEnv = ctx.createGain();
        launchEnv.gain.setValueAtTime(0.25, t0);
        launchEnv.gain.exponentialRampToValueAtTime(0.001, t0 + 0.22);
        osc.connect(launchEnv);
        launchEnv.connect(masterGain);
        osc.start(t0);
        osc.stop(t0 + 0.23);

        // 爆炸音：带通噪声
        const boom = ctx.createBufferSource();
        boom.buffer = _makeNoise(ctx, 0.9);
        const bpf = ctx.createBiquadFilter();
        bpf.type = "bandpass";
        bpf.frequency.value = 1000 + Math.random() * 1500;
        bpf.Q.value = 0.5;
        const boomEnv = ctx.createGain();
        const t1 = t0 + 0.18;
        boomEnv.gain.setValueAtTime(0, t1);
        boomEnv.gain.linearRampToValueAtTime(0.45, t1 + 0.02);
        boomEnv.gain.exponentialRampToValueAtTime(0.001, t1 + 0.75);
        boom.connect(bpf);
        bpf.connect(boomEnv);
        boomEnv.connect(masterGain);
        boom.start(t1);
        boom.stop(t1 + 0.9);
    }

    /**
     * 启动烟花音效。
     *
     * @returns {Function}  停止函数
     */
    function _startFireworks() {
        const ctx = _getCtx();
        const master = ctx.createGain();
        master.gain.value = 0.6;
        master.connect(ctx.destination);

        let t = 0.2;
        for (let i = 0; i < 9; i++) {
            _fireworkPop(ctx, master, t);
            t += 0.5 + Math.random() * 1.0;
        }

        const timer = setTimeout(
            () => {
                try {
                    master.disconnect();
                } catch (_) {}
            },
            (t + 2) * 1000,
        );

        return () => {
            clearTimeout(timer);
            try {
                master.disconnect();
            } catch (_) {}
        };
    }

    // ─── 下雨音效（低音量，与视觉 8000ms 对齐）─────────────────────

    /**
     * 启动下雨音效：双层带通白噪声，主音量 0.15，随视觉效果结束淡出。
     * 视觉雨效持续 8000ms，音效在相同时间开始淡出。
     *
     * @returns {Function}  停止函数
     */
    function _startRain() {
        const ctx = _getCtx();
        const master = ctx.createGain();
        master.gain.value = 0;
        master.connect(ctx.destination);

        // 主雨声：中频 1200Hz 沙沙感
        const src1 = ctx.createBufferSource();
        src1.buffer = _makeNoise(ctx, 3);
        src1.loop = true;
        const bpf1 = ctx.createBiquadFilter();
        bpf1.type = "bandpass";
        bpf1.frequency.value = 1200;
        bpf1.Q.value = 0.3;

        // 细密层：高频 3500Hz，增加雨点密度感
        const src2 = ctx.createBufferSource();
        src2.buffer = _makeNoise(ctx, 3);
        src2.loop = true;
        const bpf2 = ctx.createBiquadFilter();
        bpf2.type = "bandpass";
        bpf2.frequency.value = 3500;
        bpf2.Q.value = 1;
        const g2 = ctx.createGain();
        g2.gain.value = 0.35;

        src1.connect(bpf1);
        bpf1.connect(master);
        src2.connect(bpf2);
        bpf2.connect(g2);
        g2.connect(master);
        src1.start();
        src2.start();

        // 淡入 1.5s → 最高音量 0.15（比之前降低约 60%）
        master.gain.linearRampToValueAtTime(0.15, ctx.currentTime + 1.5);

        // 与视觉雨效对齐：8000ms 后开始 2s 淡出
        const endTimer = setTimeout(() => {
            master.gain.linearRampToValueAtTime(0, ctx.currentTime + 2.0);
            setTimeout(() => {
                try {
                    src1.stop();
                    src2.stop();
                    master.disconnect();
                } catch (_) {}
            }, 2200);
        }, 8000);

        return () => {
            clearTimeout(endTimer);
            master.gain.setValueAtTime(0, ctx.currentTime);
            setTimeout(() => {
                try {
                    src1.stop();
                    src2.stop();
                    master.disconnect();
                } catch (_) {}
            }, 100);
        };
    }

    // ─── 下雪音效（仅五声音阶铃音，无风声）─────────────────────────

    /**
     * 播放单次铃声（五声音阶 C/E/G/C，含泛音模拟铃铛共鸣）。
     *
     * @param  {AudioContext}  ctx
     * @param  {GainNode}      masterGain
     * @param  {number}        delay  延迟（秒）
     */
    function _snowBell(ctx, masterGain, delay) {
        // 五声音阶：C5 E5 G5 C6
        const freqs = [523.25, 659.25, 783.99, 1046.5];
        const freq = freqs[Math.floor(Math.random() * freqs.length)];
        const t0 = ctx.currentTime + delay;

        // 基音
        const osc1 = ctx.createOscillator();
        osc1.type = "sine";
        osc1.frequency.value = freq;

        // 第二泛音（5倍频，音量很低，增加金属铃铛感）
        const osc2 = ctx.createOscillator();
        osc2.type = "sine";
        osc2.frequency.value = freq * 2.76; // 铃铛典型泛音比
        const g2 = ctx.createGain();
        g2.gain.value = 0.12;

        // 共用包络：快冲击，缓慢衰减 2s
        const env = ctx.createGain();
        env.gain.setValueAtTime(0.22, t0);
        env.gain.exponentialRampToValueAtTime(0.001, t0 + 2.0);

        osc1.connect(env);
        osc2.connect(g2);
        g2.connect(env);
        env.connect(masterGain);

        osc1.start(t0);
        osc1.stop(t0 + 2.1);
        osc2.start(t0);
        osc2.stop(t0 + 2.1);
    }

    /**
     * 启动下雪音效：在 10 秒内随机播放 8 次铃音，无背景风声。
     * 与视觉雪效持续时间（10000ms）对齐。
     *
     * @returns {Function}  停止函数
     */
    function _startSnow() {
        const ctx = _getCtx();
        const master = ctx.createGain();
        master.gain.value = 0.9;
        master.connect(ctx.destination);

        // 随机分布 8 次铃声，间隔在 0.5~9.5s 之间
        const bellTimers = [];
        for (let i = 0; i < 8; i++) {
            const d = 0.3 + Math.random() * 9.0;
            bellTimers.push(
                setTimeout(() => _snowBell(ctx, master, 0), d * 1000),
            );
        }

        const endTimer = setTimeout(() => {
            master.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.5);
            setTimeout(() => {
                try {
                    master.disconnect();
                } catch (_) {}
            }, 600);
        }, 10800);

        return () => {
            clearTimeout(endTimer);
            bellTimers.forEach((t) => clearTimeout(t));
            try {
                master.gain.setValueAtTime(0, ctx.currentTime);
                master.disconnect();
            } catch (_) {}
        };
    }

    // ─── 樱花 / 流星 / 金币雨 / 爱心 / 彩带 / 萤火虫音效 ────────────

    /**
     * 启动樱花音效：低音量风铃加轻微风声。
     *
     * @returns {Function} 停止函数
     */
    function _startSakura() {
        const ctx = _getCtx();
        const master = ctx.createGain();
        master.gain.value = 0.42;
        master.connect(ctx.destination);

        const notes = [523.25, 659.25, 783.99, 880];
        const plan = [0.35, 1.2, 2.1, 3.6, 5.1, 6.8, 8.2];
        plan.forEach((delay, index) => {
            _scheduleTone(ctx, master, {
                delay,
                duration: 1.8,
                freq: notes[index % notes.length],
                endFreq: notes[index % notes.length] * 0.98,
                volume: 0.11,
                type: "sine",
            });
        });

        _scheduleNoiseSweep(ctx, master, {
            delay: 0.2,
            duration: 3.2,
            startFreq: 1400,
            endFreq: 500,
            volume: 0.04,
            q: 0.35,
        });
        _scheduleNoiseSweep(ctx, master, {
            delay: 4.8,
            duration: 2.8,
            startFreq: 1200,
            endFreq: 420,
            volume: 0.035,
            q: 0.3,
        });

        const endTimer = setTimeout(() => {
            master.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.8);
            setTimeout(() => {
                try {
                    master.disconnect();
                } catch (_) {}
            }, 900);
        }, 9800);

        return () => {
            clearTimeout(endTimer);
            try {
                master.gain.setValueAtTime(0, ctx.currentTime);
                master.disconnect();
            } catch (_) {}
        };
    }

    /**
     * 启动流星音效：多段高速呼啸掠空。
     *
     * @returns {Function} 停止函数
     */
    function _startMeteors() {
        const ctx = _getCtx();
        const master = ctx.createGain();
        master.gain.value = 0.55;
        master.connect(ctx.destination);

        [0.4, 1.5, 2.8, 4.2, 5.7, 7.1].forEach((delay) => {
            _scheduleNoiseSweep(ctx, master, {
                delay,
                duration: 0.85,
                startFreq: 3600,
                endFreq: 320,
                volume: 0.14,
                q: 1.1,
            });
            _scheduleTone(ctx, master, {
                delay: delay + 0.08,
                duration: 0.55,
                freq: 1100,
                endFreq: 420,
                volume: 0.06,
                type: "triangle",
            });
        });

        const endTimer = setTimeout(() => {
            try {
                master.disconnect();
            } catch (_) {}
        }, 9300);

        return () => {
            clearTimeout(endTimer);
            try {
                master.disconnect();
            } catch (_) {}
        };
    }

    /**
     * 启动金币雨音效：连续金属叮当声。
     *
     * @returns {Function} 停止函数
     */
    function _startGoldRain() {
        const ctx = _getCtx();
        const master = ctx.createGain();
        master.gain.value = 0.62;
        master.connect(ctx.destination);

        const notes = [880, 987.77, 1174.66, 1318.51];
        for (let i = 0; i < 16; i++) {
            const delay = 0.25 + i * 0.42;
            const freq = notes[i % notes.length];
            _scheduleTone(ctx, master, {
                delay,
                duration: 0.5,
                freq,
                endFreq: freq * 0.92,
                volume: 0.16,
                type: "triangle",
            });
            _scheduleTone(ctx, master, {
                delay: delay + 0.02,
                duration: 0.38,
                freq: freq * 2.1,
                endFreq: freq * 1.9,
                volume: 0.06,
                type: "sine",
            });
        }

        const endTimer = setTimeout(() => {
            master.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.6);
            setTimeout(() => {
                try {
                    master.disconnect();
                } catch (_) {}
            }, 700);
        }, 7600);

        return () => {
            clearTimeout(endTimer);
            try {
                master.gain.setValueAtTime(0, ctx.currentTime);
                master.disconnect();
            } catch (_) {}
        };
    }

    /**
     * 启动爱心飘落音效：柔和双音和声。
     *
     * @returns {Function} 停止函数
     */
    function _startHearts() {
        const ctx = _getCtx();
        const master = ctx.createGain();
        master.gain.value = 0.5;
        master.connect(ctx.destination);

        [0.35, 1.5, 2.9, 4.2, 5.8, 7.2].forEach((delay) => {
            _scheduleTone(ctx, master, {
                delay,
                duration: 0.9,
                freq: 523.25,
                endFreq: 493.88,
                volume: 0.12,
                type: "triangle",
            });
            _scheduleTone(ctx, master, {
                delay: delay + 0.12,
                duration: 1.05,
                freq: 659.25,
                endFreq: 622.25,
                volume: 0.1,
                type: "sine",
            });
        });

        const endTimer = setTimeout(() => {
            master.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.8);
            setTimeout(() => {
                try {
                    master.disconnect();
                } catch (_) {}
            }, 900);
        }, 9000);

        return () => {
            clearTimeout(endTimer);
            try {
                master.gain.setValueAtTime(0, ctx.currentTime);
                master.disconnect();
            } catch (_) {}
        };
    }

    /**
     * 启动彩带庆典音效：礼炮碎响与亮点装饰音。
     *
     * @returns {Function} 停止函数
     */
    function _startConfetti() {
        const ctx = _getCtx();
        const master = ctx.createGain();
        master.gain.value = 0.58;
        master.connect(ctx.destination);

        [0.2, 1.4, 2.8, 4.1, 5.6].forEach((delay) => {
            _scheduleNoiseSweep(ctx, master, {
                delay,
                duration: 0.38,
                startFreq: 1800,
                endFreq: 700,
                volume: 0.2,
                q: 0.7,
            });
            _scheduleTone(ctx, master, {
                delay: delay + 0.04,
                duration: 0.42,
                freq: 1046.5,
                endFreq: 880,
                volume: 0.08,
                type: "square",
            });
        });

        const endTimer = setTimeout(() => {
            try {
                master.disconnect();
            } catch (_) {}
        }, 7200);

        return () => {
            clearTimeout(endTimer);
            try {
                master.disconnect();
            } catch (_) {}
        };
    }

    /**
     * 启动萤火虫音效：低音量稀疏闪烁音。
     *
     * @returns {Function} 停止函数
     */
    function _startFireflies() {
        const ctx = _getCtx();
        const master = ctx.createGain();
        master.gain.value = 0.28;
        master.connect(ctx.destination);

        [0.6, 1.8, 2.7, 3.9, 5.2, 6.4, 7.6, 8.8].forEach((delay, index) => {
            _scheduleTone(ctx, master, {
                delay,
                duration: 1.4,
                freq: [659.25, 783.99, 987.77][index % 3],
                endFreq: [659.25, 783.99, 987.77][index % 3] * 0.99,
                volume: 0.06,
                type: "sine",
            });
        });

        const endTimer = setTimeout(() => {
            master.gain.linearRampToValueAtTime(0, ctx.currentTime + 1.0);
            setTimeout(() => {
                try {
                    master.disconnect();
                } catch (_) {}
            }, 1100);
        }, 10200);

        return () => {
            clearTimeout(endTimer);
            try {
                master.gain.setValueAtTime(0, ctx.currentTime);
                master.disconnect();
            } catch (_) {}
        };
    }

    // ─── 公开 API ──────────────────────────────────────────────────

    /**
     * 播放指定特效对应的音效（自动停止上一个）。
     * 静音状态下直接跳过，不做任何音频操作。
     * 当 AudioContext 处于 suspended 状态时，先 resume() 再播放，
     * 解决页面无用户手势时的自动静音问题（如管理员进房自动烟花）。
     *
     * @param  {string}  type  'lightning' | 'fireworks' | 'rain' | 'snow' | 'sakura' | 'meteors' | 'gold-rain' | 'hearts' | 'confetti' | 'fireflies'
     */
    function play(type) {
        // 用户开启禁音则跳过
        if (localStorage.getItem("chat_sound_muted") === "1") return;
        stop();

        try {
            const ctx = _getCtx();

            const _doPlay = () => {
                try {
                    switch (type) {
                        case "lightning":
                            _stopFn = _startLightning();
                            break;
                        case "fireworks":
                            _stopFn = _startFireworks();
                            break;
                        case "rain":
                            _stopFn = _startRain();
                            break;
                        case "snow":
                            _stopFn = _startSnow();
                            break;
                        case "sakura":
                            _stopFn = _startSakura();
                            break;
                        case "meteors":
                            _stopFn = _startMeteors();
                            break;
                        case "gold-rain":
                            _stopFn = _startGoldRain();
                            break;
                        case "hearts":
                            _stopFn = _startHearts();
                            break;
                        case "confetti":
                            _stopFn = _startConfetti();
                            break;
                        case "fireflies":
                            _stopFn = _startFireflies();
                            break;
                        default:
                            break;
                    }
                } catch (e) {
                    console.warn("[EffectSounds] 音效内部错误:", e);
                }
            };

            if (ctx.state === "suspended") {
                // AudioContext 尚未被用户手势激活，先 resume 再播放
                ctx.resume()
                    .then(_doPlay)
                    .catch(() => {
                        // 浏览器拒绝 resume（无用户手势），静默处理
                    });
            } else {
                _doPlay();
            }
        } catch (e) {
            console.warn(
                "[EffectSounds] 音效播放失败（可能未能解锁 AudioContext）:",
                e,
            );
        }
    }

    // ─── 叮咚通知音（大卡片 / 小卡片弹出时调用）─────────────────

    /**
     * 播放简短的叮咚两音通知音效。
     *
     * 音型：A5（880Hz）→ 间隔 110ms → E5（659Hz），均为正弦波，
     * 快速冲击 + 铃铛式缓慢衰减，总时长约 0.5 秒。
     * 禁音状态下自动跳过。
     */
    function ding() {
        if (localStorage.getItem("chat_sound_muted") === "1") return;
        try {
            const ctx = _getCtx();

            const _doDing = () => {
                try {
                    const master = ctx.createGain();
                    master.gain.value = 0.45;
                    master.connect(ctx.destination);

                    function _tone(freq, t0, decay) {
                        const osc = ctx.createOscillator();
                        osc.type = "sine";
                        osc.frequency.value = freq;
                        const osc2 = ctx.createOscillator();
                        osc2.type = "sine";
                        osc2.frequency.value = freq * 2.76;
                        const g2 = ctx.createGain();
                        g2.gain.value = 0.1;
                        const env = ctx.createGain();
                        env.gain.setValueAtTime(1.0, t0);
                        env.gain.exponentialRampToValueAtTime(
                            0.001,
                            t0 + decay,
                        );
                        osc.connect(env);
                        osc2.connect(g2);
                        g2.connect(env);
                        env.connect(master);
                        osc.start(t0);
                        osc.stop(t0 + decay + 0.05);
                        osc2.start(t0);
                        osc2.stop(t0 + decay + 0.05);
                    }

                    const now = ctx.currentTime;
                    _tone(880, now, 0.35); // 叮：A5
                    _tone(659, now + 0.11, 0.4); // 咚：E5

                    setTimeout(() => {
                        try {
                            master.disconnect();
                        } catch (_) {}
                    }, 700);
                } catch (e) {
                    console.warn("[EffectSounds.ding] 通知音内部错误:", e);
                }
            };

            if (ctx.state === "suspended") {
                ctx.resume()
                    .then(_doDing)
                    .catch(() => {});
            } else {
                _doDing();
            }
        } catch (e) {
            console.warn("[EffectSounds.ding] 通知音播放失败:", e);
        }
    }

    /**
     * 停止当前音效并释放资源。
     */
    function stop() {
        if (_stopFn) {
            try {
                _stopFn();
            } catch (_) {}
            _stopFn = null;
        }
    }

    return { play, stop, ding };
})();

window.EffectSounds = EffectSounds;
// 将叮咚通知音暴露为独立全局变量，供 toast/banner 等组件直接调用
window.chatSound = { ding: () => EffectSounds.ding() };
