// tts-panel.js
/**
 * TTS 播放器面板 - 支持楼层按钮和悬浮按钮双模式
 */

import { registerToToolbar, removeFromToolbar } from '../../widgets/message-toolbar.js';

// ═══════════════════════════════════════════════════════════════════════════
// 常量
// ═══════════════════════════════════════════════════════════════════════════

const FLOAT_POS_KEY = 'xb_tts_float_pos';
const INITIAL_RENDER_LIMIT = 1;

// ═══════════════════════════════════════════════════════════════════════════
// 状态
// ═══════════════════════════════════════════════════════════════════════════

// 楼层按钮
const panelMap = new Map();
const pendingCallbacks = new Map();
let floorObserver = null;

// 悬浮按钮
let floatingEl = null;
let floatingDragState = null;
let $floatingCache = {};

// 通用
let stylesInjected = false;

// 配置接口
let getConfigFn = null;
let saveConfigFn = null;
let openSettingsFn = null;
let clearQueueFn = null;
let getLastAIMessageIdFn = null;
let speakMessageFn = null;

export function setPanelConfigHandlers(handlers) {
    getConfigFn = handlers.getConfig;
    saveConfigFn = handlers.saveConfig;
    openSettingsFn = handlers.openSettings;
    clearQueueFn = handlers.clearQueue;
    getLastAIMessageIdFn = handlers.getLastAIMessageId;
    speakMessageFn = handlers.speakMessage;
}

export function clearPanelConfigHandlers() {
    getConfigFn = null;
    saveConfigFn = null;
    openSettingsFn = null;
    clearQueueFn = null;
    getLastAIMessageIdFn = null;
    speakMessageFn = null;
}

// ═══════════════════════════════════════════════════════════════════════════
// 样式
// ═══════════════════════════════════════════════════════════════════════════

