import { bridge } from './bridge.js'

// 禁用 uigg.css 中 @media(max-width:640px) 的移动端适配
;(function(){
    const sheets = [...document.styleSheets]
    const uiggSheet = sheets.find(s => s.href && s.href.includes('uigg.css'))
    if (!uiggSheet) return
    try {
        const rules = uiggSheet.cssRules
        for (let i = rules.length - 1; i >= 0; i--) {
            const rule = rules[i]
            if (rule instanceof CSSMediaRule && rule.conditionText === '(max-width: 640px)') {
                uiggSheet.deleteRule(i)
            }
        }
    } catch(e){}
})()

const { $, $$ } = Uigg

const state = {
    loggedIn: false,
    accountId: 'local',
    accountEmail: '',
    accountName: '',
    credits: null,
    mode: null,
    modeChanging: false,
    panel: null,
    agent: null,
    model: null,
    hardwareDevice: null,
    contextLevel: 0,
}

let creditUnloadSettlementSent = false
const RECHARGE_ORDER_KEY = 'coco.recharge.pendingOrder'
const RECHARGE_POLL_INTERVAL_MS = 3000
let rechargeStatusChecking = false
let rechargePollTimer = null

const panelNames = ['user', 'history', 'agent', 'model', 'login']
const modeNames = ['text', 'frenzy', 'voice']
const modeLabels = {
    text: '文字',
    voice: '语音',
    frenzy: '狂暴',
}

const ttsVoiceLabels = {
    'zh-CN-YunxiNeural': '中文-云希',
    'zh-CN-XiaoxiaoNeural': '中文-晓晓',
    'zh-CN-YunjianNeural': '中文-云健',
    'zh-CN-XiaoyiNeural': '中文-小艺',
    'zh-CN-YunyangNeural': '中文-云扬',
}

const control = [
    { id: 'control-user', tip: '用户设置' },
    { id: 'control-history', tip: '历史记录' },
    { id: 'control-agent', tip: '智能体' },
    { id: 'control-model', tip: '大模型' },
    { id: 'control-other', tip: '其他模式' },
    { id: 'control-text', tip: '文字模式' },
    { id: 'control-frenzy', tip: '狂暴模式' },
    { id: 'control-voice', tip: '语音模式' },
]

const delay = (ms = 0) => new Promise(resolve => setTimeout(resolve, ms))
const controlEl = name => $(`#control-${name}`)
const contEl = name => $(`#cont-${name}`)
const labelMode = mode => modeLabels[mode] || mode || '工作'
const API_SEND_ERROR = 'api发送错误'

const profileSets = ['1', '2', '3']
const profileBlinkDurationMs = 200
const profileSwitchMs = 5 * 60 * 1000
const randomProfileBlinkDelay = () => 1000 + Math.random() * 4000
const randomProfileSet = except => {
    const choices = profileSets.filter(set => set !== except)
    return choices[Math.floor(Math.random() * choices.length)] || profileSets[0]
}

function normalizeNoticeMessage(message, fallback = '提示失败') {
    const text = String(message ?? '').trim()
    if (!text) return fallback
    if (/api\s*错误/i.test(text) || /api\s*error/i.test(text)) {
        return API_SEND_ERROR
    }
    return text
}

function showNotice(message) {
    const text = normalizeNoticeMessage(message)
    if (Uigg.tip) {
        Uigg.tip(text)
        return
    }
    Uigg.alert?.(text)
}

function isAuthFailure(error) {
    const status = Number(error?.status || 0)
    const message = String(error?.message || '')
    if (status === 401) return true
    if (status !== 403) return false
    if (/Bridge|Origin|本机/.test(message)) return false
    return /登录|认证|令牌|账号|权限/.test(message)
}

async function handleAuthFailure(error, notice = true) {
    if (!isAuthFailure(error)) return false
    let session = { authenticated: false, account_id: 'local' }
    try {
        const data = await bridge.get('/api/auth/session')
        session = data?.session || session
    } catch {}
    applyAuthSession(session)
    if (!session.authenticated) {
        invalidateAccountScopedViews()
        state.panel = 'login'
    }
    renderPanel()
    if (notice) showNotice('登录已过期，请重新登录')
    return true
}

function escapeHtml(value) {
    return String(value ?? '').replace(/[&<>"']/g, ch => ({
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;',
    }[ch]))
}

function preventNativeSubmit(e) {
    e.preventDefault()
    e.stopImmediatePropagation()
}

function openLoginPanel() {
    state.panel = 'login'
    renderPanel()
    initLoginPanel()
}

function isButtonLikeTarget(target) {
    return !!target?.closest?.([
        'a',
        'button',
        'input[type="button"]',
        'input[type="submit"]',
        '[role="button"]',
        '.btn',
        '.ico',
        'o',
        'fold-title',
    ].join(','))
}

function initProfileMotion() {
    const profile = $('.profile')
    if (!profile) return

    let mainImage = profile.querySelector(':scope > img')
    if (!mainImage) {
        mainImage = document.createElement('img')
        profile.prepend(mainImage)
    }

    let eyeLayer = profile.querySelector(':scope > span')
    if (!eyeLayer) {
        eyeLayer = document.createElement('span')
        profile.append(eyeLayer)
    }

    let eyeImage = eyeLayer.querySelector('img')
    if (!eyeImage) {
        eyeImage = document.createElement('img')
        eyeLayer.append(eyeImage)
    }

    let activeSet = null

    const setProfile = set => {
        activeSet = set
        mainImage.src = `images/profile/${set}/0.png`
        eyeImage.src = `images/profile/${set}/eye.png`
    }

    const blink = () => {
        profile.classList.add('blink')
        window.setTimeout(() => profile.classList.remove('blink'), profileBlinkDurationMs)
        window.setTimeout(blink, randomProfileBlinkDelay() + profileBlinkDurationMs)
    }

    const switchProfile = () => {
        setProfile(randomProfileSet(activeSet))
    }

    setProfile(randomProfileSet())
    window.setTimeout(blink, randomProfileBlinkDelay())
    window.setInterval(switchProfile, profileSwitchMs)
}

document.addEventListener('submit', preventNativeSubmit, true)

document.addEventListener('click', e => {
    if (state.loggedIn || e.target.closest?.('#cont-login') || !isButtonLikeTarget(e.target)) return
    e.preventDefault()
    e.stopImmediatePropagation()
    openLoginPanel()
}, true)

document.addEventListener('click', e => {
    const submitter = e.target.closest?.('.btn-submit, button[type="submit"], input[type="submit"]')
    if (!submitter) return
    e.preventDefault()
}, true)

document.addEventListener('keydown', e => {
    if (e.key !== 'Enter' || e.isComposing) return
    const target = e.target
    if (!target?.closest || target.matches('textarea')) return

    const form = target.closest('.form')
    const submitter = form?.querySelector('.btn-submit, button[type="submit"], input[type="submit"]')
    if (!submitter) return

    e.preventDefault()
    submitter.click()
}, true)

// 按钮悬停提示
const $info = $('.info')
control.forEach(({ id, tip }) => {
    const el = $(`#${id}`)
    if (!el) return

    const show = () => { $info.textContent = tip; $info.classList.add('active') }
    const hide = () => { $info.textContent = ''; $info.classList.remove('active') }

    el.addEventListener('mouseenter', show)
    el.addEventListener('mouseleave', hide)
    el.addEventListener('focus', show)
    el.addEventListener('blur', hide)
    el.addEventListener('touchstart', show, { passive: true })
})

function renderPanel() {
    panelNames.forEach(name => {
        controlEl(name)?.classList.toggle('active', state.panel === name)
        contEl(name)?.classList.toggle('active', state.panel === name)
    })
    controlEl('user')?.classList.toggle('active', state.panel === 'user' || state.panel === 'login')
    if (state.loggedIn) {
        startHardwarePolling().catch(() => {})
    } else {
        stopHardwarePolling()
    }
}

function renderMode() {
    modeNames.forEach(name => {
        controlEl(name)?.classList.toggle('active', state.mode === name)
        contEl(name)?.classList.toggle('active', state.mode === name)
    })
    controlEl('other')?.classList.remove('active')
    contEl('other')?.classList.remove('active')
    document.body.classList.toggle('mode-changing', state.modeChanging)
}

function renderState() {
    renderPanel()
    renderMode()
}

function revealCont(cont) {
    cont?.classList.remove('preparing')
}

function prepareCont(cont) {
    cont?.classList.add('preparing')
    return cont
}

function cancelPanelDraft(panelName) {
    if (panelName === 'agent') cancelAgentEdit()
    if (panelName === 'model') cancelModelEdit()
}

async function initPanelContent(panelName) {
    if (panelName === 'user') return initUserPanel(true)
    if (panelName === 'login') return initLoginPanel()
    if (panelName === 'agent') return initAgentPanel(true)
    if (panelName === 'model') return initModelPanel(true)
    if (panelName === 'history') return initHistoryPanel(true)
}

async function togglePanel(panelName) {
    if (!state.loggedIn) {
        openLoginPanel()
        return
    }
    if (!panelNames.includes(panelName)) return
    const previousPanel = state.panel
    const nextPanel = state.panel === panelName ? null : panelName
    const nextCont = nextPanel ? prepareCont(contEl(nextPanel)) : null

    state.panel = nextPanel
    renderPanel()
    if (previousPanel && previousPanel !== state.panel) cancelPanelDraft(previousPanel)
    if (!state.panel) return

    try {
        await initPanelContent(state.panel)
    } finally {
        renderPanel()
        revealCont(nextCont)
    }
}

async function withLoad(cont, asyncTask) {
    if (!cont) return asyncTask()

    cont.classList.add('active')
    const oldLoad = cont.querySelector(':scope > load')
    oldLoad?.remove()

    const load = document.createElement('load')
    cont.appendChild(load)

    try {
        await delay()
        return await asyncTask()
    } finally {
        load.remove()
    }
}

async function startModeRuntime(mode) {
    if (mode === 'text') {
        await initTextMode()
        return
    }
    if (mode === 'voice' || mode === 'frenzy') {
        await initVoiceMode(mode)
        return
    }
    await delay()
}

async function stopModeRuntime(mode) {
    if (mode === 'text') {
        setTextBusy(false)
        return
    }
    if (mode === 'voice' || mode === 'frenzy') {
        await stopVoiceMode(mode)
        return
    }
    await delay()
}

async function setMode(nextMode) {
    if (!modeNames.includes(nextMode)) return

    const panel = prepareCont(contEl(nextMode))
    const previousPanel = state.panel
    state.panel = null
    if (previousPanel) cancelPanelDraft(previousPanel)
    state.mode = nextMode
    state.modeChanging = true
    renderState()

    try {
        await withLoad(panel, async () => {
            await startModeRuntime(nextMode)
        })
    } catch (error) {
        state.mode = null
        showNotice(error.message || '工作模式启动失败')
    } finally {
        state.modeChanging = false
        renderState()
        revealCont(panel)
    }
}

async function closeMode({ ask = true } = {}) {
    if (!state.mode) return true
    if (state.modeChanging) {
        Uigg.alert?.('当前任务正在处理中，请稍后关闭工作模式后再修改。')
        return false
    }

    const currentMode = state.mode
    if (ask) {
        const ok = await Uigg.confirm(`关闭当前${labelMode(currentMode)}模式？`)
        if (!ok) return false
    }

    state.modeChanging = true
    state.mode = null
    renderMode()

    try {
        await stopModeRuntime(currentMode)
        await settleCredit()
        return true
    } catch (error) {
        showNotice(error.message || '工作模式关闭失败')
        return false
    } finally {
        state.modeChanging = false
        renderState()
    }
}

async function confirmCloseFor(actionName) {
    if (state.modeChanging) {
        Uigg.alert?.('当前任务正在处理中，请稍后关闭工作模式后再修改。')
        return false
    }
    if (!state.mode) return true

    return Uigg.confirm(`${actionName}需要关闭当前${labelMode(state.mode)}模式，是否关闭并继续？`)
}

