<template>
    <div class="default-main">
        <el-alert type="info" :title="t('game.live.tip')" show-icon class="mb-12" />
        <el-alert :type="wsConnected ? 'success' : 'warning'" :title="wsConnected ? t('game.live.ws_connected') : t('game.live.ws_disconnected')" show-icon class="mb-12" />
        <el-alert
            v-if="snapshot.runtime_enabled === false && !snapshot.maintenance_ui"
            type="warning"
            :title="t('game.live.runtime_draining_banner')"
            show-icon
            class="mb-12"
        />
        <el-alert
            v-if="snapshot.maintenance_ui"
            type="warning"
            :title="t('game.live.runtime_maintenance_banner')"
            show-icon
            class="mb-12"
        />

        <div class="live-top-toolbar">
            <div class="live-top-toolbar__row">
                <span class="live-top-toolbar__label">{{ t('game.live.runtime_switch') }}</span>
                <el-switch
                    :model-value="snapshot.runtime_enabled"
                    :loading="runtimeSwitchLoading"
                    :disabled="runtimeSwitchLoading || voidSubmitting"
                    @change="onRuntimeSwitch"
                />
                <span v-if="snapshot.maintenance_ui" class="live-top-toolbar__hint">{{ t('game.live.runtime_off_tip') }}</span>
                <div class="live-top-toolbar__actions">
                    <el-button
                        class="live-top-toolbar__btn-void"
                        type="danger"
                        plain
                        :disabled="asideOperationLocked || !canVoidPeriod || voidSubmitting || runtimeSwitchLoading"
                        @click="openVoidDialog"
                    >
                        {{ t('game.live.void_btn') }}
                    </el-button>
                    <el-button :loading="loading" :disabled="asideOperationLocked" @click="loadSnapshot({ force: true })">{{ t('Refresh') }}</el-button>
                </div>
            </div>
        </div>

        <el-card shadow="never" class="mb-12 live-control-card">
            <el-alert v-if="snapshot.is_payout_phase" type="warning" :title="t('game.live.payout_phase')" show-icon class="mb-12" />
            <div class="live-control-layout">
                <div class="live-control-main">
                    <el-descriptions :column="1" border size="small" class="live-desc">
                        <el-descriptions-item :label="t('game.live.current_record')">
                            <span class="period-no">{{ snapshot.record?.period_no || '—' }}</span>
                        </el-descriptions-item>
                        <el-descriptions-item :label="t('game.live.ai_default_number')">
                            <span class="num-em">{{ snapshot.ai_default_number ?? '—' }}</span>
                        </el-descriptions-item>
                        <el-descriptions-item v-if="snapshot.pending_draw_number != null" :label="t('game.live.pending_draw')">
                            <el-tag type="primary" effect="plain">{{ snapshot.pending_draw_number }}</el-tag>
                        </el-descriptions-item>
                    </el-descriptions>

                    <div class="countdown-block">
                        <div class="countdown-block__title">{{ t('game.live.countdown') }}</div>
                        <div v-if="snapshot.maintenance_ui" class="countdown-maintenance">
                            {{ t('game.live.countdown_maintenance') }}
                        </div>
                        <div v-else class="countdown-cards">
                            <div class="cd-card">
                                <span class="cd-card__label">{{ t('game.live.bet_countdown') }}</span>
                                <span class="cd-card__val">{{ countdownParts.bet }}s</span>
                            </div>
                            <div class="cd-card">
                                <span class="cd-card__label">{{ t('game.live.draw_countdown') }}</span>
                                <span class="cd-card__val">{{ countdownParts.draw }}s</span>
                            </div>
                            <div class="cd-card" :class="{ 'is-active': snapshot.is_payout_phase }">
                                <span class="cd-card__label">{{ t('game.live.payout_countdown') }}</span>
                                <span class="cd-card__val">{{ countdownParts.payout }}</span>
                            </div>
                        </div>
                    </div>

                    <div class="calc-result-bar">
                        <span class="calc-result-bar__item">
                            <span class="calc-result-bar__k">{{ t('game.live.calc_result_number') }}</span>
                            <span class="calc-result-bar__v">{{ displayResultNumber ?? '—' }}</span>
                        </span>
                        <span class="calc-result-bar__item">
                            <span class="calc-result-bar__k">{{ t('game.live.calc_estimated_loss') }}</span>
                            <span class="calc-result-bar__v mono">{{ calcEstimatedLoss }}</span>
                        </span>
                    </div>
                </div>
            </div>
        </el-card>

        <el-row :gutter="12" class="live-tables-row">
            <el-col :xs="24" :sm="12">
                <el-card shadow="never">
                    <template #header>{{ t('game.live.candidate_title') }}</template>
                    <el-table
                        :data="candidateNumbersSorted"
                        :height="tableHeight"
                        :row-class-name="candidateRowClassName"
                        class="candidate-table"
                        :default-sort="{ prop: 'estimated_loss', order: 'ascending' }"
                        @sort-change="onCandidateSortChange"
                    >
                        <el-table-column prop="number" :label="t('game.live.number')" width="76" align="center" header-align="center" sortable="custom">
                            <template #default="scope">
                                <el-tag size="small" effect="plain" class="number-tag">
                                    {{ scope.row.number ?? '-' }}
                                </el-tag>
                            </template>
                        </el-table-column>
                        <el-table-column
                            prop="estimated_loss"
                            :label="t('game.live.estimated_loss')"
                            width="110"
                            align="center"
                            header-align="center"
                            sortable="custom"
                        />
                        <el-table-column :label="t('game.live.btn_draw')" width="96" align="center" header-align="center">
                            <template #default="scope">
                                <el-switch
                                    :model-value="isScheduledNumber(scope.row.number)"
                                    :loading="drawLoading && pendingSwitchNumber === scope.row.number"
                                    :disabled="asideOperationLocked || !snapshot.can_schedule_draw || drawLoading"
                                    inline-prompt
                                    @change="(v:boolean) => onPickSwitchChange(v, scope.row.number)"
                                />
                            </template>
                        </el-table-column>
                    </el-table>
                </el-card>
            </el-col>
            <el-col :xs="24" :sm="12">
                <el-card shadow="never">
                    <template #header>{{ t('game.live.bet_stream_title') }}</template>
                    <el-table :data="snapshot.bets" :height="tableHeight" class="bet-stream-table">
                        <el-table-column prop="username" :label="t('game.live.username')" width="120" align="center" header-align="center">
                            <template #default="scope">
                                <span class="bet-user">{{ String(scope.row.username || '-') }}</span>
                            </template>
                        </el-table-column>
                        <el-table-column prop="pick_numbers" :label="t('game.live.pick_numbers')" min-width="150" align="center" header-align="center">
                            <template #default="scope">
                                <div class="pick-tags">
                                    <el-tag
                                        v-for="num in parsePickNumbers(scope.row.pick_numbers)"
                                        :key="`pick-${scope.row.id}-${String(num)}`"
                                        size="small"
                                        effect="plain"
                                        class="pick-tags__item"
                                    >
                                        {{ num }}
                                    </el-tag>
                                    <span v-if="parsePickNumbers(scope.row.pick_numbers).length === 0">-</span>
                                </div>
                            </template>
                        </el-table-column>
                        <el-table-column prop="total_amount" :label="t('game.live.total_amount')" width="92" align="center" header-align="center" />
                        <el-table-column prop="win_amount" :label="t('game.live.win_amount')" width="92" align="center" header-align="center">
                            <template #default="scope">
                                <span class="mono">{{ formatWinAmount(scope.row) }}</span>
                            </template>
                        </el-table-column>
                    </el-table>
                </el-card>
            </el-col>
        </el-row>

        <el-dialog
            v-model="voidDialogVisible"
            class="live-void-dialog"
            :title="t('game.live.void_dialog_title')"
            width="520px"
            align-center
            append-to-body
            destroy-on-close
            :close-on-click-modal="false"
            @closed="voidReason = ''"
        >
            <el-form class="live-void-form" label-position="top" @submit.prevent>
                <el-form-item :label="t('game.live.void_reason_label')">
                    <el-input
                        v-model="voidReason"
                        type="textarea"
                        :rows="5"
                        :autosize="{ minRows: 4, maxRows: 12 }"
                        :placeholder="t('game.live.void_reason_placeholder')"
                        maxlength="255"
                        show-word-limit
                    />
                </el-form-item>
            </el-form>
            <template #footer>
                <div class="live-void-footer">
                    <el-button @click="voidDialogVisible = false">{{ t('Cancel') }}</el-button>
                    <el-button type="primary" :loading="voidSubmitting" @click="submitVoidPeriod">{{ t('game.live.void_submit') }}</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>