const STYLES = `
.xb-tts-panel {
    --h: 34px;
    --bg: rgba(0, 0, 0, 0.55);
    --bg-solid: rgba(24, 24, 28, 0.98);
    --bg-hover: rgba(0, 0, 0, 0.7);
    --border: rgba(255, 255, 255, 0.08);
    --border-hover: rgba(255, 255, 255, 0.2);
    --text: rgba(255, 255, 255, 0.85);
    --text-sub: rgba(255, 255, 255, 0.45);
    --text-dim: rgba(255, 255, 255, 0.25);
    --success: rgba(62, 207, 142, 0.9);
    --error: rgba(239, 68, 68, 0.8);
    position: relative;
    display: inline-flex;
    flex-direction: column;
    z-index: 10;
    user-select: none;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

.xb-tts-capsule {
    display: flex;
    align-items: center;
    height: var(--h);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 17px;
    padding: 0 3px;
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
    width: fit-content;
    gap: 1px;
}

.xb-tts-panel:hover .xb-tts-capsule {
    background: var(--bg-hover);
    border-color: var(--border-hover);
}

.xb-tts-panel[data-auto="true"] .xb-tts-capsule {
    border-color: rgba(62, 207, 142, 0.25);
}
.xb-tts-panel[data-auto="true"]:hover .xb-tts-capsule {
    border-color: rgba(62, 207, 142, 0.4);
}

.xb-tts-panel[data-status="playing"] .xb-tts-capsule {
    border-color: rgba(255, 255, 255, 0.25);
}
.xb-tts-panel[data-status="error"] .xb-tts-capsule {
    border-color: var(--error);
}

.xb-tts-btn {
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: transparent;
    color: var(--text);
    cursor: pointer;
    border-radius: 50%;
    font-size: 11px;
    transition: all 0.25s ease;
    flex-shrink: 0;
    position: relative;
}

.xb-tts-btn:hover {
    background: rgba(255, 255, 255, 0.12);
}

.xb-tts-btn:active {
    transform: scale(0.92);
}

.xb-tts-auto-dot {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 6px;
    height: 6px;
    background: var(--success);
    border-radius: 50%;
    box-shadow: 0 0 6px rgba(62, 207, 142, 0.6);
    opacity: 0;
    transform: scale(0);
    transition: all 0.25s ease;
}
.xb-tts-panel[data-auto="true"] .xb-tts-auto-dot {
    opacity: 1;
    transform: scale(1);
}

.xb-tts-btn.stop-btn {
    color: var(--text-sub);
    font-size: 8px;
}
.xb-tts-btn.stop-btn:hover {
    color: var(--error);
    background: rgba(239, 68, 68, 0.1);
}

.xb-tts-btn.expand-btn {
    width: 24px;
    height: 24px;
    font-size: 8px;
    color: var(--text-dim);
    opacity: 0.6;
    transition: opacity 0.25s, transform 0.25s;
}
.xb-tts-panel:hover .xb-tts-btn.expand-btn {
    opacity: 1;
}
.xb-tts-panel.expanded .xb-tts-btn.expand-btn {
    transform: rotate(180deg);
}

.xb-tts-sep {
    width: 1px;
    height: 12px;
    background: var(--border);
    margin: 0 2px;
    flex-shrink: 0;
}

.xb-tts-info {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 0 6px;
    min-width: 50px;
}

.xb-tts-status {
    font-size: 11px;
    color: var(--text-sub);
    white-space: nowrap;
    transition: color 0.25s;
}
.xb-tts-panel[data-status="playing"] .xb-tts-status {
    color: var(--text);
}
.xb-tts-panel[data-status="error"] .xb-tts-status {
    color: var(--error);
}

.xb-tts-badge {
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.1);
    color: var(--text);
    padding: 2px 6px;
    border-radius: 8px;
    font-size: 10px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}
.xb-tts-panel[data-has-queue="true"] .xb-tts-badge {
    display: flex;
}

.xb-tts-wave {
    display: none;
    align-items: center;
    gap: 2px;
    height: 14px;
    padding: 0 4px;
}

.xb-tts-panel[data-status="playing"] .xb-tts-wave {
    display: flex;
}
.xb-tts-panel[data-status="playing"] .xb-tts-status {
    display: none;
}

.xb-tts-bar {
    width: 2px;
    background: var(--text);
    border-radius: 1px;
    animation: xb-tts-wave 1.6s infinite ease-in-out;
    opacity: 0.7;
}
.xb-tts-bar:nth-child(1) { height: 4px; animation-delay: 0.0s; }
.xb-tts-bar:nth-child(2) { height: 10px; animation-delay: 0.2s; }
.xb-tts-bar:nth-child(3) { height: 6px; animation-delay: 0.4s; }
.xb-tts-bar:nth-child(4) { height: 8px; animation-delay: 0.6s; }

@keyframes xb-tts-wave {
    0%, 100% { transform: scaleY(0.4); opacity: 0.4; }
    50% { transform: scaleY(1); opacity: 0.85; }
}

.xb-tts-loading {
    display: none;
    width: 12px;
    height: 12px;
    border: 1.5px solid rgba(255, 255, 255, 0.15);
    border-top-color: var(--text);
    border-radius: 50%;
    animation: xb-tts-spin 1s linear infinite;
    margin: 0 4px;
}

.xb-tts-panel[data-status="sending"] .xb-tts-loading,
.xb-tts-panel[data-status="queued"] .xb-tts-loading {
    display: block;
}
.xb-tts-panel[data-status="sending"] .play-btn,
.xb-tts-panel[data-status="queued"] .play-btn {
    display: none;
}

@keyframes xb-tts-spin {
    to { transform: rotate(360deg); }
}

.xb-tts-progress {
    position: absolute;
    bottom: 0;
    left: 8px;
    right: 8px;
    height: 2px;
    background: rgba(255, 255, 255, 0.08);
    border-radius: 1px;
    overflow: hidden;
    opacity: 0;
    transition: opacity 0.3s;
}

.xb-tts-panel[data-status="playing"] .xb-tts-progress,
.xb-tts-panel[data-has-queue="true"] .xb-tts-progress {
    opacity: 1;
}

.xb-tts-progress-inner {
    height: 100%;
    background: rgba(255, 255, 255, 0.6);
    width: 0%;
    transition: width 0.4s ease-out;
    border-radius: 1px;
}

.xb-tts-menu {
    position: absolute;
    top: calc(100% + 8px);
    left: 0;
    background: rgba(18, 18, 22, 0.96);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 10px;
    min-width: 220px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-6px) scale(0.96);
    transform-origin: top left;
    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    z-index: 100;
}

.xb-tts-panel.expanded .xb-tts-menu {
    opacity: 1;
    visibility: visible;
    transform: translateY(0) scale(1);
}

.xb-tts-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 2px;
}

.xb-tts-label {
    font-size: 11px;
    color: var(--text-sub);
    width: 32px;
    flex-shrink: 0;
}

.xb-tts-select {
    flex: 1;
    background: rgba(255, 255, 255, 0.06);
    border: 1px solid var(--border);
    color: var(--text);
    font-size: 11px;
    border-radius: 6px;
    padding: 6px 8px;
    outline: none;
    cursor: pointer;
    transition: border-color 0.2s;
}
.xb-tts-select:hover { border-color: rgba(255, 255, 255, 0.2); }
.xb-tts-select:focus { border-color: rgba(255, 255, 255, 0.3); }

.xb-tts-slider {
    flex: 1;
    height: 4px;
    accent-color: #fff;
    cursor: pointer;
}

.xb-tts-val {
    font-size: 11px;
    color: var(--text);
    width: 32px;
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.xb-tts-tools {
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: 6px;
}

.xb-tts-usage {
    font-size: 10px;
    color: var(--text-dim);
    flex-shrink: 0;
    min-width: 32px;
}

.xb-tts-auto-toggle {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 6px 10px;
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid var(--border);
    border-radius: 6px;
    cursor: pointer;
    transition: all 0.2s ease;
}
.xb-tts-auto-toggle:hover {
    background: rgba(255, 255, 255, 0.06);
    border-color: rgba(255, 255, 255, 0.15);
}
.xb-tts-auto-toggle.on {
    background: rgba(62, 207, 142, 0.08);
    border-color: rgba(62, 207, 142, 0.25);
}

.xb-tts-auto-indicator {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.2);
    transition: all 0.25s ease;
    flex-shrink: 0;
}
.xb-tts-auto-toggle.on .xb-tts-auto-indicator {
    background: var(--success);
    box-shadow: 0 0 6px rgba(62, 207, 142, 0.5);
}

.xb-tts-auto-text {
    font-size: 11px;
    color: var(--text-sub);
    transition: color 0.2s;
}
.xb-tts-auto-toggle:hover .xb-tts-auto-text { color: var(--text); }
.xb-tts-auto-toggle.on .xb-tts-auto-text { color: rgba(62, 207, 142, 0.9); }

.xb-tts-icon-btn {
    color: var(--text-sub);
    cursor: pointer;
    font-size: 13px;
    padding: 4px 6px;
    border-radius: 4px;
    transition: all 0.2s;
    flex-shrink: 0;
}
.xb-tts-icon-btn:hover {
    color: var(--text);
    background: rgba(255, 255, 255, 0.08);
}

.xb-tts-floating-global {
    position: fixed;
    z-index: 10000;
    user-select: none;
    will-change: transform;
}

.xb-tts-floating-global .xb-tts-capsule {
    background: var(--bg-solid);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
    touch-action: none;
    cursor: grab;
}

.xb-tts-floating-global .xb-tts-capsule:active { cursor: grabbing; }

.xb-tts-floating-global .xb-tts-menu {
    top: auto;
    bottom: calc(100% + 10px);
    transform: translateY(6px) scale(0.98);
    transform-origin: bottom left;
}

.xb-tts-floating-global.expanded .xb-tts-menu {
    transform: translateY(0) scale(1);
}

.xb-tts-floating-global .xb-tts-btn.expand-btn { transform: rotate(180deg); }
.xb-tts-floating-global.expanded .xb-tts-btn.expand-btn { transform: rotate(0deg); }

.xb-tts-tag {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    color: rgba(255, 255, 255, 0.25);
    font-size: 11px;
    font-style: italic;
    vertical-align: baseline;
    user-select: none;
    transition: color 0.3s ease;
}
.xb-tts-tag:hover { color: rgba(255, 255, 255, 0.45); }
.xb-tts-tag-icon { font-style: normal; font-size: 10px; opacity: 0.7; }
.xb-tts-tag-dot { opacity: 0.4; }
.xb-tts-tag[data-has-params="true"] { color: rgba(255, 255, 255, 0.3); }
`;