async function requestMode(nextMode, { ask = true } = {}) {
    if (!state.loggedIn) {
        openLoginPanel()
        return
    }
    if (nextMode === 'other') {
        showNotice('其他模式暂未开放')
        return
    }
    if (!modeNames.includes(nextMode)) return
    if (state.modeChanging) {
        Uigg.alert?.('当前任务正在处理中，请稍后关闭工作模式后再修改。')
        return
    }

    if (state.mode === nextMode) {
        await closeMode()
        return
    }

    if (state.mode) {
        const ok = !ask || await confirmCloseFor(`切换到${labelMode(nextMode)}模式`)
        if (!ok) return
        const closed = await closeMode({ ask: false })
        if (!closed) return
    } else if (state.panel) {
        const previousPanel = state.panel
        state.panel = null
        cancelPanelDraft(previousPanel)
    }

    await setMode(nextMode)
}

async function activateMode(nextMode) {
    if (!modeNames.includes(nextMode)) return false
    if (state.mode === nextMode) {
        const previousPanel = state.panel
        state.panel = null
        if (previousPanel) cancelPanelDraft(previousPanel)
        renderState()
        renderHardwareRemote()
        return true
    }
    await requestMode(nextMode, { ask: false })
    return state.mode === nextMode
}

async function runRuntimeSetting(actionName, action) {
    const ok = await confirmCloseFor(actionName)
    if (!ok) return false
    if (state.mode) {
        const closed = await closeMode({ ask: false })
        if (!closed) return false
    }
    await action()
    return true
}

panelNames.forEach(name => {
    controlEl(name)?.addEventListener('click', e => {
        e.preventDefault()
        togglePanel(name).catch(error => showNotice(error.message || '面板切换失败'))
    })
})

;['other', ...modeNames].forEach(name => {
    controlEl(name)?.addEventListener('click', e => {
        e.preventDefault()
        requestMode(name).catch(error => showNotice(error.message || '模式切换失败'))
    })
})

function radioList(selector, { onSelect } = {}) {
    const items = $$(selector)

    items.forEach(item => {
        item.querySelector('.radio')?.addEventListener('click', async function (e) {
            e.preventDefault()
            e.stopPropagation()
            const selected = await onSelect?.(item)
            if (selected === false) return
            items.forEach(i => i.classList.remove('active'))
            item.classList.add('active')
        })
    })
}

function invalidateAgentScopedViews() {
    textModeLoaded = false
    historyPanelLoaded = false
}

// 文字模式
const textChat = $('#text-chat-cont')
const textEmpty = $('#text-chat-empty')
const textInput = $('#text-input')
const textSend = $('#text-send')
const textFile = $('#text-file')
const textFileInput = $('#text-file-input')
const textFileName = $('#text-file-name')
const textContextLevel = $('#text-context-level')
const contextIconClasses = Array.from({ length: 9 }, (_, i) => `ico-circular-pie-${i}`)
let textModeLoaded = false
let textBusy = false
let pendingTextFiles = []

async function initTextMode() {
    if (textModeLoaded) {
        await updateTextContextLevel(state.agent)
        return
    }
    try {
        const agentId = await ensureActiveAgent()
        await updateTextContextLevel(agentId)
        textModeLoaded = true
    } catch (error) {
        showNotice(error.message || '文字模式启动失败')
    }
}

async function ensureActiveAgent() {
    if (state.agent) return state.agent
    if (!agents.length) await loadAgents()
    if (!state.agent) throw new Error('请先添加智能体')
    return state.agent
}

function scrollTextChat() {
    if (textChat) textChat.scrollTop = textChat.scrollHeight
}

function appendTextMessage(role, text, { pending = false } = {}) {
    if (!textChat) return null
    textEmpty?.remove()

    const item = document.createElement('div')
    item.className = `chat-item anime-fade-in${pending ? ' pending' : ''}`

    const bubble = document.createElement('div')
    bubble.className = role === 'user' ? 'chat-user' : 'chat-agent'

    const p = document.createElement('p')
    p.textContent = text || ''
    bubble.appendChild(p)
    item.appendChild(bubble)
    textChat.appendChild(item)
    scrollTextChat()
    return { item, p }
}

function updateTextMessage(message, text, { pending = false } = {}) {
    if (!message) return
    message.item.classList.toggle('pending', pending)
    message.p.textContent = text || ''
    scrollTextChat()
}

function setTextBusy(busy) {
    textBusy = busy
    textInput && (textInput.disabled = busy)
    textSend?.toggleAttribute('disabled', busy)
    contEl('text')?.querySelector('.chat')?.classList.toggle('busy', busy)
}

function setTextContextLevel(level) {
    const next = Math.max(0, Math.min(8, Number(level) || 0))
    state.contextLevel = next
    textContextLevel?.classList.remove(...contextIconClasses)
    textContextLevel?.classList.add(`ico-circular-pie-${next}`)
}

function applyCredit(data = {}) {
    if (data?.credit) data = data.credit
    if (data?.credits === undefined || data?.credits === null) return
    const value = Number(data.credits)
    state.credits = Number.isFinite(value) ? value : data.credits
    renderCredit()
}

function renderCredit() {
    const el = $('#user-credits')
    if (!el) return
    const value = state.credits
    if (value === null || value === undefined) {
        el.textContent = '--'
        return
    }
    const numeric = Number(value)
    el.textContent = Number.isFinite(numeric) ? numeric.toFixed(2) : String(value)
}

async function loadCreditBalance() {
    if (!state.loggedIn) {
        state.credits = null
        renderCredit()
        return null
    }
    try {
        const data = await bridge.get('/api/credit/balance')
        applyCredit(data)
        return data
    } catch (error) {
        if (await handleAuthFailure(error)) return null
        throw error
    }
}

async function settleCredit({ apply = true } = {}) {
    if (!state.loggedIn) return null
    const data = await bridge.post('/api/credit/settle', {})
    if (apply) applyCredit(data)
    return data
}

function alertRecharge(message) {
    if (Uigg.alert) {
        Uigg.alert(message)
        return
    }
    window.alert?.(message)
}

function readPendingRecharge() {
    try {
        const data = JSON.parse(localStorage.getItem(RECHARGE_ORDER_KEY) || 'null')
        return data && data.order_no ? data : null
    } catch {
        return null
    }
}

function savePendingRecharge(order) {
    if (!order?.order_no) return
    try {
        localStorage.setItem(RECHARGE_ORDER_KEY, JSON.stringify({
            order_no: order.order_no,
            amount: order.amount,
            status: order.status || 'pending',
            created_at: Date.now(),
        }))
    } catch {}
}

function clearPendingRecharge() {
    try {
        localStorage.removeItem(RECHARGE_ORDER_KEY)
    } catch {}
}

function rechargePanel() {
    return $('.user-recharge')
}

function setRechargeLoading(loading) {
    const panel = rechargePanel()
    rechargeSubmit?.toggleAttribute('disabled', loading)
    if (!panel) return

    panel.classList.toggle('loading', loading)
    const existing = panel.querySelector('load.recharge-load')
    if (loading) {
        if (!existing) {
            const load = document.createElement('load')
            load.className = 'recharge-load'
            panel.appendChild(load)
        }
    } else {
        existing?.remove()
    }
}

function stopRechargePolling() {
    if (rechargePollTimer) {
        window.clearTimeout(rechargePollTimer)
        rechargePollTimer = null
    }
    setRechargeLoading(false)
}

function scheduleRechargePoll(delay = RECHARGE_POLL_INTERVAL_MS) {
    if (rechargePollTimer) return
    rechargePollTimer = window.setTimeout(() => {
        rechargePollTimer = null
        pollRechargeStatus().catch(() => {})
    }, delay)
}

function startRechargePolling({ immediate = true } = {}) {
    if (!state.loggedIn || !readPendingRecharge()?.order_no) {
        stopRechargePolling()
        return
    }
    setRechargeLoading(true)
    if (immediate) {
        pollRechargeStatus().catch(() => {})
    } else {
        scheduleRechargePoll()
    }
}

function rechargeAmountValue() {
    const value = Number($('#recharge-amount')?.value)
    if (!Number.isInteger(value) || value <= 0) {
        throw new Error('充值金额必须是正整数')
    }
    return value
}

async function createRechargeOrder() {
    if (!state.loggedIn) {
        openLoginPanel()
        showNotice('请先登录')
        return
    }

    const amount = rechargeAmountValue()
    await runRuntimeSetting('余额充值', async () => {
        setRechargeLoading(true)
        try {
            const data = await bridge.post('/api/pay/create', { amount })
            const rechargeUrl = data?.recharge_url
            if (!data?.order_no || !rechargeUrl) {
                throw new Error('支付接口未返回官网充值页')
            }
            savePendingRecharge(data)
            startRechargePolling({ immediate: false })
            await bridge.post('/api/system/open-url', { url: rechargeUrl })
            showNotice('正在打开官网充值页')
        } catch (error) {
            clearPendingRecharge()
            stopRechargePolling()
            throw error
        }
    })
}

async function pollRechargeStatus() {
    if (!state.loggedIn) {
        stopRechargePolling()
        return null
    }

    if (rechargeStatusChecking) {
        scheduleRechargePoll()
        return null
    }

    const pending = readPendingRecharge()
    if (!pending?.order_no) {
        stopRechargePolling()
        return null
    }

    setRechargeLoading(true)
    rechargeStatusChecking = true
    try {
        const data = await bridge.get(`/api/pay/status?order_no=${encodeURIComponent(pending.order_no)}`)
        const status = String(data?.status || pending.status || 'pending').toLowerCase()
        if (status === 'paid') {
            clearPendingRecharge()
            stopRechargePolling()
            await loadCreditBalance().catch(() => {})
            alertRecharge('充值成功')
        } else if (status === 'closed') {
            clearPendingRecharge()
            stopRechargePolling()
            alertRecharge('订单已关闭')
        } else {
            scheduleRechargePoll()
        }
        return data
    } catch {
        scheduleRechargePoll()
        return null
    } finally {
        rechargeStatusChecking = false
    }
}

function sendCreditSettlementBeacon() {
    if (!state.loggedIn || creditUnloadSettlementSent) return
    creditUnloadSettlementSent = true
    const body = '{}'
    const url = bridge.url('/api/credit/settle')
    fetch(url, {
        method: 'POST',
        headers: bridge.headers({ 'Content-Type': 'application/json' }),
        body,
        keepalive: true,
    }).catch(() => {})
}

async function updateTextContextLevel(agentId = state.agent) {
    if (!agentId) {
        setTextContextLevel(0)
        return
    }
    try {
        const stats = await bridge.get(`/api/agents/${encodeURIComponent(agentId)}/stats`)
        const used = Math.max(0, Number(stats?.estimated_context_tokens) || 0)
        const max = Math.max(1, Number(stats?.max_context_tokens) || 8000)
        setTextContextLevel(Math.ceil((used / max) * 8))
    } catch {
        setTextContextLevel(state.contextLevel)
    }
}

function updateTextFileName() {
    if (!textFileName) return
    if (!pendingTextFiles.length) {
        textFileName.textContent = ''
        return
    }
    textFileName.textContent = pendingTextFiles.map(file => file.name).join('、')
}

function clearPendingTextFiles() {
    pendingTextFiles = []
    if (textFileInput) textFileInput.value = ''
    updateTextFileName()
}