<script setup lang="ts">
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import createAxios from '/@/utils/axios'

interface Snapshot {
    record: anyObj | null
    bets: anyObj[]
    candidate_numbers: anyObj[]
    ai_default_number: number | null
    pending_draw_number: number | null
    period_seconds?: number
    bet_seconds?: number
    pick_max_number_count?: number
    /** 开奖号码池上限（1–draw_number_max），与单注可选号码上限无关 */
    draw_number_max?: number
    remaining_seconds?: number
    bet_remaining_seconds?: number
    payout_remaining_seconds?: number
    is_payout_phase?: boolean
    can_calculate?: boolean
    can_draw?: boolean
    can_schedule_draw?: boolean
    server_time?: number
    /** 游戏运行开关：false 表示已关服（当局仍可进行至派彩结束） */
    runtime_enabled?: boolean
    /** 完整维护 UI：关服且当前无进行中/未结清对局（派彩已全部完成） */
    maintenance_ui?: boolean
    /** 已开奖号码（status≥2 时有值） */
    result_number?: number | null
}

const { t } = useI18n()
const loading = ref(false)
const snapshot = reactive<Snapshot>({
    record: null,
    bets: [],
    candidate_numbers: [],
    ai_default_number: null,
    pending_draw_number: null,
    period_seconds: 30,
    bet_seconds: 20,
    pick_max_number_count: 10,
    draw_number_max: 36,
    remaining_seconds: 0,
    bet_remaining_seconds: 0,
    payout_remaining_seconds: 0,
    is_payout_phase: false,
    can_calculate: false,
    can_draw: false,
    can_schedule_draw: false,
    runtime_enabled: true,
    maintenance_ui: false,
    result_number: null,
})
const calcLoading = ref(false)
const drawLoading = ref(false)
const pendingSwitchNumber = ref<number | null>(null)
const runtimeSwitchLoading = ref(false)
const pendingRuntimeTarget = ref<boolean | null>(null)
const voidDialogVisible = ref(false)
const voidReason = ref('')
const voidSubmitting = ref(false)
const calcResultNumber = ref<number | null>(null)
const calcEstimatedLoss = ref<string>('0.00')

/** 服务端 Unix 秒 − 本地 Unix 秒，用于派彩倒计时与服务器对齐 */
const serverSkewSeconds = ref(0)
/** 每秒递增，驱动派彩剩余秒本地刷新 */
const clockTick = ref(0)
let clockTimer: number | null = null
let payoutStuckRefreshTimer: number | null = null
let drawStuckRefreshTimer: number | null = null
let drawStuckSeconds = 0
let payoutPhaseStuckSeconds = 0
let fallbackPollTimer: number | null = null
let betStreamRefreshTimer: number | null = null
/** 合并并发 snapshot 请求，避免 axios 重复请求取消导致控制台报错 */
let snapshotLoadPromise: Promise<void> | null = null