function injectStyles() {
    if (stylesInjected) return;
    stylesInjected = true;
    const el = document.createElement('style');
    el.id = 'xb-tts-panel-styles';
    el.textContent = STYLES;
    document.head.appendChild(el);
}

// ═══════════════════════════════════════════════════════════════════════════
// 通用工具
// ═══════════════════════════════════════════════════════════════════════════

function fillVoiceSelect(selectEl) {
    if (!selectEl) return;
    const config = getConfigFn?.();
    const mySpeakers = config?.volc?.mySpeakers || [];
    const currentSpeaker = config?.volc?.defaultSpeaker || '';

    selectEl.replaceChildren();

    if (mySpeakers.length === 0) {
        const opt = document.createElement('option');
        opt.value = '';
        opt.textContent = '暂无音色';
        opt.disabled = true;
        selectEl.appendChild(opt);
        return;
    }

    mySpeakers.forEach(s => {
        const opt = document.createElement('option');
        opt.value = s.value;
        opt.textContent = s.name || s.value;
        if (s.value === currentSpeaker) opt.selected = true;
        selectEl.appendChild(opt);
    });
}

function safeGetLastAIMessageId() {
    const id = getLastAIMessageIdFn?.();
    return typeof id === 'number' && id >= 0 ? id : -1;
}

function syncSpeedUI($cache) {
    const config = getConfigFn?.();
    const currentSpeed = config?.volc?.speechRate || 1.0;
    if ($cache.speedSlider) $cache.speedSlider.value = currentSpeed;
    if ($cache.speedVal) $cache.speedVal.textContent = currentSpeed.toFixed(1) + 'x';
}

// ═══════════════════════════════════════════════════════════════════════════
// DOM 构建（符合 ESLint 规范，不使用 innerHTML）
// ═══════════════════════════════════════════════════════════════════════════

function createWaveElement() {
    const wave = document.createElement('div');
    wave.className = 'xb-tts-wave';
    for (let i = 0; i < 4; i++) {
        const bar = document.createElement('div');
        bar.className = 'xb-tts-bar';
        wave.appendChild(bar);
    }
    return wave;
}