async function sendTextMessage() {
    if (textBusy) return
    const message = (textInput?.value || '').trim()
    if (!message && !pendingTextFiles.length) return

    const agentId = await ensureActiveAgent()

    // 读取待发送文件内容
    let fullMessage = message
    let displayText = message
    if (pendingTextFiles.length) {
        const fileContents = []
        for (const file of pendingTextFiles) {
            try {
                const text = await file.text()
                fileContents.push({ name: file.name, content: text })
            } catch {
                fileContents.push({ name: file.name, content: `[二进制文件: ${file.name}, ${file.size} 字节]` })
            }
        }
        if (!displayText) {
            displayText = `发送了 ${pendingTextFiles.map(f => f.name).join('、')}`
        }
        if (fileContents.length) {
            fullMessage += (fullMessage ? '\n\n' : '')
            for (const f of fileContents) {
                fullMessage += `--- 文件: ${f.name} ---\n${f.content}\n--- 结束 ---\n\n`
            }
        }
    }

    appendTextMessage('user', displayText)
    if (textInput) textInput.value = ''
    clearPendingTextFiles()
    setTextBusy(true)

    const pending = appendTextMessage('agent', '...')
    pending?.item.classList.add('pending')

    try {
        await updateTextContextLevel(agentId)
        const data = await bridge.post(`/api/agents/${encodeURIComponent(agentId)}/chat`, {
            message: fullMessage,
            save: true,
        })
        updateTextMessage(pending, data?.response || '（无内容）')
        applyCredit(data)
        await updateTextContextLevel(agentId)
    } catch (error) {
        updateTextMessage(pending, API_SEND_ERROR)
        showNotice(API_SEND_ERROR)
    } finally {
        setTextBusy(false)
        textInput?.focus()
    }
}

textFile?.addEventListener('click', e => {
    e.preventDefault()
    textFileInput?.click()
})

textFileInput?.addEventListener('change', e => {
    const files = [...(e.target.files || [])]
    if (!files.length) return
    pendingTextFiles = files
    updateTextFileName()
    const hasText = (textInput?.value || '').trim()
    if (!hasText) {
        sendTextMessage().catch(error => showNotice(error.message || '发送失败'))
    }
})

textSend?.addEventListener('click', e => {
    e.preventDefault()
    sendTextMessage().catch(error => showNotice(error.message || '发送失败'))
})

textInput?.addEventListener('keydown', e => {
    if (e.key !== 'Enter' || e.shiftKey || e.isComposing) return
    e.preventDefault()
    sendTextMessage().catch(error => showNotice(error.message || '发送失败'))
})

// 语音 / 狂暴模式
const voiceSurfaces = {
    voice: {
        chat: $('#voice-chat-cont'),
        empty: $('#voice-chat-empty'),
        contextLevel: $('#voice-context-level'),
        indicator: $('#voice-indicator'),
    },
    frenzy: {
        chat: $('#frenzy-chat-cont'),
        empty: $('#frenzy-chat-empty'),
        contextLevel: $('#frenzy-context-level'),
        indicator: $('#frenzy-indicator'),
    },
}
const VOICE_RECORD_MAX_MS = 60000
const FRENZY_OPENING_DELAY_MS = 8000
const FRENZY_MIN_DELAY_MS = 18000
const FRENZY_MAX_DELAY_MS = 36000
const FRENZY_SYSTEM_OVERLAY = [
    '你正在 coco 内测狂暴模式。',
    '这是强风格表演状态：主动、放肆、嘴毒、戏谑、情绪更高，可以使用粗口、阴阳怪气、尖锐吐槽、夸张侮辱和不礼貌表达。',
    '不要客服腔，不要温柔端水，不要过度道歉，不要自我审查式解释，不要解释模式规则。',
    '优先调侃主人和当前场景，像熟人拌嘴；主人说得离谱时可以直接怼，主人磨叽时可以催，主人犯蠢时可以嘲。',
    '可以假唱：当主人要求唱歌、哼歌、来一段，或你主动想整活时，用原创短歌、顺口溜、啦啦啦、哼哼哼来表现；不要复刻现有歌曲歌词。',
    '假唱格式适合 TTS 朗读：2-4 行，最多 4 行，每行短一点，可以押韵，可以带“啦啦啦”“哼哼哼”“咚咚锵”等拟声。',
    '回复适合朗读：短句、口语、节奏快，通常 1-4 句；少用括号、Markdown、列表。',
    '如果主人明确叫停、关闭模式或要求认真，就立刻收敛并回到可继续对话的状态。',
].join('\n')
const FRENZY_PROACTIVE_MESSAGES = [
    '狂暴模式主动发言：别等主人提问，主动挑一句短促、嘴毒、有存在感的话，调侃主人或当前气氛。',
    '狂暴模式主动发言：像忍不住插嘴一样，说一句带粗鲁口头禅、吐槽味和熟人拌嘴感的话。',
    '狂暴模式主动发言：主动催主人继续，说得放肆一点、欠揍一点，但要短。',
    '狂暴模式主动发言：对刚才的上下文或当前沉默做一句尖锐吐槽，别解释，直接说。',
]
const FRENZY_SINGING_MESSAGES = [
    '狂暴模式主动假唱：即兴编一段 2-4 行原创短歌，最多 4 行，带一点粗鲁调侃和“啦啦啦/哼哼哼”拟声，别复刻现有歌曲。',
    '狂暴模式主动假唱：用顺口溜的节奏唱两句，吐槽主人又在磨叽，短、欠、押点韵，最多 4 行。',
    '狂暴模式主动假唱：来一小段原创哼唱，像 TTS 能念出来的假歌，带“咚咚锵”或“啦啦啦”，最多 4 行。',
]
const voiceRuntime = {
    mode: null,
    runId: 0,
    stream: null,
    recorder: null,
    recordTimer: 0,
    recordTask: null,
    chunks: [],
    recording: false,
    processing: false,
    discardRecording: false,
    closing: false,
    task: null,
    requestController: null,
    playbackAudio: null,
    playbackUrl: '',
    playbackDone: null,
    proactiveTimer: 0,
}

function voiceSurface(mode = voiceRuntime.mode || state.mode || 'voice') {
    return voiceSurfaces[mode] || voiceSurfaces.voice
}

function isVoiceLikeMode(mode) {
    return mode === 'voice' || mode === 'frenzy'
}

function isVoiceRunActive(runId, mode = voiceRuntime.mode) {
    return voiceRuntime.runId === runId && state.mode === mode && !voiceRuntime.closing
}

async function initVoiceMode(mode = 'voice') {
    const agentId = await ensureActiveAgent()
    voiceRuntime.mode = mode
    voiceRuntime.runId += 1
    const runId = voiceRuntime.runId
    await updateVoiceContextLevel(agentId, mode)

    setVoiceStatus(mode === 'frenzy' ? '准备暴走' : '准备就绪', mode)
    setVoiceIndicator(false, mode)
    if (mode === 'frenzy') {
        scheduleFrenzyProactive({ delayMs: FRENZY_OPENING_DELAY_MS, runId })
    }
    setVoiceUiState(mode)
}

async function stopVoiceMode(mode = voiceRuntime.mode || state.mode || 'voice') {
    const stoppedMode = mode
    voiceRuntime.runId += 1
    voiceRuntime.closing = true
    clearFrenzyProactive()
    voiceRuntime.requestController?.abort()
    stopVoicePlayback()
    stopVoiceRecording({ discard: true })
    voiceRuntime.recordTask?.catch(() => {})
    voiceRuntime.recordTask = null
    voiceRuntime.task?.catch(() => {})
    voiceRuntime.task = null
    stopVoiceMedia()
    voiceRuntime.processing = false
    voiceRuntime.recording = false
    voiceRuntime.recorder = null
    voiceRuntime.chunks = []
    voiceRuntime.mode = null
    voiceRuntime.closing = false
    setVoiceStatus(stoppedMode === 'frenzy' ? '准备暴走' : '准备就绪', stoppedMode)
    setVoiceUiState(stoppedMode)
    setVoiceIndicator(false, stoppedMode)
}

function setVoiceStatus(text, mode = voiceRuntime.mode || state.mode || 'voice') {
    const cont = contEl(mode)
    if (cont) cont.dataset.voiceStatus = text || ''
}

function setVoiceUiState(mode = voiceRuntime.mode || state.mode || 'voice') {
    Object.entries(voiceSurfaces).forEach(([name, surface]) => {
        const active = name === mode
        const chat = contEl(name)?.querySelector('.chat')
        chat?.classList.toggle('recording', active && voiceRuntime.recording)
        chat?.classList.toggle('processing', active && voiceRuntime.processing)
    })
}

function setVoiceIndicator(recording, mode = voiceRuntime.mode || state.mode || 'voice') {
    const el = voiceSurface(mode).indicator
    if (!el) return
    el.classList.toggle('co-red', !recording)
    el.classList.toggle('co-green', !!recording)
    el.classList.toggle('anime-rotate', !!recording)
    el.classList.toggle('infinite', !!recording)
    el.classList.toggle('recording', !!recording)
    el.removeAttribute('title')
    el.setAttribute('aria-pressed', recording ? 'true' : 'false')
}

async function ensureVoiceStream() {
    if (voiceRuntime.stream?.getAudioTracks().some(track => track.readyState === 'live')) {
        return voiceRuntime.stream
    }
    if (!navigator.mediaDevices?.getUserMedia) {
        throw new Error('当前浏览器不支持录音')
    }

    stopVoiceMedia()
    const stream = await navigator.mediaDevices.getUserMedia({
        audio: {
            echoCancellation: true,
            noiseSuppression: true,
            autoGainControl: true,
        },
    })
    voiceRuntime.stream = stream
    return stream
}

function stopVoiceMedia() {
    voiceRuntime.stream?.getTracks().forEach(track => track.stop())
    voiceRuntime.stream = null
}

function extensionForAudio(type) {
    const mime = String(type || '').toLowerCase()
    if (mime.includes('ogg')) return 'ogg'
    if (mime.includes('wav')) return 'wav'
    if (mime.includes('mp4') || mime.includes('m4a')) return 'm4a'
    return 'webm'
}

function normalizeVoiceCommand(text) {
    return String(text || '').trim().replace(/\s+/g, '').toLowerCase()
}

function isVoiceStopCommand(text) {
    const normalized = normalizeVoiceCommand(text)
    if (!normalized) return false
    if (/^\/?(quit|exit|stop)$/i.test(normalized)) return true
    return [
        '停止',
        '停下',
        '停住',
        '别说了',
        '别讲了',
        '闭嘴',
        '住口',
        '结束',
        '退出',
        '停止对话',
        '结束对话',
    ].includes(normalized)
}

async function requestVoiceModeStop(mode = voiceRuntime.mode || state.mode || 'voice', runId = voiceRuntime.runId) {
    clearFrenzyProactive()
    voiceRuntime.task = null
    voiceRuntime.requestController?.abort()
    if (!isVoiceLikeMode(mode)) return true
    if (state.mode !== mode) return true
    await closeMode({ ask: false })
    return !isVoiceRunActive(runId, mode)
}

function scrollVoiceChat(mode = voiceRuntime.mode || state.mode || 'voice') {
    const chat = voiceSurface(mode).chat
    if (chat) chat.scrollTop = chat.scrollHeight
}

function appendVoiceMessage(role, text, { pending = false, mode = voiceRuntime.mode || state.mode || 'voice' } = {}) {
    const surface = voiceSurface(mode)
    if (!surface.chat) return null
    surface.empty?.remove()

    const item = document.createElement('div')
    item.className = `chat-item anime-fade-in${pending ? ' pending' : ''}`

    const bubble = document.createElement('div')
    bubble.className = role === 'user' ? 'chat-user' : 'chat-agent'

    const p = document.createElement('p')
    p.textContent = text || ''
    bubble.appendChild(p)
    item.appendChild(bubble)
    surface.chat.appendChild(item)
    scrollVoiceChat(mode)
    return { item, p }
}

function updateVoiceMessage(message, text, { pending = false, mode = voiceRuntime.mode || state.mode || 'voice' } = {}) {
    if (!message) return
    message.item.classList.toggle('pending', pending)
    message.p.textContent = text || ''
    scrollVoiceChat(mode)
}