const wsLoading = ref(false)
const wsReady = ref(false)
const wsConnected = ref(false)
const wsUrl = ref('')
const wsTopics = ref<string[]>([])
const wsClient = ref<WebSocket | null>(null)
const isMobile = ref(false)
const candidateSort = ref<{ prop: string; order: 'ascending' | 'descending' | null }>({ prop: 'estimated_loss', order: 'ascending' })

function updateIsMobile(): void {
    isMobile.value = window.matchMedia('(max-width: 768px)').matches
}

const tableHeight = computed(() => (isMobile.value ? 320 : 420))

async function reloadWsConfig(): Promise<void> {
    wsLoading.value = true
    try {
        const res = await createAxios({
            url: '/admin/game.Live/wsConfig',
            method: 'get',
            showCodeMessage: true,
        })
        if (res.code !== 1 || !res.data) {
            wsReady.value = false
            return
        }
        wsUrl.value = String(res.data.ws_url || '')
        if (Array.isArray(res.data.subscribe_topics)) {
            wsTopics.value = res.data.subscribe_topics.filter((topic: unknown): topic is string => typeof topic === 'string' && topic.trim() !== '')
        } else {
            wsTopics.value = []
        }
        wsReady.value = wsUrl.value !== ''
    } finally {
        wsLoading.value = false
    }
}

function handleWsPayload(raw: unknown): void {
    let parsed: anyObj | null = null
    if (typeof raw === 'string') {
        try {
            parsed = JSON.parse(raw)
        } catch {
            return
        }
    } else if (raw && typeof raw === 'object') {
        parsed = raw as anyObj
    }
    if (!parsed) {
        return
    }
    const event = typeof parsed.event === 'string' ? parsed.event : ''
    if (event === 'admin.live.snapshot' && parsed.data && typeof parsed.data === 'object') {
        mergeLiveSnapshot(parsed.data as anyObj)
        return
    }
    if (event === 'admin.live.opened') {
        const opened = parsed.data as anyObj
        if (typeof opened.result_number === 'number') {
            snapshot.result_number = opened.result_number
            calcResultNumber.value = opened.result_number
        }
        void loadSnapshot({ force: true })
        return
    }
    if (event === 'admin.live.finalized' && parsed.data && typeof parsed.data === 'object') {
        const fin = parsed.data as anyObj
        if (toBool(fin.maintenance_ui) === true) {
            snapshot.is_payout_phase = false
            snapshot.payout_remaining_seconds = 0
        }
        void loadSnapshot({ force: true })
        return
    }
    if (event === 'period.payout' && parsed.data && typeof parsed.data === 'object') {
        mergePeriodPayoutTick(parsed.data as anyObj)
        const payoutData = parsed.data as anyObj
        if (typeof payoutData.result_number === 'number') {
            snapshot.result_number = payoutData.result_number
            calcResultNumber.value = payoutData.result_number
        }
        snapshot.is_payout_phase = true
        return
    }
    if (event === 'bet.win' && parsed.data && typeof parsed.data === 'object') {
        const winData = parsed.data as anyObj
        if (winData.is_jackpot === true) {
            ElMessage.success(t('game.live.jackpot_hit_tip'))
        }
        return
    }
    if (event === 'jackpot.hit' && parsed.data && typeof parsed.data === 'object') {
        const jackpotData = parsed.data as anyObj
        const hits = Array.isArray(jackpotData.hits) ? jackpotData.hits : []
        if (hits.length > 0) {
            ElMessage.success(t('game.live.jackpot_hit_tip'))
        }
        return
    }
    if (event === 'period.payout.tick' && parsed.data && typeof parsed.data === 'object') {
        mergePeriodPayoutTick(parsed.data as anyObj)
        return
    }
    if (event === 'period.tick' && parsed.data && typeof parsed.data === 'object') {
        handlePeriodTickEvent(parsed.data as anyObj)
        return
    }
    if (event === 'bet.accepted' && parsed.data && typeof parsed.data === 'object') {
        const betData = parsed.data as anyObj
        const periodNo = typeof betData.period_no === 'string' ? betData.period_no : ''
        const currentNo = typeof snapshot.record?.period_no === 'string' ? snapshot.record.period_no : ''
        if (periodNo !== '' && periodNo === currentNo) {
            scheduleBetStreamRefresh()
        } else if (!wsConnected.value) {
            void loadSnapshot({ force: true })
        }
        return
    }
}

/** 有新下注时防抖拉取快照，补全 WS 每秒快照之间的下注列表 */
function scheduleBetStreamRefresh(): void {
    if (betStreamRefreshTimer !== null) {
        window.clearTimeout(betStreamRefreshTimer)
    }
    betStreamRefreshTimer = window.setTimeout(() => {
        betStreamRefreshTimer = null
        void loadSnapshot({ force: true })
    }, 600)
}

/** 用 period.tick 轻量字段刷新倒计时（不触发 HTTP，避免与 WS 每秒 snapshot 冲突） */
function mergePeriodTickFields(periodData: anyObj): void {
    if (typeof periodData.server_time === 'number') {
        syncServerClock(periodData.server_time)
    }
    if (typeof periodData.countdown === 'number') {
        snapshot.remaining_seconds = Math.max(0, periodData.countdown)
    }
    if (typeof periodData.bet_close_in === 'number') {
        snapshot.bet_remaining_seconds = Math.max(0, periodData.bet_close_in)
    }
}