function createMenuElement(speed, isAuto) {
    const menu = document.createElement('div');
    menu.className = 'xb-tts-menu';

    // 音色行
    const voiceRow = document.createElement('div');
    voiceRow.className = 'xb-tts-row';
    const voiceLabel = document.createElement('span');
    voiceLabel.className = 'xb-tts-label';
    voiceLabel.textContent = '音色';
    voiceRow.appendChild(voiceLabel);
    const voiceSelect = document.createElement('select');
    voiceSelect.className = 'xb-tts-select voice-select';
    voiceRow.appendChild(voiceSelect);
    menu.appendChild(voiceRow);

    // 语速行
    const speedRow = document.createElement('div');
    speedRow.className = 'xb-tts-row';
    const speedLabel = document.createElement('span');
    speedLabel.className = 'xb-tts-label';
    speedLabel.textContent = '语速';
    speedRow.appendChild(speedLabel);
    const speedSlider = document.createElement('input');
    speedSlider.type = 'range';
    speedSlider.className = 'xb-tts-slider speed-slider';
    speedSlider.min = '0.5';
    speedSlider.max = '2.0';
    speedSlider.step = '0.1';
    speedSlider.value = String(speed);
    speedRow.appendChild(speedSlider);
    const speedVal = document.createElement('span');
    speedVal.className = 'xb-tts-val speed-val';
    speedVal.textContent = speed.toFixed(1) + 'x';
    speedRow.appendChild(speedVal);
    menu.appendChild(speedRow);

    // 工具栏
    const tools = document.createElement('div');
    tools.className = 'xb-tts-tools';

    const usage = document.createElement('span');
    usage.className = 'xb-tts-usage';
    usage.textContent = '-字';
    tools.appendChild(usage);

    const autoToggle = document.createElement('div');
    autoToggle.className = 'xb-tts-auto-toggle' + (isAuto ? ' on' : '');
    autoToggle.title = 'AI回复后自动朗读';
    const autoIndicator = document.createElement('span');
    autoIndicator.className = 'xb-tts-auto-indicator';
    autoToggle.appendChild(autoIndicator);
    const autoText = document.createElement('span');
    autoText.className = 'xb-tts-auto-text';
    autoText.textContent = '自动朗读';
    autoToggle.appendChild(autoText);
    tools.appendChild(autoToggle);

    const settingsBtn = document.createElement('span');
    settingsBtn.className = 'xb-tts-icon-btn settings-btn';
    settingsBtn.title = 'TTS 设置';
    settingsBtn.textContent = '⚙';
    tools.appendChild(settingsBtn);

    menu.appendChild(tools);

    return menu;
}

function createCapsuleElement(mode) {
    const capsule = document.createElement('div');
    capsule.className = 'xb-tts-capsule';

    const loading = document.createElement('div');
    loading.className = 'xb-tts-loading';
    capsule.appendChild(loading);

    const playBtn = document.createElement('button');
    playBtn.className = 'xb-tts-btn play-btn';
    playBtn.title = '播放';
    playBtn.textContent = '▶';
    const autoDot = document.createElement('span');
    autoDot.className = 'xb-tts-auto-dot';
    playBtn.appendChild(autoDot);
    capsule.appendChild(playBtn);

    const info = document.createElement('div');
    info.className = 'xb-tts-info';
    info.appendChild(createWaveElement());
    const statusText = document.createElement('span');
    statusText.className = 'xb-tts-status';
    statusText.textContent = '播放';
    info.appendChild(statusText);
    const badge = document.createElement('span');
    badge.className = 'xb-tts-badge';
    badge.textContent = '0/0';
    info.appendChild(badge);
    capsule.appendChild(info);

    const stopBtn = document.createElement('button');
    stopBtn.className = 'xb-tts-btn stop-btn';
    stopBtn.title = '停止';
    stopBtn.textContent = '■';
    stopBtn.style.display = 'none';
    capsule.appendChild(stopBtn);

    const sep = document.createElement('div');
    sep.className = 'xb-tts-sep';
    capsule.appendChild(sep);

    const expandBtn = document.createElement('button');
    expandBtn.className = 'xb-tts-btn expand-btn';
    expandBtn.title = '设置';
    expandBtn.textContent = mode === 'floating' ? '▲' : '▼';
    capsule.appendChild(expandBtn);

    const progress = document.createElement('div');
    progress.className = 'xb-tts-progress';
    const progressInner = document.createElement('div');
    progressInner.className = 'xb-tts-progress-inner';
    progress.appendChild(progressInner);
    capsule.appendChild(progress);

    return capsule;
}