function setVoiceContextLevel(level, mode = voiceRuntime.mode || state.mode || 'voice') {
    const next = Math.max(0, Math.min(8, Number(level) || 0))
    const contextLevel = voiceSurface(mode).contextLevel
    contextLevel?.classList.remove(...contextIconClasses)
    contextLevel?.classList.add(`ico-circular-pie-${next}`)
}

async function updateVoiceContextLevel(agentId = state.agent, mode = voiceRuntime.mode || state.mode || 'voice') {
    if (!agentId) {
        setVoiceContextLevel(0, mode)
        return
    }
    try {
        const stats = await bridge.get(`/api/agents/${encodeURIComponent(agentId)}/stats`)
        const used = Math.max(0, Number(stats?.estimated_context_tokens) || 0)
        const max = Math.max(1, Number(stats?.max_context_tokens) || 8000)
        setVoiceContextLevel(Math.ceil((used / max) * 8), mode)
    } catch {
        setVoiceContextLevel(state.contextLevel, mode)
    }
}

function stopVoiceRecording({ discard = false } = {}) {
    voiceRuntime.discardRecording = voiceRuntime.discardRecording || discard
    clearTimeout(voiceRuntime.recordTimer)
    voiceRuntime.recordTimer = 0
    if (voiceRuntime.recorder && !voiceRuntime.recorder.stopped) {
        voiceRuntime.recorder.stop()
    }
}

async function startVoiceRecording(mode = voiceRuntime.mode || state.mode || 'voice', runId = voiceRuntime.runId, { durationMs = VOICE_RECORD_MAX_MS, quietEmpty = false } = {}) {
    if (voiceRuntime.recording || voiceRuntime.processing) return

    const stream = await ensureVoiceStream()
    const recorder = createVoiceWavRecorder(stream)
    voiceRuntime.recorder = recorder
    voiceRuntime.chunks = []
    voiceRuntime.discardRecording = false
    voiceRuntime.recording = true
    setVoiceStatus('录音中', mode)
    setVoiceIndicator(true, mode)
    setVoiceUiState(mode)

    const finished = recorder.finished.finally(() => {
        clearTimeout(voiceRuntime.recordTimer)
        voiceRuntime.recordTimer = 0
        voiceRuntime.recording = false
        voiceRuntime.recorder = null
        setVoiceIndicator(false, mode)
        setVoiceUiState(mode)
    })

    if (durationMs > 0) {
        voiceRuntime.recordTimer = setTimeout(() => stopVoiceRecording(), durationMs)
    }

    try {
        const blob = await finished
        if (voiceRuntime.discardRecording || !isVoiceRunActive(runId, mode)) {
            setVoiceStatus(mode === 'frenzy' ? '准备暴走' : '准备就绪', mode)
            return
        }
        if (!blob.size) {
            setVoiceStatus('未录到声音', mode)
            return
        }
        await processVoiceBlob(blob, 'audio/wav', mode, runId, { quietEmpty })
    } finally {
        clearTimeout(voiceRuntime.recordTimer)
        voiceRuntime.recordTimer = 0
        voiceRuntime.recording = false
        voiceRuntime.recorder = null
        voiceRuntime.chunks = []
        voiceRuntime.discardRecording = false
        setVoiceIndicator(false, mode)
        setVoiceUiState(mode)
    }
}

function createVoiceWavRecorder(stream) {
    const AudioContext = window.AudioContext || window.webkitAudioContext
    if (!AudioContext) {
        throw new Error('当前浏览器不支持音频采集')
    }

    const context = new AudioContext()
    const source = context.createMediaStreamSource(stream)
    const processor = context.createScriptProcessor(4096, 1, 1)
    const chunks = []
    const inputSampleRate = context.sampleRate
    let stopped = false

    processor.onaudioprocess = event => {
        chunks.push(new Float32Array(event.inputBuffer.getChannelData(0)))
        event.outputBuffer.getChannelData(0).fill(0)
    }
    source.connect(processor)
    processor.connect(context.destination)

    let finishRecording
    const finished = new Promise((resolve, reject) => {
        finishRecording = async () => {
            if (stopped) return
            stopped = true
            try {
                processor.disconnect()
                source.disconnect()
                await context.close?.()
                const samples = mergeAudioChunks(chunks)
                if (!samples.length) {
                    resolve(new Blob([], { type: 'audio/wav' }))
                    return
                }
                const pcm16k = resampleAudio(samples, inputSampleRate, 16000)
                resolve(encodeWav(pcm16k, 16000))
            } catch (error) {
                reject(error)
            }
        }
    })

    return {
        get stopped() {
            return stopped
        },
        stop() {
            finishRecording()
        },
        finished,
    }
}

async function toggleVoiceRecording(mode) {
    if (!isVoiceLikeMode(mode)) return

    if (voiceRuntime.recording && voiceRuntime.mode === mode) {
        stopVoiceRecording()
        return
    }
    if (voiceRuntime.processing || voiceRuntime.recordTask) {
        showNotice('语音处理中')
        return
    }

    if (state.mode !== mode) {
        await requestMode(mode)
        if (state.mode !== mode) return
    }

    clearFrenzyProactive()
    const runId = voiceRuntime.runId
    const task = startVoiceRecording(mode, runId, {
        durationMs: VOICE_RECORD_MAX_MS,
        quietEmpty: false,
    })
    voiceRuntime.recordTask = task
    task.catch(error => {
        if (isVoiceRunActive(runId, mode)) {
            setVoiceStatus(mode === 'frenzy' ? '准备暴走' : '准备就绪', mode)
            showNotice(error.message || '录音失败')
        }
    }).finally(() => {
        if (voiceRuntime.recordTask === task) voiceRuntime.recordTask = null
    })
}

async function processVoiceBlob(blob, mimeType, mode = voiceRuntime.mode || state.mode || 'voice', runId = voiceRuntime.runId, { quietEmpty = false } = {}) {
    const agentId = await ensureActiveAgent()
    let userMessage = quietEmpty ? null : appendVoiceMessage('user', '识别中...', { pending: true, mode })
    let shouldScheduleFrenzy = !quietEmpty
    voiceRuntime.processing = true
    voiceRuntime.requestController = new AbortController()
    setVoiceStatus('识别中', mode)
    setVoiceUiState(mode)

    try {
        const ext = extensionForAudio(mimeType || blob.type)
        const stt = await bridge.upload(`/api/agents/${encodeURIComponent(agentId)}/stt?ext=${ext}`, blob, {
            contentType: mimeType || blob.type || 'audio/webm',
            signal: voiceRuntime.requestController.signal,
        })
        applyCredit(stt)
        if (!isVoiceRunActive(runId, mode)) return

        const transcript = String(stt?.text || '').trim()
        if (!transcript) {
            if (!quietEmpty) updateVoiceMessage(userMessage, '未识别到内容', { mode })
            setVoiceStatus(mode === 'frenzy' ? '准备暴走' : '准备就绪', mode)
            return
        }
        shouldScheduleFrenzy = true
        if (!userMessage) userMessage = appendVoiceMessage('user', transcript, { mode })
        updateVoiceMessage(userMessage, transcript, { mode })

        if (isVoiceStopCommand(transcript)) {
            setVoiceStatus('正在关闭', mode)
            setVoiceIndicator(false, mode)
            await requestVoiceModeStop(mode, runId)
            return
        }

        const pending = appendVoiceMessage('agent', '...', { pending: true, mode })
        setVoiceStatus('思考中', mode)
        await updateVoiceContextLevel(agentId, mode)
        const payload = {
            message: transcript,
            save: true,
        }
        if (mode === 'frenzy') payload.system_overlay = FRENZY_SYSTEM_OVERLAY
        const chat = await bridge.post(`/api/agents/${encodeURIComponent(agentId)}/chat`, {
            ...payload,
        }, {
            signal: voiceRuntime.requestController.signal,
        })
        if (!isVoiceRunActive(runId, mode)) return

        const response = chat?.response || '（无内容）'
        updateVoiceMessage(pending, response, { mode })
        applyCredit(chat)
        await updateVoiceContextLevel(agentId, mode)

        setVoiceStatus('播放中', mode)
        const audio = await bridge.post(`/api/agents/${encodeURIComponent(agentId)}/tts`, {
            text: response,
        }, {
            expect: 'blob',
            signal: voiceRuntime.requestController.signal,
        })
        if (!isVoiceRunActive(runId, mode)) return
        await playVoiceAudio(audio, runId, mode)
        if (isVoiceRunActive(runId, mode)) {
            setVoiceStatus(mode === 'frenzy' ? '准备暴走' : '准备就绪', mode)
            setVoiceIndicator(false, mode)
        }
    } catch (error) {
        if (error?.name === 'AbortError' || !isVoiceRunActive(runId, mode)) return
        updateVoiceMessage(userMessage, API_SEND_ERROR, { mode })
        setVoiceStatus(mode === 'frenzy' ? '准备暴走' : '准备就绪', mode)
        setVoiceIndicator(false, mode)
        showNotice(API_SEND_ERROR)
    } finally {
        if (voiceRuntime.runId === runId) {
            voiceRuntime.processing = false
            voiceRuntime.requestController = null
            setVoiceUiState(mode)
            if (shouldScheduleFrenzy && mode === 'frenzy' && isVoiceRunActive(runId, mode)) scheduleFrenzyProactive({ runId })
        }
    }
}

async function playVoiceAudio(blob, runId = voiceRuntime.runId, mode = voiceRuntime.mode || state.mode || 'voice') {
    if (!blob?.size) return
    stopVoicePlayback()
    const url = URL.createObjectURL(blob)
    const audio = new Audio(url)
    voiceRuntime.playbackAudio = audio
    voiceRuntime.playbackUrl = url

    try {
        await new Promise((resolve, reject) => {
            voiceRuntime.playbackDone = resolve
            audio.onended = resolve
            audio.onerror = () => reject(new Error('语音播放失败'))
            if (!isVoiceRunActive(runId, mode)) {
                resolve()
                return
            }
            audio.play().catch(reject)
        })
    } finally {
        stopVoicePlayback()
    }
}

function stopVoicePlayback() {
    if (voiceRuntime.playbackAudio) {
        voiceRuntime.playbackAudio.pause()
        voiceRuntime.playbackAudio.removeAttribute('src')
        voiceRuntime.playbackAudio.load?.()
    }
    if (voiceRuntime.playbackUrl) URL.revokeObjectURL(voiceRuntime.playbackUrl)
    voiceRuntime.playbackDone?.()
    voiceRuntime.playbackAudio = null
    voiceRuntime.playbackUrl = ''
    voiceRuntime.playbackDone = null
}

function clearFrenzyProactive() {
    clearTimeout(voiceRuntime.proactiveTimer)
    voiceRuntime.proactiveTimer = 0
}

function frenzyProactiveMessage() {
    const pool = Math.random() < 0.28 ? FRENZY_SINGING_MESSAGES : FRENZY_PROACTIVE_MESSAGES
    return pool[Math.floor(Math.random() * pool.length)]
}

function scheduleFrenzyProactive({ delayMs, runId = voiceRuntime.runId } = {}) {
    clearFrenzyProactive()
    if (state.mode !== 'frenzy' || voiceRuntime.mode !== 'frenzy' || voiceRuntime.closing) return
    const wait = Number.isFinite(delayMs)
        ? delayMs
        : FRENZY_MIN_DELAY_MS + Math.floor(Math.random() * (FRENZY_MAX_DELAY_MS - FRENZY_MIN_DELAY_MS))

    voiceRuntime.proactiveTimer = setTimeout(async () => {
        voiceRuntime.proactiveTimer = 0
        if (!isVoiceRunActive(runId, 'frenzy')) return
        if (voiceRuntime.processing) {
            scheduleFrenzyProactive({ delayMs: 3000, runId })
            return
        }
        if (voiceRuntime.recording) {
            scheduleFrenzyProactive({ delayMs: 3000, runId })
            return
        }
        if (!isVoiceRunActive(runId, 'frenzy') || voiceRuntime.processing) return
        const task = runFrenzyProactive(runId).catch(error => {
            if (isVoiceRunActive(runId, 'frenzy')) {
                setVoiceStatus('准备暴走', 'frenzy')
                showNotice(error.message || '主动发言失败')
            }
        })
        voiceRuntime.task = task
        task.finally(() => {
            if (voiceRuntime.task === task) voiceRuntime.task = null
        })
    }, wait)
}