function mergePeriodPayoutTick(data: anyObj): void {
    if (typeof data.server_time === 'number') {
        syncServerClock(data.server_time)
    }
    const remain = numberValue(data.payout_remaining_seconds)
    if (remain !== null) {
        snapshot.payout_remaining_seconds = Math.max(0, remain)
        snapshot.is_payout_phase = remain > 0
    }
    const until = readPayoutUntilUnix(data)
    if (until !== null && snapshot.record && typeof snapshot.record === 'object') {
        snapshot.record.payout_until = until
    }
}

function handlePeriodTickEvent(periodData: anyObj): void {
    mergePeriodTickFields(periodData)
    const status = typeof periodData.status === 'string' ? periodData.status : ''
    const periodNo = typeof periodData.period_no === 'string' ? periodData.period_no : ''
    const currentNo = typeof snapshot.record?.period_no === 'string' ? snapshot.record.period_no : ''
    const runtimeOff =
        toBool(snapshot.runtime_enabled) === false || toBool(periodData.runtime_enabled) === false
    if (runtimeOff && (status === 'betting' || status === 'locked')) {
        return
    }
    if (status === 'betting' && periodNo !== '' && periodNo !== currentNo) {
        void loadSnapshot({ force: true })
        return
    }
    if (status === 'finished') {
        snapshot.is_payout_phase = false
        snapshot.payout_remaining_seconds = 0
        void loadSnapshot({ force: true })
        return
    }
    if (currentNo === '' && periodNo !== '' && !runtimeOff) {
        void loadSnapshot({ force: true })
    }
}

function connectWs(): void {
    if (!wsReady.value || !wsUrl.value) {
        return
    }
    disconnectWs()
    const socket = new WebSocket(wsUrl.value)
    wsClient.value = socket
    socket.onopen = () => {
        wsConnected.value = true
        const topics = wsTopics.value
        const payload = JSON.stringify({ action: 'subscribe', topics })
        socket.send(payload)
    }
    socket.onmessage = (event) => {
        handleWsPayload(event.data)
    }
    socket.onerror = () => {
        wsConnected.value = false
    }
    socket.onclose = () => {
        wsConnected.value = false
        wsClient.value = null
        window.setTimeout(() => {
            if (!wsConnected.value) {
                connectWs()
            }
        }, 1200)
    }
}

function disconnectWs(): void {
    if (wsClient.value) {
        wsClient.value.close()
        wsClient.value = null
    }
    wsConnected.value = false
}

function formatPicks(v: unknown): string {
    if (Array.isArray(v)) return JSON.stringify(v)
    if (typeof v === 'string') return v
    return '-'
}

function parsePickNumbers(v: unknown): Array<number | string> {
    if (Array.isArray(v)) {
        return v
            .map((item) => {
                if (typeof item === 'number' || typeof item === 'string') {
                    return item
                }
                return null
            })
            .filter((item): item is number | string => item !== null)
    }
    if (typeof v === 'string') {
        const s = v.trim()
        if (s === '') {
            return []
        }
        try {
            const parsed = JSON.parse(s)
            if (Array.isArray(parsed)) {
                return parsed
                    .map((item) => {
                        if (typeof item === 'number' || typeof item === 'string') {
                            return item
                        }
                        return null
                    })
                    .filter((item): item is number | string => item !== null)
            }
        } catch {
            return [s]
        }
        return [s]
    }

    return []
}

function numberValue(v: unknown): number | null {
    if (typeof v === 'number' && Number.isFinite(v)) {
        return v
    }
    if (typeof v === 'string' && v.trim() !== '') {
        const n = Number(v)
        if (!Number.isNaN(n) && Number.isFinite(n)) {
            return n
        }
    }
    return null
}

function estimatedLossSortValue(v: unknown): number {
    if (typeof v === 'number' && Number.isFinite(v)) {
        return v
    }
    if (typeof v === 'string' && v.trim() !== '') {
        const n = Number(v)
        if (!Number.isNaN(n) && Number.isFinite(n)) {
            return n
        }
    }
    return Number.MAX_SAFE_INTEGER
}

const candidateNumbersSorted = computed(() => {
    const list = Array.isArray(snapshot.candidate_numbers) ? [...snapshot.candidate_numbers] : []
    list.sort((a, b) => {
        const sp = candidateSort.value.prop
        const so = candidateSort.value.order
        const dir = so === 'descending' ? -1 : 1

        if (sp === 'number') {
            const na = numberValue(a?.number)
            const nb = numberValue(b?.number)
            if (na !== null && nb !== null && na !== nb) {
                return (na - nb) * dir
            }
        } else {
            const ea = estimatedLossSortValue(a?.estimated_loss)
            const eb = estimatedLossSortValue(b?.estimated_loss)
            if (ea !== eb) {
                return (ea - eb) * dir
            }
        }

        const ea2 = estimatedLossSortValue(a?.estimated_loss)
        const eb2 = estimatedLossSortValue(b?.estimated_loss)
        if (ea2 !== eb2) {
            return ea2 - eb2
        }
        const na2 = numberValue(a?.number)
        const nb2 = numberValue(b?.number)
        if (na2 !== null && nb2 !== null && na2 !== nb2) {
            return na2 - nb2
        }
        return String(a?.number ?? '').localeCompare(String(b?.number ?? ''))
    })
    return list
})

function onCandidateSortChange(arg: { prop: string; order: 'ascending' | 'descending' | null }): void {
    const prop = typeof arg?.prop === 'string' && arg.prop !== '' ? arg.prop : 'estimated_loss'
    const order = arg?.order === 'descending' || arg?.order === 'ascending' ? arg.order : 'ascending'
    candidateSort.value = { prop, order }
}