function createPanelElement(speed, isAuto, mode = 'floor') {
    const div = document.createElement('div');
    div.className = 'xb-tts-panel';
    div.dataset.status = 'idle';
    div.dataset.hasQueue = 'false';
    div.dataset.auto = isAuto ? 'true' : 'false';

    const menu = createMenuElement(speed, isAuto);
    const capsule = createCapsuleElement(mode);

    if (mode === 'floating') {
        div.appendChild(menu);
        div.appendChild(capsule);
    } else {
        div.appendChild(capsule);
        div.appendChild(menu);
    }

    return div;
}

function cachePanelDOM(el) {
    return {
        capsule: el.querySelector('.xb-tts-capsule'),
        playBtn: el.querySelector('.play-btn'),
        stopBtn: el.querySelector('.stop-btn'),
        statusText: el.querySelector('.xb-tts-status'),
        badge: el.querySelector('.xb-tts-badge'),
        progressInner: el.querySelector('.xb-tts-progress-inner'),
        voiceSelect: el.querySelector('.voice-select'),
        speedSlider: el.querySelector('.speed-slider'),
        speedVal: el.querySelector('.speed-val'),
        usageText: el.querySelector('.xb-tts-usage'),
        autoToggle: el.querySelector('.xb-tts-auto-toggle'),
        expandBtn: el.querySelector('.expand-btn'),
        settingsBtn: el.querySelector('.settings-btn'),
    };
}

// ═══════════════════════════════════════════════════════════════════════════
// 共用事件绑定
// ═══════════════════════════════════════════════════════════════════════════

function bindCommonEvents($cache, parentEl = null) {
    $cache.autoToggle?.addEventListener('click', async (e) => {
        e.stopPropagation();
        const config = getConfigFn?.();
        if (!config) return;
        const newValue = config.autoSpeak === false;
        config.autoSpeak = newValue;
        await saveConfigFn?.({ autoSpeak: newValue });
        updateAutoSpeakAll();
    });
    $cache.voiceSelect?.addEventListener('change', async (e) => {
        const config = getConfigFn?.();
        if (config?.volc) {
            config.volc.defaultSpeaker = e.target.value;
            await saveConfigFn?.({ volc: config.volc });
        }
    });
    $cache.speedSlider?.addEventListener('input', (e) => {
        if ($cache.speedVal) {
            $cache.speedVal.textContent = Number(e.target.value).toFixed(1) + 'x';
        }
    });
    $cache.speedSlider?.addEventListener('change', async (e) => {
        const config = getConfigFn?.();
        if (config?.volc) {
            config.volc.speechRate = Number(e.target.value);
            await saveConfigFn?.({ volc: config.volc });
            updateSpeedAll();
        }
    });
    $cache.settingsBtn?.addEventListener('click', (e) => {
        e.stopPropagation();
        // ★ 关闭所有菜单
        panelMap.forEach(data => data.root?.classList.remove('expanded'));
        floatingEl?.classList.remove('expanded');
        openSettingsFn?.();
    });
}

// ═══════════════════════════════════════════════════════════════════════════
// 楼层面板
// ═══════════════════════════════════════════════════════════════════════════

function createFloorPanel(messageId) {
    const config = getConfigFn?.() || {};
    const currentSpeed = config?.volc?.speechRate || 1.0;
    const isAutoSpeak = config?.autoSpeak !== false;

    const div = createPanelElement(currentSpeed, isAutoSpeak, 'floor');
    div.dataset.messageId = messageId;

    return div;
}

function bindFloorPanelEvents(panelData, onPlay) {
    const { messageId, root: el, $cache } = panelData;

    $cache.playBtn?.addEventListener('click', (e) => {
        e.stopPropagation();
        onPlay(messageId);
    });

    $cache.stopBtn?.addEventListener('click', (e) => {
        e.stopPropagation();
        clearQueueFn?.(messageId);
    });

    $cache.expandBtn?.addEventListener('click', (e) => {
        e.stopPropagation();
        el.classList.toggle('expanded');
        if (el.classList.contains('expanded')) {
            fillVoiceSelect($cache.voiceSelect);
            syncSpeedUI($cache);
        }
    });

    bindCommonEvents($cache);

    const closeMenu = (e) => {
        if (!el.contains(e.target)) {
            el.classList.remove('expanded');
        }
    };
    document.addEventListener('click', closeMenu, { passive: true });

    panelData._cleanup = () => {
        document.removeEventListener('click', closeMenu);
        removeFromToolbar(messageId, el);
    };
}

function mountFloorPanel(messageEl, messageId, onPlay) {
    if (panelMap.has(messageId)) {
        const existing = panelMap.get(messageId);
        if (existing.root?.isConnected) return existing;
        existing._cleanup?.();
        panelMap.delete(messageId);
    }

    injectStyles();

    const panel = createFloorPanel(messageId);
    const panelData = { messageId, root: panel, $cache: cachePanelDOM(panel) };

    const success = registerToToolbar(messageId, panel, {
        position: 'left',
        id: `tts-${messageId}`
    });

    if (!success) return null;

    bindFloorPanelEvents(panelData, onPlay);
    panelMap.set(messageId, panelData);

    return panelData;
}