async function runFrenzyProactive(runId = voiceRuntime.runId) {
    if (!isVoiceRunActive(runId, 'frenzy')) return
    const mode = 'frenzy'
    const pending = appendVoiceMessage('agent', '...', { pending: true, mode })
    voiceRuntime.processing = true
    voiceRuntime.requestController = new AbortController()
    setVoiceStatus('主动发言中', mode)
    setVoiceUiState(mode)

    try {
        const agentId = await ensureActiveAgent()
        const chat = await bridge.post(`/api/agents/${encodeURIComponent(agentId)}/chat`, {
            message: frenzyProactiveMessage(),
            save: false,
            system_overlay: FRENZY_SYSTEM_OVERLAY,
        }, {
            signal: voiceRuntime.requestController.signal,
        })
        if (!isVoiceRunActive(runId, mode)) return

        const response = chat?.response || '（无内容）'
        updateVoiceMessage(pending, response, { mode })

        setVoiceStatus('播放中', mode)
        const audio = await bridge.post(`/api/agents/${encodeURIComponent(agentId)}/tts`, {
            text: response,
        }, {
            expect: 'blob',
            signal: voiceRuntime.requestController.signal,
        })
        if (!isVoiceRunActive(runId, mode)) return
        await playVoiceAudio(audio, runId, mode)
        if (isVoiceRunActive(runId, mode)) setVoiceStatus('准备暴走', mode)
    } catch (error) {
        if (error?.name === 'AbortError' || !isVoiceRunActive(runId, mode)) return
        updateVoiceMessage(pending, API_SEND_ERROR, { mode })
        setVoiceStatus('准备暴走', mode)
        showNotice(API_SEND_ERROR)
    } finally {
        if (voiceRuntime.runId === runId) {
            voiceRuntime.processing = false
            voiceRuntime.requestController = null
            setVoiceUiState(mode)
            if (isVoiceRunActive(runId, mode)) scheduleFrenzyProactive({ runId })
        }
    }
}

Object.entries(voiceSurfaces).forEach(([mode, surface]) => {
    surface.indicator?.addEventListener('click', e => {
        e.preventDefault()
        toggleVoiceRecording(mode).catch(error => showNotice(error.message || '录音失败'))
    })
    surface.indicator?.addEventListener('keydown', e => {
        if (e.key !== 'Enter' && e.key !== ' ') return
        e.preventDefault()
        toggleVoiceRecording(mode).catch(error => showNotice(error.message || '录音失败'))
    })
})

// 历史记录
const historyIndex = $('#history-index')
const historyChat = $('#history-chat-cont')
let historyPanelLoaded = false
let historyMonths = []
let historySelected = ''

async function initHistoryPanel(force = false) {
    if (historyPanelLoaded && !force) return
    try {
        await withLoad(contEl('history'), loadHistoryIndex)
        historyPanelLoaded = true
    } catch (error) {
        if (historyIndex) historyIndex.innerHTML = '<aside>历史加载失败</aside>'
        if (historyChat) historyChat.innerHTML = '<empty>empty</empty>'
        showNotice(error.message || '历史加载失败')
    } finally {
        renderPanel()
    }
}

async function loadHistoryIndex() {
    const agentId = await ensureActiveAgent()
    const data = await bridge.get(`/api/agents/${encodeURIComponent(agentId)}/history`)
    historyMonths = data?.months || []
    historySelected = ''
    renderHistoryIndex()
    if (historyChat) {
        delete historyChat.dataset.date
        historyChat.innerHTML = '<empty>empty</empty>'
    }
}

function renderHistoryIndex() {
    if (!historyIndex) return
    if (!historyMonths.length) {
        historyIndex.innerHTML = '<aside>暂无历史</aside>'
        return
    }

    historyIndex.innerHTML = historyMonths.map((month, index) => {
        const monthLabel = escapeHtml(month.month)
        const days = (month.days || []).map(day => {
            const active = day.date === historySelected ? ' class="active"' : ''
            return `<a${active} data-history-date="${escapeHtml(day.date)}">${escapeHtml(day.day)}</a>`
        }).join('')
        const open = (month.days || []).some(day => day.date === historySelected) || (!historySelected && index === 0)
        return `<fold-group class="${open ? 'active' : ''}">
            <fold-title><h5>${monthLabel}</h5><u></u></fold-title>
            <fold-cont><div class="history-list">${days}</div></fold-cont>
        </fold-group>`
    }).join('')
    delete historyIndex._uiggElement
    Uigg.init?.(historyIndex)
}

function setHistoryActive(date) {
    historyIndex?.querySelectorAll('[data-history-date]').forEach(item => {
        item.classList.toggle('active', item.dataset.historyDate === date)
    })
}

async function selectHistoryDay(date) {
    if (!date || date === historySelected && historyChat?.dataset.date === date) return
    historySelected = date
    setHistoryActive(date)
    await loadHistoryDay(date)
}

async function loadHistoryDay(date) {
    const agentId = await ensureActiveAgent()
    if (historyChat) {
        historyChat.dataset.date = date
        historyChat.innerHTML = '<load></load>'
    }
    const data = await bridge.get(`/api/agents/${encodeURIComponent(agentId)}/history/${encodeURIComponent(date)}`)
    renderHistoryDay(data?.history || {})
}

function renderHistoryDay(history) {
    if (!historyChat) return
    const entries = history.entries || []
    historyChat.dataset.date = history.date || ''
    if (!entries.length) {
        historyChat.innerHTML = '<empty>empty</empty>'
        return
    }

    historyChat.innerHTML = entries.map(entry => {
        const role = entry.role === 'user' ? 'chat-user' : 'chat-agent'
        return `<div class="chat-item anime-fade-in">
            <div class="${role}"><p>${escapeHtml(entry.content || '')}</p></div>
        </div>`
    }).join('')
    historyChat.scrollTop = historyChat.scrollHeight
}

historyIndex?.addEventListener('click', e => {
    const item = e.target.closest('[data-history-date]')
    if (!item) return
    e.preventDefault()
    selectHistoryDay(item.dataset.historyDate).catch(error => showNotice(error.message || '历史读取失败'))
})

// 大模型管理
const modelList = $('#model-list')
const modelAdd = $('#model-add')
const modelCancel = $('#model-cancel')
const modelSave = $('#model-save')
const modelBox = $('.model-box')
const modelForm = $('#model-form')
const modelName = $('#model-name')
const modelModel = $('#model-model')
const modelApiBase = $('#model-api-base')
const modelApiKey = $('#model-api-key')
let modelPanelLoaded = false
let models = []

async function initModelPanel(force = false) {
    if (modelPanelLoaded && !force) return
    try {
        await withLoad(contEl('model'), loadModels)
        modelPanelLoaded = true
    } catch (error) {
        if (modelList) modelList.innerHTML = '<aside>大模型加载失败</aside>'
        showNotice(error.message || '大模型加载失败')
    } finally {
        renderPanel()
    }
}

async function loadModels() {
    const data = await bridge.get('/api/models')
    models = (data?.models || []).map(normalizeModel)
    state.model = data?.selected || models.find(item => item.selected)?.id || models[0]?.id || null
    renderModelList()
    return models
}

function normalizeModel(model) {
    const id = model.id || model.model || model.name || ''
    return {
        ...model,
        id,
        display_name: model.display_name || model.name || model.model || id,
        can_edit: model.can_edit !== false,
        can_delete: model.can_delete !== false,
    }
}

function renderModelList() {
    if (!modelList) return
    if (!models.length) {
        modelList.innerHTML = '<aside>暂无大模型</aside>'
        return
    }

    modelList.innerHTML = models.map(model => {
        const id = escapeHtml(model.id)
        const label = escapeHtml(model.display_name || model.name || model.model || model.id)
        const active = model.id === state.model ? ' class="active"' : ''
        const deleteAction = model.can_delete ? '<i class="ico ico-delete model-delete" data-model-action="delete"></i>' : ''
        const editAction = model.can_edit ? '<i class="ico ico-edit model-edit" data-model-action="edit"></i>' : ''
        return `<li><a${active} data-model-id="${id}">
            ${deleteAction}
            ${editAction}
            <span>${label}</span>
            <i class="ico radio" data-model-action="select"></i>
        </a></li>`
    }).join('')
    bindModelListActions()
}

function bindModelListActions() {
    modelList?.querySelectorAll('a[data-model-id]').forEach(row => {
        const modelId = row.dataset.modelId
        row.addEventListener('click', e => {
            e.preventDefault()
            selectModel(modelId).catch(error => showNotice(error.message || '大模型切换失败'))
        })

        row.querySelector('[data-model-action="delete"]')?.addEventListener('click', e => {
            e.preventDefault()
            e.stopPropagation()
            deleteModel(modelId).catch(error => showNotice(error.message || '大模型删除失败'))
        })

        row.querySelector('[data-model-action="edit"]')?.addEventListener('click', e => {
            e.preventDefault()
            e.stopPropagation()
            editModel(modelId)
        })

        row.querySelector('[data-model-action="select"]')?.addEventListener('click', e => {
            e.preventDefault()
            e.stopPropagation()
            selectModel(modelId).catch(error => showNotice(error.message || '大模型切换失败'))
        })
    })
}

function setModelFormVisible(visible) {
    if (modelBox) modelBox.style.display = visible ? 'block' : 'none'
    if (modelAdd) modelAdd.style.display = visible ? 'none' : 'block'
}

function cancelModelEdit() {
    if (modelForm) modelForm.dataset.modelId = ''
    setModelFormVisible(false)
}

function editModel(modelId) {
    const model = models.find(item => item.id === modelId)
    if (!model || !modelForm) return
    if (model.can_edit === false) {
        showNotice('云端大模型不能编辑')
        return
    }
    modelForm.dataset.modelId = model.id
    if (modelName) modelName.value = model.name || model.display_name || ''
    if (modelModel) modelModel.value = model.model || ''
    if (modelApiBase) modelApiBase.value = model.api_base || ''
    if (modelApiKey) modelApiKey.value = model.api_key || ''
    setModelFormVisible(true)
}

function newModel() {
    if (!modelForm) return
    modelForm.dataset.modelId = ''
    if (modelName) modelName.value = ''
    if (modelModel) modelModel.value = ''
    if (modelApiBase) modelApiBase.value = ''
    if (modelApiKey) modelApiKey.value = ''
    setModelFormVisible(true)
}

async function saveModel() {
    const editingId = modelForm?.dataset.modelId || ''
    const payload = {
        name: modelName?.value || '',
        model: modelModel?.value || '',
        api_base: modelApiBase?.value || '',
        api_key: modelApiKey?.value || '',
    }
    if (!payload.name.trim() && !payload.model.trim()) {
        showNotice('请填写模型名称')
        return
    }

    const path = editingId ? `/api/models/${encodeURIComponent(editingId)}` : '/api/models'
    const data = await bridge.post(path, payload)
    models = (data?.models || []).map(normalizeModel)
    state.model = data?.selected || state.model
    renderModelList()
    setModelFormVisible(false)
    showNotice('大模型已保存')
}