function isScheduledNumber(v: unknown): boolean {
    const n = numberValue(v)
    if (n === null) {
        return false
    }
    return snapshot.pending_draw_number === n
}

const displayResultNumber = computed(() => {
    const fromSnap = numberValue(snapshot.result_number)
    if (fromSnap !== null) {
        return fromSnap
    }
    const fromRec = numberValue(snapshot.record?.result_number)
    if (fromRec !== null) {
        return fromRec
    }
    return calcResultNumber.value
})

function updateCalcLossFromResultNumber(): void {
    const rn = displayResultNumber.value
    if (rn === null) {
        return
    }
    const row = snapshot.candidate_numbers.find((c) => numberValue(c?.number) === rn)
    if (row && row.estimated_loss !== undefined && row.estimated_loss !== null) {
        calcEstimatedLoss.value = String(row.estimated_loss)
    }
}

function formatWinAmount(row: anyObj): string {
    const st = Number(row?.bet_status ?? 0)
    const win = row?.win_amount
    if (st === 2 || st === 5) {
        return win !== undefined && win !== null && String(win) !== '' ? String(win) : '0.00'
    }
    return '—'
}

function candidateRowClassName(arg: { row: anyObj }): string {
    const classes: string[] = []
    if (isScheduledNumber(arg.row?.number)) {
        classes.push('is-scheduled-row')
    }
    const result = displayResultNumber.value
    const num = numberValue(arg.row?.number)
    if (result !== null && num !== null && num === result) {
        classes.push('is-result-row')
    }
    return classes.join(' ')
}

async function onPickSwitchChange(val: boolean, rowNumber: unknown): Promise<void> {
    const target = numberValue(rowNumber)
    if (target === null) {
        return
    }
    if (!val) {
        return
    }
    if (snapshot.pending_draw_number === target) {
        return
    }
    pendingSwitchNumber.value = target
    try {
        await onDraw(target)
    } finally {
        pendingSwitchNumber.value = null
    }
}

const canVoidPeriod = computed(() => {
    const r = snapshot.record
    if (!r) {
        return false
    }
    const s = Number(r.status)
    return s === 0 || s === 1
})

/** 派彩结束后的完整维护态：操作区除顶部开关外全部锁定 */
const asideOperationLocked = computed(() => snapshot.maintenance_ui === true)

function toBool(v: unknown): boolean | null {
    if (typeof v === 'boolean') {
        return v
    }
    if (typeof v === 'number' && Number.isFinite(v)) {
        if (v === 1) return true
        if (v === 0) return false
        return null
    }
    if (typeof v === 'string') {
        const s = v.trim().toLowerCase()
        if (s === '1' || s === 'true') return true
        if (s === '0' || s === 'false') return false
    }
    return null
}

function mergeLiveSnapshot(data: anyObj): void {
    const prevPeriodId = snapshot.record?.id != null ? Number(snapshot.record.id) : null
    let periodChanged = false

    if (data.record !== undefined) {
        const nextId = data.record?.id != null ? Number(data.record.id) : null
        periodChanged = prevPeriodId !== null && nextId !== null && prevPeriodId !== nextId
        const runtimeOff = toBool(data.runtime_enabled) === false || toBool(snapshot.runtime_enabled) === false
        const serverMaintenance = data.maintenance_ui === true
        if (runtimeOff && serverMaintenance) {
            snapshot.record = null
            snapshot.is_payout_phase = false
            snapshot.payout_remaining_seconds = 0
            snapshot.can_calculate = false
            snapshot.can_draw = false
            snapshot.can_schedule_draw = false
        } else {
            snapshot.record = data.record
        }
    }

    const incomingBets = Array.isArray(data.bets) ? data.bets : null
    if (incomingBets !== null) {
        if (incomingBets.length > 0 || periodChanged || prevPeriodId === null) {
            snapshot.bets = incomingBets
        }
    }

    const incomingCandidates = Array.isArray(data.candidate_numbers) ? data.candidate_numbers : null
    if (incomingCandidates !== null) {
        if (incomingCandidates.length > 0 || periodChanged || prevPeriodId === null) {
            snapshot.candidate_numbers = incomingCandidates
        }
    }

    snapshot.ai_default_number = data.ai_default_number ?? null
    snapshot.pending_draw_number = typeof data.pending_draw_number === 'number' ? data.pending_draw_number : null
    snapshot.period_seconds = data.period_seconds ?? 30
    snapshot.bet_seconds = data.bet_seconds ?? 20
    snapshot.pick_max_number_count = data.pick_max_number_count ?? 10
    snapshot.draw_number_max = data.draw_number_max ?? 36
    snapshot.remaining_seconds = data.remaining_seconds ?? 0
    snapshot.bet_remaining_seconds = data.bet_remaining_seconds ?? 0
    snapshot.payout_remaining_seconds = data.payout_remaining_seconds ?? 0
    snapshot.is_payout_phase = !!data.is_payout_phase
    snapshot.can_calculate = !!data.can_calculate
    snapshot.can_draw = !!data.can_draw
    snapshot.can_schedule_draw = !!(data.can_schedule_draw || data.can_draw)
    const runtimeEnabled = toBool(data.runtime_enabled)
    if (pendingRuntimeTarget.value !== null) {
        // 开关请求进行中：避免 ws 的旧快照覆盖用户刚切换的状态，导致 UI 回弹
        snapshot.runtime_enabled = pendingRuntimeTarget.value
    } else if (runtimeEnabled !== null) {
        snapshot.runtime_enabled = runtimeEnabled
    }
    if (typeof data.maintenance_ui === 'boolean') {
        if (pendingRuntimeTarget.value !== null) {
            // 开启时不应显示维护中；关闭后的维护中应由服务端在对局完全结束后下发
            snapshot.maintenance_ui = pendingRuntimeTarget.value ? false : snapshot.maintenance_ui
        } else {
            snapshot.maintenance_ui = data.maintenance_ui
        }
    }

    if (typeof data.result_number === 'number') {
        snapshot.result_number = data.result_number
        calcResultNumber.value = data.result_number
    } else if (periodChanged) {
        snapshot.result_number = null
        calcResultNumber.value = null
        calcEstimatedLoss.value = '0.00'
    }

    updateCalcLossFromResultNumber()
    syncServerClock(data.server_time)
}