function setupFloorObserver() {
    if (floorObserver) return;

    floorObserver = new IntersectionObserver((entries) => {
        const toMount = [];

        for (const entry of entries) {
            if (!entry.isIntersecting) continue;

            const el = entry.target;
            const mid = Number(el.getAttribute('mesid'));
            const cb = pendingCallbacks.get(mid);

            if (cb) {
                toMount.push({ el, mid, cb });
                pendingCallbacks.delete(mid);
                floorObserver.unobserve(el);
            }
        }

        if (toMount.length > 0) {
            requestAnimationFrame(() => {
                for (const { el, mid, cb } of toMount) {
                    mountFloorPanel(el, mid, cb);
                }
            });
        }
    }, { rootMargin: '300px', threshold: 0 });
}

// ═══════════════════════════════════════════════════════════════════════════
// 悬浮按钮
// ═══════════════════════════════════════════════════════════════════════════

function getFloatingPosition() {
    try {
        const raw = localStorage.getItem(FLOAT_POS_KEY);
        if (raw) return JSON.parse(raw);
    } catch {}
    return { left: window.innerWidth - 110, top: window.innerHeight - 80 };
}

function saveFloatingPosition() {
    if (!floatingEl) return;
    const r = floatingEl.getBoundingClientRect();
    try {
        localStorage.setItem(FLOAT_POS_KEY, JSON.stringify({
            left: Math.round(r.left),
            top: Math.round(r.top)
        }));
    } catch {}
}

function applyFloatingPosition() {
    if (!floatingEl) return;
    const pos = getFloatingPosition();
    const w = floatingEl.offsetWidth || 88;
    const h = floatingEl.offsetHeight || 36;
    floatingEl.style.left = `${Math.max(0, Math.min(pos.left, window.innerWidth - w))}px`;
    floatingEl.style.top = `${Math.max(0, Math.min(pos.top, window.innerHeight - h))}px`;
}

function onFloatingPointerDown(e) {
    if (e.button !== 0) return;

    floatingDragState = {
        startX: e.clientX,
        startY: e.clientY,
        startLeft: floatingEl.getBoundingClientRect().left,
        startTop: floatingEl.getBoundingClientRect().top,
        pointerId: e.pointerId,
        moved: false,
        originalTarget: e.target
    };

    try { e.currentTarget.setPointerCapture(e.pointerId); } catch {}
    e.preventDefault();
}

function onFloatingPointerMove(e) {
    if (!floatingDragState || floatingDragState.pointerId !== e.pointerId) return;

    const dx = e.clientX - floatingDragState.startX;
    const dy = e.clientY - floatingDragState.startY;

    if (!floatingDragState.moved && (Math.abs(dx) > 3 || Math.abs(dy) > 3)) {
        floatingDragState.moved = true;
    }

    if (floatingDragState.moved) {
        const w = floatingEl.offsetWidth || 88;
        const h = floatingEl.offsetHeight || 36;
        floatingEl.style.left = `${Math.max(0, Math.min(floatingDragState.startLeft + dx, window.innerWidth - w))}px`;
        floatingEl.style.top = `${Math.max(0, Math.min(floatingDragState.startTop + dy, window.innerHeight - h))}px`;
    }

    e.preventDefault();
}

function onFloatingPointerUp(e) {
    if (!floatingDragState || floatingDragState.pointerId !== e.pointerId) return;

    const { moved, originalTarget } = floatingDragState;

    try { e.currentTarget.releasePointerCapture(e.pointerId); } catch {}
    floatingDragState = null;

    if (moved) {
        saveFloatingPosition();
    } else {
        routeFloatingClick(originalTarget);
    }
}

function routeFloatingClick(target) {
    if (target.closest('.play-btn')) {
        handleFloatingPlayClick();
    } else if (target.closest('.stop-btn')) {
        const messageId = safeGetLastAIMessageId();
        if (messageId >= 0) clearQueueFn?.(messageId);
    } else if (target.closest('.expand-btn')) {
        floatingEl.classList.toggle('expanded');
        if (floatingEl.classList.contains('expanded')) {
            fillVoiceSelect($floatingCache.voiceSelect);
            syncSpeedUI($floatingCache);
        }
    }
}

function handleFloatingPlayClick() {
    const messageId = safeGetLastAIMessageId();
    if (messageId < 0) {
        if (typeof toastr !== 'undefined') {
            toastr.warning('没有可朗读的AI消息');
        }
        return;
    }
    speakMessageFn?.(messageId);
}

function handleFloatingOutsideClick(e) {
    if (floatingEl && !floatingEl.contains(e.target)) {
        floatingEl.classList.remove('expanded');
    }
}