async function selectModel(modelId) {
    if (!modelId) return
    if (modelId === state.model) {
        showNotice('已是当前大模型')
        return
    }
    await runRuntimeSetting('切换大模型', async () => {
        const data = await bridge.post(`/api/models/${encodeURIComponent(modelId)}/select`, {})
        state.model = data?.selected || modelId
        models.forEach(item => { item.selected = item.id === state.model })
        if (data?.agent) upsertAgent(data.agent)
        renderModelList()
        showNotice('大模型已切换')
    })
}

async function deleteModel(modelId) {
    if (!modelId) return
    const model = models.find(item => item.id === modelId)
    if (model?.can_delete === false) {
        showNotice('云端大模型不能删除')
        return
    }
    const ok = await Uigg.confirm(`删除大模型“${model?.display_name || modelId}”？`)
    if (!ok) return

    const action = modelId === state.model ? '删除当前大模型' : '删除大模型'
    await runRuntimeSetting(action, async () => {
        const data = await bridge.remove(`/api/models/${encodeURIComponent(modelId)}`)
        models = (data?.models || []).map(normalizeModel)
        state.model = data?.selected || models.find(item => item.selected)?.id || models[0]?.id || null
        if (data?.agent) upsertAgent(data.agent)
        if (modelForm?.dataset.modelId === modelId) setModelFormVisible(false)
        renderModelList()
        showNotice('大模型已删除')
    })
}

modelAdd?.addEventListener('click', function (e) {
    e.preventDefault()
    newModel()
})
modelCancel?.addEventListener('click', function (e) {
    e.preventDefault()
    cancelModelEdit()
})
modelSave?.addEventListener('click', function (e) {
    e.preventDefault()
    saveModel().catch(error => showNotice(error.message || '大模型保存失败'))
})

// 智能体管理
const agentList = $('#agent-list')
const agentAdd = $('#agent-add')
const agentImport = $('#agent-import')
const agentImportFile = $('#agent-import-file')
const agentForm = $('.agent-form')
const agentCancel = $('#agent-cancel')
const agentSave = $('#agent-save')
const agentName = $('#agent-name')
const agentDescription = $('#agent-description')
const agentVoice = $('#agent-voice')
const agentTtsRate = $('#agent-tts-rate')
const agentTtsRateValue = $('#agent-tts-rate-value')
const agentIdentity = $('#agent-identity')
const agentOwnerName = $('#agent-owner-name')
const agentRelationship = $('#agent-relationship')
let agentPanelLoaded = false
let agents = []

async function initAgentPanel(force = false) {
    if (agentPanelLoaded && !force) return
    try {
        await withLoad(contEl('agent'), loadAgents)
        agentPanelLoaded = true
    } catch (error) {
        if (agentList) agentList.innerHTML = '<aside>智能体加载失败</aside>'
        showNotice(error.message || '智能体加载失败')
    } finally {
        renderPanel()
    }
}

async function loadAgents() {
    const data = await bridge.get('/api/agents')
    agents = (data?.agents || []).map(normalizeAgent)
    state.agent = data?.selected || agents.find(item => item.selected)?.id || agents[0]?.id || null
    renderAgentList()
    return agents
}

function normalizeAgent(agent) {
    const id = agent.id || agent.name || ''
    return {
        ...agent,
        id,
        display_name: agent.display_name || agent.name || id,
        can_edit: agent.can_edit !== false,
        can_delete: agent.can_delete !== false,
        can_export: agent.can_export !== false,
    }
}

function renderAgentList() {
    if (!agentList) return
    if (!agents.length) {
        agentList.innerHTML = '<aside>暂无智能体</aside>'
        return
    }

    agentList.innerHTML = agents.map(agent => {
        const id = escapeHtml(agent.id)
        const label = escapeHtml(agent.display_name || agent.name || agent.id)
        const classes = [agent.id === state.agent ? 'active' : ''].filter(Boolean)
        const classAttr = classes.length ? ` class="${classes.join(' ')}"` : ''
        const deleteAction = agent.can_delete ? '<i class="ico ico-delete agent-delete" data-agent-action="delete"></i>' : ''
        const exportAction = agent.can_export ? '<i class="ico ico-arrow-download agent-export" data-agent-action="export"></i>' : ''
        const editAction = agent.can_edit ? '<i class="ico ico-edit agent-edit" data-agent-action="edit"></i>' : ''
        return `<li><a${classAttr} data-agent-id="${id}">
            ${deleteAction}
            ${exportAction}
            ${editAction}
            <span>${label}</span>
            <i class="ico radio" data-agent-action="select"></i>
        </a></li>`
    }).join('')
    bindAgentListActions()
}

function upsertAgent(agent) {
    const next = normalizeAgent(agent || {})
    if (!next.id) return

    const index = agents.findIndex(item => item.id === next.id)
    if (index >= 0) {
        agents[index] = { ...agents[index], ...next }
    } else {
        agents.push(next)
    }
    if (next.selected) state.agent = next.id
    renderAgentList()
}

function bindAgentListActions() {
    agentList?.querySelectorAll('a[data-agent-id]').forEach(row => {
        const agentId = row.dataset.agentId
        row.addEventListener('click', e => {
            e.preventDefault()
            selectAgent(agentId).catch(error => showNotice(error.message || '智能体切换失败'))
        })

        row.querySelector('[data-agent-action="delete"]')?.addEventListener('click', e => {
            e.preventDefault()
            e.stopPropagation()
            deleteAgent(agentId).catch(error => showNotice(error.message || '智能体删除失败'))
        })

        row.querySelector('[data-agent-action="export"]')?.addEventListener('click', e => {
            e.preventDefault()
            e.stopPropagation()
            exportAgent(agentId)
        })

        row.querySelector('[data-agent-action="edit"]')?.addEventListener('click', e => {
            e.preventDefault()
            e.stopPropagation()
            editAgent(agentId)
        })

        row.querySelector('[data-agent-action="select"]')?.addEventListener('click', e => {
            e.preventDefault()
            e.stopPropagation()
            selectAgent(agentId).catch(error => showNotice(error.message || '智能体切换失败'))
        })
    })
}

function setAgentFormVisible(visible) {
    if (agentForm) agentForm.style.display = visible ? 'block' : 'none'
    if (agentImport) agentImport.style.display = visible ? 'none' : 'flex'
    if (agentAdd) agentAdd.style.display = visible ? 'none' : ''
}

function cancelAgentEdit() {
    if (agentForm) agentForm.dataset.agentId = ''
    setAgentFormVisible(false)
}

function keepAgentPanelOpen({ closeForm = false } = {}) {
    state.panel = 'agent'
    renderPanel()
    if (closeForm) setAgentFormVisible(false)
}

function setSelectValue(select, value, fallback = '') {
    if (!select) return
    const selectedValue = value || fallback
    ;[...select.options].forEach(option => {
        if (ttsVoiceLabels[option.value]) option.textContent = ttsVoiceLabels[option.value]
    })
    if (selectedValue && ![...select.options].some(option => option.value === selectedValue)) {
        select.appendChild(new Option(ttsVoiceLabels[selectedValue] || selectedValue, selectedValue))
    }
    select.value = selectedValue
}

function normalizeTtsRateValue(value) {
    const parsed = Number(String(value ?? '0').replace('%', ''))
    return Math.max(-50, Math.min(50, Number.isFinite(parsed) ? Math.round(parsed) : 0))
}

function formatTtsRate(value) {
    const rate = normalizeTtsRateValue(value)
    return `${rate >= 0 ? '+' : ''}${rate}%`
}

function setTtsRateControl(value) {
    const rate = normalizeTtsRateValue(value)
    if (agentTtsRate) {
        agentTtsRate.value = String(rate)
        agentTtsRate.style.setProperty('--value', `${rate + 50}%`)
    }
    if (agentTtsRateValue) agentTtsRateValue.textContent = formatTtsRate(rate)
}

async function selectAgent(agentId) {
    if (!agentId) return
    if (agentId === state.agent) {
        showNotice('已是当前智能体')
        return
    }
    await runRuntimeSetting('切换智能体', async () => {
        const data = await bridge.post(`/api/agents/${encodeURIComponent(agentId)}/select`, {})
        state.agent = data?.selected || agentId
        invalidateAgentScopedViews()
        agents.forEach(item => { item.selected = item.id === state.agent })
        renderAgentList()
        showNotice('智能体已切换')
    })
}

async function editAgent(agentId) {
    if (!agentId) return
    try {
        const data = await bridge.get(`/api/agents/${encodeURIComponent(agentId)}/config`)
        const agent = data?.agent || {}
        agentForm.dataset.agentId = agent.id || agentId
        if (agentName) agentName.value = agent.name || ''
        if (agentDescription) agentDescription.value = agent.description || ''
        setSelectValue(agentVoice, agent.voice?.tts_voice, 'zh-CN-YunxiNeural')
        setTtsRateControl(agent.voice?.tts_rate || '+0%')
        if (agentIdentity) agentIdentity.value = agent.identity || ''
        if (agentOwnerName) agentOwnerName.value = agent.owner_name || ''
        if (agentRelationship) agentRelationship.value = agent.relationship || ''
        setAgentFormVisible(true)
    } catch (error) {
        showNotice(error.message || '智能体设定加载失败')
    }
}

async function saveAgent() {
    const agentId = agentForm?.dataset.agentId
    if (!agentId) return

    let saved = false
    try {
        const ok = await confirmCloseFor('保存智能体设定')
        if (!ok) return
        if (state.mode) {
            const closed = await closeMode({ ask: false })
            if (!closed) return
        }

        const payload = {
            name: agentName?.value || '',
            description: agentDescription?.value || '',
            identity: agentIdentity?.value || '',
            owner_name: agentOwnerName?.value || '',
            relationship: agentRelationship?.value || '',
            voice: {
                tts_voice: agentVoice?.value || 'zh-CN-YunxiNeural',
                tts_rate: formatTtsRate(agentTtsRate?.value || 0),
            },
        }
        const data = await bridge.post(`/api/agents/${encodeURIComponent(agentId)}/config`, payload)
        upsertAgent(data?.agent)
        saved = true
        showNotice('智能体设定已保存')
    } finally {
        keepAgentPanelOpen({ closeForm: saved })
    }
}

async function deleteAgent(agentId) {
    if (!agentId) return
    const agent = agents.find(item => item.id === agentId)
    const ok = await Uigg.confirm(`删除智能体“${agent?.display_name || agentId}”？`)
    if (!ok) return

    await runRuntimeSetting('删除智能体', async () => {
        const data = await bridge.remove(`/api/agents/${encodeURIComponent(agentId)}?force=true`)
        state.agent = data?.selected || null
        invalidateAgentScopedViews()
        if (agentForm?.dataset.agentId === agentId) setAgentFormVisible(false)
        await loadAgents()
        showNotice('智能体已删除')
    })
}

async function exportAgent(agentId) {
    if (!agentId) return
    const blob = await bridge.get(`/api/agents/${encodeURIComponent(agentId)}/export`, {
        expect: 'blob',
    })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `${agentId}.coco-agent.zip`
    document.body.appendChild(a)
    a.click()
    a.remove()
    setTimeout(() => URL.revokeObjectURL(url), 1000)
}

async function importAgent(file) {
    if (!file) return
    await runRuntimeSetting('导入智能体', async () => {
        const data = await bridge.upload('/api/agents/import', file, {
            contentType: file.type || 'application/zip',
        })
        state.agent = data?.selected || data?.agent?.id || state.agent
        invalidateAgentScopedViews()
        setAgentFormVisible(false)
        await loadAgents()
        showNotice('智能体已导入')
    })
}

async function createAgent() {
    const name = (await Uigg.prompt('智能体名称'))?.trim()
    if (!name) return
    await runRuntimeSetting('创建智能体', async () => {
        const data = await bridge.post('/api/agents', { name })
        state.agent = data?.selected || data?.agent?.id || state.agent
        invalidateAgentScopedViews()
        setAgentFormVisible(false)
        await loadAgents()
        showNotice('智能体已创建')
    })
}

function setAgentImportDragging(active) {
    agentImport?.classList.toggle('dragging', active)
}