function syncServerClock(serverTime: unknown): void {
    if (typeof serverTime === 'number' && Number.isFinite(serverTime)) {
        serverSkewSeconds.value = serverTime - Math.floor(Date.now() / 1000)
        snapshot.server_time = serverTime
    }
}

function readPayoutUntilUnix(rec: anyObj | null): number | null {
    if (!rec) {
        return null
    }
    const v = rec.payout_until
    if (v === null || v === undefined || v === '') {
        return null
    }
    if (typeof v === 'number' && Number.isFinite(v)) {
        return v
    }
    if (typeof v === 'string' && /^\d+$/.test(v)) {
        return parseInt(v, 10)
    }
    return null
}

/** 派彩剩余秒：优先用 payout_until 与对时后的「服务器当前秒」计算，便于每秒递减 */
const payoutRemainingLive = computed(() => {
    clockTick.value
    if (!snapshot.is_payout_phase) {
        return null
    }
    const until = readPayoutUntilUnix(snapshot.record)
    if (until !== null) {
        const serverNow = Math.floor(Date.now() / 1000) + serverSkewSeconds.value
        const diff = until - serverNow
        return diff > 0 ? diff : 0
    }
    return snapshot.payout_remaining_seconds ?? 0
})

function isRequestCanceled(err: unknown): boolean {
    return axios.isCancel(err) || (err instanceof Error && err.name === 'CanceledError')
}

async function loadSnapshot(options?: { force?: boolean }): Promise<void> {
    if (!options?.force && wsConnected.value && snapshot.record) {
        return
    }
    if (snapshotLoadPromise) {
        return snapshotLoadPromise
    }
    snapshotLoadPromise = (async () => {
        loading.value = true
        try {
            const res = await createAxios({
                url: '/admin/game.Live/snapshot',
                method: 'get',
                showCodeMessage: false,
                showErrorMessage: false,
                cancelDuplicateRequest: false,
            })
            if (res.code === 1 && res.data) {
                mergeLiveSnapshot(res.data as anyObj)
            }
        } catch (err) {
            if (!isRequestCanceled(err)) {
                throw err
            }
        } finally {
            loading.value = false
            snapshotLoadPromise = null
        }
    })()
    return snapshotLoadPromise
}

async function onRuntimeSwitch(val: boolean | string | number): void {
    const on = toBool(val) === true
    if (runtimeSwitchLoading.value) {
        return
    }
    // el-switch 为受控组件（model-value 来自 snapshot），接口返回前先乐观更新，避免点击后立刻回弹
    snapshot.runtime_enabled = on
    if (on) {
        snapshot.maintenance_ui = false
    }
    pendingRuntimeTarget.value = on
    runtimeSwitchLoading.value = true
    try {
        const res = await createAxios({
            url: '/admin/game.Live/runtime',
            method: 'post',
            data: { enabled: on ? 1 : 0 },
            showSuccessMessage: true,
        })
        if (res.code === 1 && res.data) {
            mergeLiveSnapshot(res.data as anyObj)
        } else {
            await loadSnapshot({ force: true })
        }
    } catch {
        await loadSnapshot({ force: true })
    } finally {
        runtimeSwitchLoading.value = false
        pendingRuntimeTarget.value = null
    }
}

function openVoidDialog(): void {
    voidReason.value = ''
    voidDialogVisible.value = true
}

async function submitVoidPeriod(): Promise<void> {
    const reason = voidReason.value.trim()
    if (reason.length < 2) {
        ElMessage.warning(t('game.live.void_reason_too_short'))
        return
    }
    if (!snapshot.record) {
        return
    }
    voidSubmitting.value = true
    try {
        const res = await createAxios({
            url: '/admin/game.Live/voidPeriod',
            method: 'post',
            data: {
                record_id: snapshot.record.id,
                void_reason: reason,
            },
            showSuccessMessage: true,
        })
        if (res.code === 1 && res.data) {
            mergeLiveSnapshot(res.data as anyObj)
            const refund = (res.data as anyObj).void_refund
            if (refund && typeof refund === 'object') {
                const orderCount = Number(refund.order_count ?? 0)
                const totalAmount = String(refund.total_amount ?? '0.00')
                if (orderCount > 0) {
                    ElMessage.success(`已退款 ${orderCount} 笔注单，合计 ${totalAmount}`)
                }
            }
        }
        voidDialogVisible.value = false
    } finally {
        voidSubmitting.value = false
    }
}

async function onCalculate() {
    if (!snapshot.record) return
    calcLoading.value = true
    try {
        const res = await createAxios({
            url: '/admin/game.Live/calculate',
            method: 'post',
            data: {
                record_id: snapshot.record.id,
                manual_number: numberValue(snapshot.pending_draw_number) ?? numberValue(candidateNumbersSorted.value[0]?.number) ?? 1,
            },
            showSuccessMessage: true,
        })
        if (res.code === 1 && res.data) {
            snapshot.candidate_numbers = res.data.candidate_numbers || []
            snapshot.ai_default_number = res.data.ai_default_number ?? null
            calcResultNumber.value = res.data.final_number ?? null
            calcEstimatedLoss.value = String(res.data.final_estimated_loss ?? '0.00')
        }
    } finally {
        calcLoading.value = false
    }
}