function createFloatingButton() {
    if (floatingEl) return;

    const config = getConfigFn?.();
    if (!config || config.showFloatingButton !== true) return;

    injectStyles();

    const isAutoSpeak = config.autoSpeak !== false;
    const currentSpeed = config.volc?.speechRate || 1.0;

    floatingEl = createPanelElement(currentSpeed, isAutoSpeak, 'floating');
    floatingEl.classList.add('xb-tts-floating-global');
    floatingEl.id = 'xb-tts-floating-global';

    document.body.appendChild(floatingEl);

    $floatingCache = cachePanelDOM(floatingEl);

    applyFloatingPosition();

    // 拖拽事件
    const capsuleEl = $floatingCache.capsule;
    if (capsuleEl) {
        capsuleEl.addEventListener('pointerdown', onFloatingPointerDown, { passive: false });
        capsuleEl.addEventListener('pointermove', onFloatingPointerMove, { passive: false });
        capsuleEl.addEventListener('pointerup', onFloatingPointerUp, { passive: false });
        capsuleEl.addEventListener('pointercancel', onFloatingPointerUp, { passive: false });
    }

    bindCommonEvents($floatingCache);

    document.addEventListener('click', handleFloatingOutsideClick, { passive: true });
    window.addEventListener('resize', applyFloatingPosition);
}

function destroyFloatingButton() {
    if (!floatingEl) return;
    window.removeEventListener('resize', applyFloatingPosition);
    document.removeEventListener('click', handleFloatingOutsideClick);
    // ★ 显式移除 pointer 事件
    const capsuleEl = $floatingCache.capsule;
    if (capsuleEl) {
        capsuleEl.removeEventListener('pointerdown', onFloatingPointerDown);
        capsuleEl.removeEventListener('pointermove', onFloatingPointerMove);
        capsuleEl.removeEventListener('pointerup', onFloatingPointerUp);
        capsuleEl.removeEventListener('pointercancel', onFloatingPointerUp);
    }
    floatingEl.remove();
    floatingEl = null;
    floatingDragState = null;
    $floatingCache = {};
}

// ═══════════════════════════════════════════════════════════════════════════
// 状态更新
// ═══════════════════════════════════════════════════════════════════════════