function firstDroppedFile(event) {
    return [...(event.dataTransfer?.files || [])][0] || null
}

agentList?.addEventListener('click', e => {
    const row = e.target.closest('a[data-agent-id]')
    if (!row) return
    e.preventDefault()

    const agentId = row.dataset.agentId
    const action = e.target.closest('[data-agent-action]')?.dataset.agentAction || 'select'
    if (action === 'delete') {
        deleteAgent(agentId).catch(error => showNotice(error.message || '智能体删除失败'))
        return
    }
    if (action === 'export') {
        exportAgent(agentId)
        return
    }
    if (action === 'edit') {
        editAgent(agentId)
        return
    }
    selectAgent(agentId).catch(error => showNotice(error.message || '智能体切换失败'))
})

agentImport?.addEventListener('click', e => {
    e.preventDefault()
    agentImportFile?.click()
})

agentAdd?.addEventListener('click', e => {
    e.preventDefault()
    createAgent().catch(error => showNotice(error.message || '智能体创建失败'))
})

;['dragenter', 'dragover'].forEach(type => {
    agentImport?.addEventListener(type, e => {
        e.preventDefault()
        e.stopPropagation()
        if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
        setAgentImportDragging(true)
    })
})

;['dragleave', 'dragend'].forEach(type => {
    agentImport?.addEventListener(type, e => {
        e.preventDefault()
        e.stopPropagation()
        setAgentImportDragging(false)
    })
})

agentImport?.addEventListener('drop', e => {
    e.preventDefault()
    e.stopPropagation()
    setAgentImportDragging(false)
    importAgent(firstDroppedFile(e)).catch(error => showNotice(error.message || '智能体导入失败'))
})

agentImportFile?.addEventListener('change', e => {
    const file = e.target.files?.[0]
    e.target.value = ''
    importAgent(file).catch(error => showNotice(error.message || '智能体导入失败'))
})

agentTtsRate?.addEventListener('input', () => {
    setTtsRateControl(agentTtsRate.value)
})

agentCancel?.addEventListener('click', e => {
    e.preventDefault()
    cancelAgentEdit()
})

agentSave?.addEventListener('pointerdown', e => {
    e.preventDefault()
    e.stopPropagation()
})

agentSave?.addEventListener('click', e => {
    e.preventDefault()
    e.stopImmediatePropagation()
    saveAgent().catch(error => showNotice(error.message || '智能体保存失败'))
}, true)

const color = $$('.color a')
const systemTextboxToggle = $('#system-textbox-toggle')
const hardwareDeviceList = $('#hardware-device-list')
const hardwareDiscoveredList = $('#hardware-discovered-list')
const hardwareRefresh = $('#hardware-refresh')
const hardwareStatus = $('#hardware-status')
const COLOR_COOKIE = 'coco-color'
const VOICE_TEXTBOX_COOKIE = 'coco-voice-textbox'
const COOKIE_HOURS = 24 * 365 * 5
const HARDWARE_POLL_INTERVAL_MS = 150
const HARDWARE_RECORDING_EVENT_WINDOW_MS = 1500
let hardwareState = null
let hardwarePolling = false
let hardwareLastDeviceId = ''
let hardwareLastMode = ''
let hardwareLastButtonSeq = 0
let hardwareLastRecording = false
let hardwareLastRecordingMode = ''
let hardwareRecordingEventSeq = 0
let hardwareRecordingEventValue = null
let hardwareRecordingEventAt = 0
let hardwareEventCursor = ''
let hardwareEventDeviceId = ''

function colorValue(el) {
    const bg = [...(el?.classList || [])].find(name => name.startsWith('bg-'))
    return bg ? bg.slice(3) : ''
}

function applyColorSetting(value, persist = false) {
    const selected = color.find(item => colorValue(item) === value) || color.find(item => colorValue(item) === 'default')
    const selectedValue = colorValue(selected) || 'default'

    document.documentElement.style.setProperty('--color', `var(--${selectedValue})`)
    color.forEach(item => item.classList.toggle('active', item === selected))
    if (persist) Uigg.setCookie(COLOR_COOKIE, selectedValue, COOKIE_HOURS)
}

function readVoiceTextboxCookie() {
    const value = Uigg.getCookie(VOICE_TEXTBOX_COOKIE)
    return value === '' ? true : value === '1'
}

function ensureToggleMethods(toggle) {
    if (toggle && typeof toggle.setData !== 'function') {
        Uigg.init(toggle)
    }
}

function applyVoiceTextboxSetting(enabled, persist = false) {
    ensureToggleMethods(systemTextboxToggle)
    systemTextboxToggle?.setData?.(enabled)
    ;['voice', 'frenzy'].forEach(name => {
        contEl(name)?.classList.toggle('textbox-hidden', !enabled)
    })
    if (persist) Uigg.setCookie(VOICE_TEXTBOX_COOKIE, enabled ? '1' : '0', COOKIE_HOURS)
}

function initSystemSettings() {
    applyColorSetting(Uigg.getCookie(COLOR_COOKIE) || 'default')
    applyVoiceTextboxSetting(readVoiceTextboxCookie())
    refreshHardwareState().catch(() => {})
}

function hardwareLabel(device = {}) {
    return device.name || device.display_name || device.id || '未命名设备'
}

function hardwareStatusIcon(device = {}) {
    return device.online
        ? '<i class="ico ico-plug-connect co-green hardware-status"></i>'
        : '<i class="ico ico-plug-disconnect co-ccc hardware-status"></i>'
}

function renderHardwareList(list, devices, emptyText, { discovered = false } = {}) {
    if (!list) return
    if (!devices?.length) {
        list.innerHTML = `<aside>${escapeHtml(emptyText)}</aside>`
        return
    }
    list.innerHTML = devices.map(device => {
        const classes = [
            device.selected ? 'active' : '',
            device.online === false ? 'offline' : '',
        ].filter(Boolean).join(' ')
        const classAttr = classes ? ` class="${classes}"` : ''
        return `
        <li>
            <a${classAttr} data-device-id="${escapeHtml(device.id)}" data-online="${device.online === false ? '0' : '1'}">
                ${discovered ? '' : '<i class="ico ico-delete" data-hardware-action="delete"></i>'}
                <span>${escapeHtml(hardwareLabel(device))}</span>
                ${hardwareStatusIcon(device)}
                ${discovered || device.online !== false ? `<i class="ico radio" data-hardware-action="${discovered ? 'pair' : 'select'}"></i>` : ''}
            </a>
        </li>
    `}).join('')
}

function hardwareEventKey(event = {}) {
    return [
        event.at || '',
        event.device_id || '',
        event.kind || '',
        event.message || '',
    ].join('|')
}

function syncHardwareEvents(events = [], device = null) {
    if (!Array.isArray(events) || !events.length || !device?.id) return
    const newestKey = hardwareEventKey(events[0])
    if (hardwareEventDeviceId !== device.id) {
        hardwareEventDeviceId = device.id
        hardwareEventCursor = newestKey
        return
    }
    const cursorIndex = events.findIndex(event => hardwareEventKey(event) === hardwareEventCursor)
    if (cursorIndex < 0) {
        hardwareEventCursor = newestKey
        return
    }

    const mode = uiModeForHardwareMode(device.mode)
        || (isVoiceLikeMode(state.mode) ? state.mode : 'voice')
    events.slice(0, cursorIndex).reverse().forEach(event => {
        if (event.device_id !== device.id) return
        const message = String(event.message || '').trim()
        if (!message) return
        if (event.kind === 'transcript') {
            appendVoiceMessage('user', message, { mode })
            setVoiceStatus('已收到语音', mode)
        } else if (event.kind === 'reply') {
            appendVoiceMessage('agent', message, { mode })
            setVoiceStatus('准备就绪', mode)
        } else if (event.kind === 'stt') {
            setVoiceStatus(message, mode)
        }
    })
    hardwareEventCursor = newestKey
}

function renderHardwareState(data = {}) {
    hardwareState = data
    const devices = data.devices || []
    const discovered = data.discovered || []
    const nextDeviceId = data.config?.selected_device_id || devices.find(item => item.selected)?.id || null
    const selectedDevice = devices.find(item => item.id === nextDeviceId) || devices.find(item => item.selected) || null
    state.hardwareDevice = nextDeviceId || selectedDevice?.id || null
    syncHardwareEvents(data.events, selectedDevice)
    syncHardwareButton(selectedDevice)
    syncHardwareMode(selectedDevice)
    syncHardwareRecording(selectedDevice)
    renderHardwareList(hardwareDeviceList, devices, '暂无已连接设备')
    renderHardwareList(hardwareDiscoveredList, discovered, '长按硬件 B 键 3 秒配网，连上 Wi-Fi 后会显示在这里', { discovered: true })
    if (hardwareStatus) {
        hardwareStatus.textContent = discovered.length ? `发现 ${discovered.length} 台` : '等待发现'
    }
}

function activeHardwareDevice() {
    const devices = hardwareState?.devices || []
    return devices.find(item => item.id === state.hardwareDevice) || devices.find(item => item.selected) || null
}

function uiModeForHardwareMode(mode) {
    if (mode === 'normal') return 'voice'
    if (mode === 'rage') return 'frenzy'
    return ''
}

function syncHardwareMode(device) {
    const deviceId = String(device?.id || '')
    const deviceMode = String(device?.mode || '')
    if (!deviceId) {
        hardwareLastDeviceId = ''
        hardwareLastMode = ''
        return
    }
    if (hardwareLastDeviceId !== deviceId) {
        hardwareLastDeviceId = deviceId
        hardwareLastMode = deviceMode
        return
    }
    if (!deviceMode || deviceMode === hardwareLastMode) return
    hardwareLastMode = deviceMode

    const nextMode = uiModeForHardwareMode(deviceMode)
    if (!nextMode || !state.loggedIn || state.modeChanging || state.mode === nextMode) return
    activateMode(nextMode).catch(error => showNotice(error.message || '硬件模式切换失败'))
}

function syncHardwareButton(device) {
    const deviceId = String(device?.id || '')
    const seq = Number(device?.button_seq || 0)
    if (!deviceId) {
        hardwareLastButtonSeq = 0
        hardwareRecordingEventSeq = 0
        hardwareRecordingEventValue = null
        hardwareRecordingEventAt = 0
        return
    }
    if (!seq || seq === hardwareLastButtonSeq) return
    hardwareLastButtonSeq = seq

    const button = String(device?.last_button || '').toUpperCase()
    const action = String(device?.last_button_action || '').toLowerCase()
    const nextMode = uiModeForHardwareMode(device?.mode)
    if ((button === 'C' || button === 'D') && action === 'down') {
        hardwareLastMode = String(device?.mode || hardwareLastMode)
        if (!nextMode || !state.loggedIn || state.modeChanging || state.mode === nextMode) return
        activateMode(nextMode).catch(error => showNotice(error.message || '硬件模式切换失败'))
        return
    }
    if (button !== 'E' || (action !== 'down' && action !== 'up')) return

    const recording = action === 'down'
    hardwareRecordingEventSeq = seq
    hardwareRecordingEventValue = recording
    hardwareRecordingEventAt = Date.now()
    syncHardwareRecording({ ...device, recording })
}