async function onDrawWithNumber(targetNumber: number) {
    if (!snapshot.record) return
    drawLoading.value = true
    try {
        await createAxios({
            url: '/admin/game.Live/draw',
            method: 'post',
            data: {
                record_id: snapshot.record.id,
                manual_number: targetNumber,
            },
            showSuccessMessage: true,
        })
        await loadSnapshot({ force: true })
    } finally {
        drawLoading.value = false
    }
}

async function onDraw(targetNumber?: number) {
    if (targetNumber !== undefined) {
        return onDrawWithNumber(targetNumber)
    }
    const pending = numberValue(snapshot.pending_draw_number)
    if (pending !== null) {
        return onDrawWithNumber(pending)
    }
    const first = candidateNumbersSorted.value[0]
    const fallback = numberValue(first?.number) ?? 1
    return onDrawWithNumber(fallback)
}

const countdownParts = computed(() => {
    const bet = snapshot.bet_remaining_seconds ?? 0
    const draw = snapshot.remaining_seconds ?? 0
    let payout = t('game.live.payout_na')
    if (snapshot.is_payout_phase && payoutRemainingLive.value !== null) {
        payout = `${payoutRemainingLive.value}s`
    }
    return { bet, draw, payout }
})

/** 派彩倒计时从 >0 变为 0 时主动拉 HTTP 快照 */
watch(payoutRemainingLive, (remain, prev) => {
    if (!snapshot.is_payout_phase || remain !== 0) {
        return
    }
    if (prev !== null && prev !== undefined && prev > 0) {
        schedulePayoutEndRefresh(400)
    }
})

function schedulePayoutEndRefresh(delayMs: number): void {
    if (payoutStuckRefreshTimer !== null) {
        window.clearTimeout(payoutStuckRefreshTimer)
    }
    payoutStuckRefreshTimer = window.setTimeout(() => {
        payoutStuckRefreshTimer = null
        if (!snapshot.is_payout_phase) {
            return
        }
        void loadSnapshot({ force: true })
    }, delayMs)
}

/** 下注/开奖倒计时均已归零但仍未进入派彩（常见于 live ticker 未跑或开奖锁竞争失败） */
function isPrePayoutDrawStuck(): boolean {
    if (snapshot.is_payout_phase || !snapshot.can_calculate) {
        return false
    }
    const bet = snapshot.bet_remaining_seconds ?? 0
    const draw = snapshot.remaining_seconds ?? 0
    if (bet > 0 || draw > 0) {
        return false
    }
    const st = numberValue(snapshot.record?.status)
    return st === 0 || st === 1
}

/** 派彩倒计时已为 0 但 is_payout_phase 仍为 true（关服排水时常见） */
function tickPayoutPhaseStuckRecovery(): void {
    if (!snapshot.is_payout_phase) {
        payoutPhaseStuckSeconds = 0
        return
    }
    const remain = payoutRemainingLive.value
    if (remain === null || remain > 0) {
        payoutPhaseStuckSeconds = 0
        return
    }
    payoutPhaseStuckSeconds++
    if (payoutPhaseStuckSeconds >= 4) {
        payoutPhaseStuckSeconds = 0
        void loadSnapshot({ force: true })
    }
}

function tickPrePayoutDrawStuckRecovery(): void {
    if (!isPrePayoutDrawStuck()) {
        drawStuckSeconds = 0
        if (drawStuckRefreshTimer !== null) {
            window.clearTimeout(drawStuckRefreshTimer)
            drawStuckRefreshTimer = null
        }
        return
    }
    drawStuckSeconds++
    if (drawStuckSeconds < 8 || drawStuckRefreshTimer !== null) {
        return
    }
    drawStuckRefreshTimer = window.setTimeout(() => {
        drawStuckRefreshTimer = null
        drawStuckSeconds = 0
        if (isPrePayoutDrawStuck()) {
            void loadSnapshot({ force: true })
        }
    }, 300)
}

onMounted(async () => {
    updateIsMobile()
    window.addEventListener('resize', updateIsMobile)
    clockTimer = window.setInterval(() => {
        clockTick.value++
        tickPrePayoutDrawStuckRecovery()
        tickPayoutPhaseStuckRecovery()
    }, 1000)
    fallbackPollTimer = window.setInterval(() => {
        if (!wsConnected.value) {
            void loadSnapshot({ force: true })
        }
    }, 3000)
    await loadSnapshot({ force: true })
    await reloadWsConfig()
    connectWs()
})

onUnmounted(() => {
    window.removeEventListener('resize', updateIsMobile)
    disconnectWs()
    if (clockTimer !== null) {
        window.clearInterval(clockTimer)
        clockTimer = null
    }
    if (fallbackPollTimer !== null) {
        window.clearInterval(fallbackPollTimer)
        fallbackPollTimer = null
    }
    if (payoutStuckRefreshTimer !== null) {
        window.clearTimeout(payoutStuckRefreshTimer)
        payoutStuckRefreshTimer = null
    }
    if (drawStuckRefreshTimer !== null) {
        window.clearTimeout(drawStuckRefreshTimer)
        drawStuckRefreshTimer = null
    }
    if (betStreamRefreshTimer !== null) {
        window.clearTimeout(betStreamRefreshTimer)
        betStreamRefreshTimer = null
    }
})

</script>

<style scoped lang="scss">
.mb-12 {
    margin-bottom: 12px;
}