function updatePanelStateCore($cache, el, state) {
    if (!el || !state) return;

    const status = state.status || 'idle';
    const current = state.currentSegment || 0;
    const total = state.totalSegments || 0;
    const hasQueue = total > 1;

    el.dataset.status = status;
    el.dataset.hasQueue = hasQueue ? 'true' : 'false';

    let statusText = '';
    let playIcon = '▶';
    let showStop = false;

    switch (status) {
        case 'idle':
            statusText = '播放';
            break;
        case 'sending':
        case 'queued':
            statusText = hasQueue ? `${current}/${total}` : '准备';
            playIcon = '■';
            showStop = true;
            break;
        case 'cached':
            statusText = hasQueue ? `${current}/${total}` : '缓存';
            break;
        case 'playing':
            statusText = hasQueue ? `${current}/${total}` : '';
            playIcon = '⏸';
            showStop = true;
            break;
        case 'paused':
            statusText = hasQueue ? `${current}/${total}` : '暂停';
            showStop = true;
            break;
        case 'ended':
            statusText = '完成';
            playIcon = '↻';
            break;
        case 'blocked':
            statusText = '受阻';
            break;
        case 'error':
            statusText = (state.error || '失败').slice(0, 8);
            playIcon = '↻';
            break;
    }

    if ($cache.playBtn) {
        const existingDot = $cache.playBtn.querySelector('.xb-tts-auto-dot');
        $cache.playBtn.textContent = playIcon;
        if (existingDot) {
            $cache.playBtn.appendChild(existingDot);
        } else {
            const newDot = document.createElement('span');
            newDot.className = 'xb-tts-auto-dot';
            $cache.playBtn.appendChild(newDot);
        }
    }

    if ($cache.statusText) $cache.statusText.textContent = statusText;
    if ($cache.badge && hasQueue && current > 0) $cache.badge.textContent = `${current}/${total}`;
    if ($cache.stopBtn) $cache.stopBtn.style.display = showStop ? '' : 'none';

    if ($cache.progressInner) {
        if (hasQueue && total > 0) {
            $cache.progressInner.style.width = `${Math.min(100, (current / total) * 100)}%`;
        } else if (state.progress && state.duration) {
            $cache.progressInner.style.width = `${Math.min(100, (state.progress / state.duration) * 100)}%`;
        } else {
            $cache.progressInner.style.width = '0%';
        }
    }

    if (state.textLength && $cache.usageText) {
        $cache.usageText.textContent = `${state.textLength} 字`;
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// 全局同步
// ═══════════════════════════════════════════════════════════════════════════

export function updateAutoSpeakAll() {
    const config = getConfigFn?.();
    const isAutoSpeak = config?.autoSpeak !== false;

    panelMap.forEach((data) => {
        if (!data.root) return;
        data.root.dataset.auto = isAutoSpeak ? 'true' : 'false';
        data.$cache?.autoToggle?.classList.toggle('on', isAutoSpeak);
    });

    if (floatingEl) {
        floatingEl.dataset.auto = isAutoSpeak ? 'true' : 'false';
        $floatingCache.autoToggle?.classList.toggle('on', isAutoSpeak);
    }
}

export function updateSpeedAll() {
    panelMap.forEach((data) => {
        if (!data.root) return;
        syncSpeedUI(data.$cache);
    });

    if (floatingEl) {
        syncSpeedUI($floatingCache);
    }
}

export function updateVoiceAll() {
    panelMap.forEach((data) => {
        if (!data.root || !data.$cache?.voiceSelect) return;
        fillVoiceSelect(data.$cache.voiceSelect);
    });

    if (floatingEl && $floatingCache.voiceSelect) {
        fillVoiceSelect($floatingCache.voiceSelect);
    }
}

// ═══════════════════════════════════════════════════════════════════════════
// 对外接口
// ═══════════════════════════════════════════════════════════════════════════

export function initTtsPanelStyles() {
    injectStyles();
}

export function ensureTtsPanel(messageEl, messageId, onPlay) {
    const config = getConfigFn?.();
    if (config?.showFloorButton === false) return null;

    injectStyles();

    if (panelMap.has(messageId)) {
        const existing = panelMap.get(messageId);
        if (existing.root?.isConnected) return existing;
        existing._cleanup?.();
        panelMap.delete(messageId);
    }

    const rect = messageEl.getBoundingClientRect();
    if (rect.top < window.innerHeight + 300 && rect.bottom > -300) {
        return mountFloorPanel(messageEl, messageId, onPlay);
    }

    setupFloorObserver();
    pendingCallbacks.set(messageId, onPlay);
    floorObserver.observe(messageEl);

    return null;
}

export function renderPanelsForChat(chat, getMessageElement, onPlay) {
    const config = getConfigFn?.();
    if (config?.showFloorButton === false) return;

    injectStyles();

    let immediateCount = 0;

    for (let i = chat.length - 1; i >= 0; i--) {
        const message = chat[i];
        if (!message || message.is_user) continue;

        const messageEl = getMessageElement(i);
        if (!messageEl) continue;

        if (panelMap.has(i) && panelMap.get(i).root?.isConnected) {
            continue;
        }

        if (immediateCount < INITIAL_RENDER_LIMIT) {
            mountFloorPanel(messageEl, i, onPlay);
            immediateCount++;
        } else {
            setupFloorObserver();
            pendingCallbacks.set(i, onPlay);
            floorObserver.observe(messageEl);
        }
    }
}

export function updateTtsPanel(messageId, state) {
    const panelData = panelMap.get(messageId);
    if (panelData?.root && state) {
        updatePanelStateCore(panelData.$cache, panelData.root, state);
    }

    if (floatingEl && messageId === safeGetLastAIMessageId()) {
        updatePanelStateCore($floatingCache, floatingEl, state);
    }
}

export function resetFloatingState() {
    if (!floatingEl) return;

    floatingEl.dataset.status = 'idle';
    floatingEl.dataset.hasQueue = 'false';

    if ($floatingCache.statusText) $floatingCache.statusText.textContent = '播放';
    if ($floatingCache.badge) $floatingCache.badge.textContent = '0/0';
    if ($floatingCache.progressInner) $floatingCache.progressInner.style.width = '0%';
    if ($floatingCache.stopBtn) $floatingCache.stopBtn.style.display = 'none';
    if ($floatingCache.usageText) $floatingCache.usageText.textContent = '-字';

    if ($floatingCache.playBtn) {
        const dot = $floatingCache.playBtn.querySelector('.xb-tts-auto-dot');
        $floatingCache.playBtn.textContent = '▶';
        if (dot) $floatingCache.playBtn.appendChild(dot);
    }
}

export function removeTtsPanel(messageId) {
    const data = panelMap.get(messageId);
    if (data) {
        data._cleanup?.();
        panelMap.delete(messageId);
    }
    pendingCallbacks.delete(messageId);
}

export function removeAllTtsPanels() {
    panelMap.forEach((data) => data._cleanup?.());
    panelMap.clear();
    pendingCallbacks.clear();

    floorObserver?.disconnect();
    floorObserver = null;
}

export function initFloatingPanel() {
    if (!getConfigFn) return;
    createFloatingButton();
}

export function destroyFloatingPanel() {
    destroyFloatingButton();
}

export function updateButtonVisibility(showFloor, showFloating) {
    if (showFloating && !floatingEl) {
        createFloatingButton();
    } else if (!showFloating && floatingEl) {
        destroyFloatingButton();
    }

    if (!showFloor) {
        removeAllTtsPanels();
    }
}

export function getPanelMap() {
    return panelMap;
}