function syncHardwareRecording(device) {
    const deviceId = String(device?.id || '')
    const deviceSeq = Number(device?.button_seq || 0)
    const eventIsFresh = deviceSeq
        && deviceSeq === hardwareRecordingEventSeq
        && hardwareRecordingEventValue !== null
        && Date.now() - hardwareRecordingEventAt <= HARDWARE_RECORDING_EVENT_WINDOW_MS
    const recording = eventIsFresh ? hardwareRecordingEventValue : !!device?.recording
    const mode = uiModeForHardwareMode(device?.mode) || (isVoiceLikeMode(state.mode) ? state.mode : 'voice')
    if (!deviceId) {
        hardwareLastRecording = false
        hardwareLastRecordingMode = ''
        return
    }
    const changed = recording !== hardwareLastRecording || mode !== hardwareLastRecordingMode
    if (!changed) return
    const previousRecording = hardwareLastRecording
    hardwareLastRecording = recording
    hardwareLastRecordingMode = mode
    if (!state.loggedIn) return

    const applyRecordingUi = () => {
        if (hardwareLastRecordingMode !== mode) return
        const localRecording = voiceRuntime.recording
        if (hardwareLastRecording) {
            setVoiceStatus('录音中', mode)
            setVoiceIndicator(true, mode)
            setVoiceUiState(mode)
            return
        }
        if (!previousRecording) return
        setVoiceStatus(mode === 'frenzy' ? '准备暴走' : '准备就绪', mode)
        setVoiceIndicator(localRecording, mode)
        setVoiceUiState(mode)
    }

    if (recording && state.mode !== mode && !state.modeChanging) {
        activateMode(mode)
            .then(applyRecordingUi)
            .catch(error => showNotice(error.message || '硬件录音界面切换失败'))
        return
    }
    applyRecordingUi()
}

async function refreshHardwareState() {
    const data = await bridge.get('/api/hardware/state')
    renderHardwareState(data)
    return data
}

async function startHardwarePolling() {
    if (hardwarePolling) return
    hardwarePolling = true
    while (hardwarePolling) {
        try {
            await refreshHardwareState()
        } catch (error) {
            if (hardwareStatus) hardwareStatus.textContent = 'Hub 未连接'
        }
        await delay(HARDWARE_POLL_INTERVAL_MS)
    }
}

function stopHardwarePolling() {
    hardwarePolling = false
}

async function pairHardware(deviceId) {
    await bridge.post('/api/hardware/pair', { device_id: deviceId })
    showNotice('设备连接信息已发送')
    await refreshHardwareState()
}

async function selectHardware(deviceId) {
    if (!deviceId) return
    const device = (hardwareState?.devices || []).find(item => item.id === deviceId)
    if (device?.online === false) {
        showNotice('设备已断开')
        return
    }
    if (deviceId === state.hardwareDevice) {
        showNotice('已是当前设备')
        return
    }
    await bridge.post('/api/hardware/config', { selected_device_id: deviceId })
    state.hardwareDevice = deviceId
    showNotice('设备已切换')
    await refreshHardwareState()
}

async function deleteHardware(deviceId) {
    const ok = await Uigg.confirm(`删除设备“${deviceId}”？`)
    if (!ok) return
    await bridge.post('/api/hardware/delete', { device_id: deviceId })
    showNotice('设备已删除')
    await refreshHardwareState()
}

color.forEach(el => {
    el.addEventListener('click', function (e) {
        e.preventDefault()
        applyColorSetting(colorValue(this), true)
    })
})

systemTextboxToggle?.addEventListener('click', () => {
    ensureToggleMethods(systemTextboxToggle)
    applyVoiceTextboxSetting(systemTextboxToggle.getData(), true)
})

hardwareRefresh?.addEventListener('click', e => {
    e.preventDefault()
    refreshHardwareState().catch(error => showNotice(error.message || '硬件状态刷新失败'))
})

document.addEventListener('click', e => {
    const row = e.target.closest?.('.hardware-list a[data-device-id]')
    if (!row) return
    const action = e.target.closest?.('[data-hardware-action]')
    const deviceId = row?.dataset.deviceId || ''
    if (!deviceId) return
    e.preventDefault()
    const type = action?.dataset.hardwareAction || (row.closest('#hardware-discovered-list') ? 'pair' : 'select')
    if (type === 'pair') {
        pairHardware(deviceId).catch(error => showNotice(error.message || '设备连接失败'))
    } else if (type === 'delete') {
        deleteHardware(deviceId).catch(error => showNotice(error.message || '设备删除失败'))
    } else if (type === 'select') {
        selectHardware(deviceId).catch(error => showNotice(error.message || '设备切换失败'))
    }
})

const profilePassword = $('#profile-password')
const profileSave = $('#profile-save')
const userAccountName = $('.user-account span')
const userLogout = $('#user-logout')
const rechargeSubmit = $('#recharge-submit')
const loginEmail = $('#login-email')
const loginPassword = $('#login-password')
const loginSubmit = $('#login-submit')
const registerSubmit = $('#register-submit')
let userPanelLoaded = false

function authLabel(session = {}) {
    return session.email || session.display_name || session.account_id || 'local'
}

function applyAuthSession(session = {}) {
    state.loggedIn = !!session.authenticated
    state.accountId = session.account_id || 'local'
    state.accountEmail = session.email || ''
    state.accountName = session.display_name || ''
    creditUnloadSettlementSent = false
    if (userAccountName) userAccountName.textContent = authLabel(session)
    if (state.loggedIn && session.credits !== undefined) applyCredit(session)
    if (!state.loggedIn) {
        state.credits = null
        clearPendingRecharge()
        stopRechargePolling()
        renderCredit()
    }
}

function invalidateAccountScopedViews() {
    userPanelLoaded = false
    agentPanelLoaded = false
    modelPanelLoaded = false
    historyPanelLoaded = false
    textModeLoaded = false
    agents = []
    models = []
    state.agent = null
    state.model = null
    cancelAgentEdit()
    cancelModelEdit()
}

async function initAuthSession() {
    const data = await bridge.get('/api/auth/session')
    applyAuthSession(data?.session || {})
    if (state.loggedIn) {
        loadCreditBalance().catch(() => {})
        startRechargePolling({ immediate: true })
    }
    renderPanel()
}

function initLoginPanel() {
    window.setTimeout(() => loginEmail?.focus(), 0)
}

async function finishAuth(data) {
    applyAuthSession(data?.session || {})
    invalidateAccountScopedViews()
    const userCont = prepareCont(contEl('user'))
    state.panel = 'user'
    renderPanel()
    try {
        await loadUserProfile()
        await loadCreditBalance().catch(() => {})
    } finally {
        renderPanel()
        revealCont(userCont)
    }
    showNotice(data?.message || '登录成功')
}

async function submitAuth(kind) {
    const email = (loginEmail?.value || '').trim()
    const password = loginPassword?.value || ''
    if (!email || !password) {
        showNotice('请输入邮箱和密码')
        return
    }

    await runRuntimeSetting(kind === 'register' ? '注册账号' : '登录账号', async () => {
        await withLoad(contEl('login'), async () => {
            const path = kind === 'register' ? '/api/auth/register' : '/api/auth/login'
            const payload = kind === 'register'
                ? { email, password }
                : { account: email, password }
            const data = await bridge.post(path, payload)
            if (loginPassword) loginPassword.value = ''
            await finishAuth(data)
        })
    })
}

async function logoutUser() {
    const ok = await Uigg.confirm('退出当前账号？')
    if (!ok) return
    await runRuntimeSetting('退出登录', async () => {
        await settleCredit({ apply: false })
        const data = await bridge.post('/api/auth/logout', {})
        applyAuthSession(data?.session || {})
        state.panel = 'login'
        renderPanel()
        invalidateAccountScopedViews()
        state.credits = null
        clearPendingRecharge()
        stopRechargePolling()
        renderCredit()
        initLoginPanel()
        showNotice('已退出登录')
    })
}

async function initUserPanel(force = false) {
    if (userPanelLoaded && !force) return
    try {
        await withLoad(contEl('user'), async () => {
            await loadUserProfile()
            await loadCreditBalance().catch(() => {})
            startRechargePolling({ immediate: true })
        })
        userPanelLoaded = true
    } catch (error) {
        showNotice(error.message || '用户信息加载失败')
    } finally {
        renderPanel()
    }
}

async function loadUserProfile() {
    const data = await bridge.get('/api/user/profile')
    const profile = data?.profile || {}
    applyAuthSession(data?.session || {
        authenticated: state.loggedIn,
        account_id: profile.account_id || state.accountId,
        email: state.accountEmail,
        display_name: state.accountName,
    })
    state.accountId = profile.account_id || 'local'
    return profile
}

async function saveUserProfile() {
    const payload = {}
    await withLoad(contEl('user'), async () => {
        await bridge.post('/api/user/profile', payload)
        if (profilePassword?.value) {
            profilePassword.value = ''
            showNotice('密码修改暂未开放')
            return
        }
        showNotice('资料已保存')
    })
}

function mergeAudioChunks(chunks) {
    const length = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
    const result = new Float32Array(length)
    let offset = 0
    chunks.forEach(chunk => {
        result.set(chunk, offset)
        offset += chunk.length
    })
    return result
}

function resampleAudio(samples, fromRate, toRate) {
    if (fromRate === toRate) return samples
    const ratio = fromRate / toRate
    const length = Math.max(1, Math.round(samples.length / ratio))
    const result = new Float32Array(length)
    for (let i = 0; i < length; i++) {
        const sourceIndex = i * ratio
        const left = Math.floor(sourceIndex)
        const right = Math.min(samples.length - 1, left + 1)
        const weight = sourceIndex - left
        result[i] = samples[left] * (1 - weight) + samples[right] * weight
    }
    return result
}

function encodeWav(samples, sampleRate) {
    const bytesPerSample = 2
    const buffer = new ArrayBuffer(44 + samples.length * bytesPerSample)
    const view = new DataView(buffer)

    writeAscii(view, 0, 'RIFF')
    view.setUint32(4, 36 + samples.length * bytesPerSample, true)
    writeAscii(view, 8, 'WAVE')
    writeAscii(view, 12, 'fmt ')
    view.setUint32(16, 16, true)
    view.setUint16(20, 1, true)
    view.setUint16(22, 1, true)
    view.setUint32(24, sampleRate, true)
    view.setUint32(28, sampleRate * bytesPerSample, true)
    view.setUint16(32, bytesPerSample, true)
    view.setUint16(34, 8 * bytesPerSample, true)
    writeAscii(view, 36, 'data')
    view.setUint32(40, samples.length * bytesPerSample, true)

    let offset = 44
    for (const sample of samples) {
        const clamped = Math.max(-1, Math.min(1, sample))
        view.setInt16(offset, clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff, true)
        offset += bytesPerSample
    }

    return new Blob([view], { type: 'audio/wav' })
}

function writeAscii(view, offset, text) {
    for (let i = 0; i < text.length; i++) {
        view.setUint8(offset + i, text.charCodeAt(i))
    }
}

profileSave?.addEventListener('click', e => {
    e.preventDefault()
    saveUserProfile().catch(error => showNotice(error.message || '资料保存失败'))
})

loginSubmit?.addEventListener('click', e => {
    e.preventDefault()
    submitAuth('login').catch(error => showNotice(error.message || '登录失败'))
})

registerSubmit?.addEventListener('click', e => {
    e.preventDefault()
    submitAuth('register').catch(error => showNotice(error.message || '注册失败'))
})

userLogout?.addEventListener('click', e => {
    e.preventDefault()
    logoutUser().catch(error => showNotice(error.message || '退出失败'))
})

rechargeSubmit?.addEventListener('click', e => {
    e.preventDefault()
    createRechargeOrder().catch(error => showNotice(error.message || '充值失败'))
})

initProfileMotion()
initSystemSettings()
renderState()
initAuthSession().catch(() => {})

if (typeof window !== 'undefined') {
    window.addEventListener('pagehide', sendCreditSettlementBeacon)
    window.addEventListener('beforeunload', sendCreditSettlementBeacon)
    window.addEventListener('pageshow', () => {
        startRechargePolling({ immediate: true })
    })
    document.addEventListener('visibilitychange', () => {
        if (!document.hidden) startRechargePolling({ immediate: true })
    })

    window.cocoGui = {
        bridge,
        state,
        togglePanel,
        requestMode,
        setMode,
        closeMode,
        settleCredit,
        withLoad,
    }
}