.live-top-toolbar {
    margin-bottom: 16px;
    padding: 12px 16px;
    border-radius: 8px;
    border: 1px solid var(--el-border-color-lighter);
    background: var(--el-bg-color-overlay);
}

.live-top-toolbar__row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 12px 20px;
}

.live-top-toolbar__label {
    font-size: 14px;
    font-weight: 600;
    color: var(--el-text-color-primary);
}

.live-top-toolbar__hint {
    font-size: 13px;
    color: var(--el-text-color-secondary);
    flex: 1 1 220px;
    line-height: 1.45;
}

.live-top-toolbar__actions {
    margin-left: auto;
    display: inline-flex;
    flex-wrap: wrap;
    gap: 8px;
}

.live-top-toolbar__btn-void {
    border-color: var(--el-color-danger-light-7);
}

.live-control-card {
    :deep(.el-card__body) {
        padding-top: 8px;
    }
}

.live-control-layout {
    display: block;
}

.live-control-main {
    min-width: 0;
}

.live-desc {
    margin-bottom: 16px;
    :deep(.el-descriptions__label) {
        width: 120px;
        font-weight: 500;
    }
}

.period-no {
    display: inline-block;
    max-width: 100%;
    word-break: break-all;
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
    font-size: 13px;
    line-height: 1.5;
    color: var(--el-text-color-primary);
}

.num-em {
    font-size: 16px;
    font-weight: 600;
    color: var(--el-color-primary);
}

.countdown-block {
    margin-bottom: 14px;
}

.countdown-block__title {
    font-size: 13px;
    color: var(--el-text-color-secondary);
    margin-bottom: 8px;
}

.countdown-maintenance {
    border: 1px solid var(--el-border-color-lighter);
    border-radius: 8px;
    padding: 16px 12px;
    text-align: center;
    font-size: 16px;
    font-weight: 600;
    color: var(--el-color-warning-dark-2);
    background: var(--el-color-warning-light-9);
}

.countdown-cards {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 10px;
}

.cd-card {
    border: 1px solid var(--el-border-color-lighter);
    border-radius: 8px;
    padding: 10px 12px;
    background: var(--el-fill-color-blank);
    display: flex;
    flex-direction: column;
    gap: 4px;
    align-items: center;
    text-align: center;
    min-width: 0;
}

/* 移动端保持同一排，压缩间距与字号以省纵向空间 */
@media (max-width: 768px) {
    .countdown-block {
        margin-bottom: 10px;
    }

    .countdown-block__title {
        margin-bottom: 6px;
    }

    .countdown-cards {
        gap: 6px;
    }

    .cd-card {
        padding: 6px 4px;
        border-radius: 6px;
        gap: 2px;
        min-width: 0;
    }

    .cd-card__label {
        font-size: 11px;
        line-height: 1.2;
        padding: 0 2px;
    }

    .cd-card__val {
        font-size: 15px;
    }
}

.cd-card.is-active {
    border-color: var(--el-color-warning-light-5);
    background: var(--el-color-warning-light-9);
}

.cd-card__label {
    font-size: 12px;
    color: var(--el-text-color-secondary);
    line-height: 1.3;
}

.cd-card__val {
    font-size: 18px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--el-text-color-primary);
}

.calc-result-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 16px 24px;
    padding: 10px 14px;
    border-radius: 8px;
    background: var(--el-fill-color-light);
    border: 1px solid var(--el-border-color-lighter);
}

.calc-result-bar__item {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
}

.calc-result-bar__k {
    font-size: 13px;
    color: var(--el-text-color-secondary);
}

.calc-result-bar__v {
    font-size: 14px;
    font-weight: 500;
}

.calc-result-bar__v.mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}

.number-tag {
    min-width: 44px;
    justify-content: center;
    font-variant-numeric: tabular-nums;
}

.pick-tags {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px;
}

.pick-tags__item {
    font-variant-numeric: tabular-nums;
}

.bet-user {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.candidate-table :deep(.is-scheduled-row td) {
    background: var(--el-color-primary-light-9);
}

.candidate-table :deep(.is-result-row td) {
    background: var(--el-color-success-light-9);
}

.candidate-table :deep(.is-result-row .number-tag) {
    border-color: var(--el-color-success);
    color: var(--el-color-success);
    font-weight: 700;
}

@media (max-width: 768px) {
    .live-tables-row .el-col {
        margin-bottom: 12px;
    }

    .pick-tags {
        gap: 4px;
    }
}
</style>

<style lang="scss">
/* Dialog teleport 到 body，独立块以便样式命中 */
.live-void-dialog.el-dialog {
    display: flex;
    flex-direction: column;
    max-height: min(92vh, 720px);
    margin: auto;
}

.live-void-dialog .el-dialog__header {
    flex-shrink: 0;
    padding: 16px 16px 10px;
}

.live-void-dialog .el-dialog__body {
    flex: 1;
    min-height: 0;
    overflow-x: hidden;
    overflow-y: auto;
    padding: 8px 16px 12px;
    -webkit-overflow-scrolling: touch;
}

.live-void-dialog .el-dialog__footer {
    flex-shrink: 0;
    padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
}

.live-void-footer {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 10px;
    width: 100%;
}

.live-void-form .el-form-item {
    margin-bottom: 0;
}

@media (max-width: 768px) {
    .live-void-dialog.el-dialog {
        width: calc(100vw - 24px) !important;
        max-width: 520px;
        max-height: 90vh;
    }

    .live-void-dialog .el-dialog__title {
        font-size: 16px;
        line-height: 1.4;
        word-break: break-word;
    }

    .live-void-footer {
        justify-content: stretch;
    }

    .live-void-footer .el-button {
        flex: 1;
        min-width: 0;
    }
}
</style>
