import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { placeGameBet } from '@/api' import { CHIP_IMAGE_MAP, CHIP_IMAGE_OPTIONS } from '@/constants' import { notify } from '@/lib/notify' import { useAuthStore, useModalStore } from '@/store' import { selectSelectionTotal, useGameAutoHostingStore, useGameRoundStore, useGameSessionStore, } from '@/store/game' import type { ConfirmState } from '@/type' function formatChipDisplayValue(amount: number) { if (Number.isInteger(amount)) { return String(amount) } return amount.toFixed(2).replace(/\.?0+$/, '') } function parseBalance(value: string | number | null | undefined) { if (typeof value === 'number') { return Number.isFinite(value) ? value : 0 } if (typeof value !== 'string') { return 0 } const parsed = Number(value) return Number.isFinite(parsed) ? parsed : 0 } function createIdempotencyKey() { if ( typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ) { return `bet-${crypto.randomUUID()}` } return `bet-${Date.now()}-${Math.random().toString(36).slice(2, 10)}` } function toBetId(chipId: string) { const match = chipId.match(/^chip-(\d+)$/) if (!match) { return null } const betId = Number(match[1]) return Number.isInteger(betId) && betId >= 1 && betId <= 6 ? betId : null } export function useGameControlVm() { const { t } = useTranslation() const chips = useGameRoundStore((state) => state.chips) const activeChipId = useGameRoundStore((state) => state.activeChipId) const activeBetQuantity = useGameRoundStore( (state) => state.activeBetQuantity, ) const round = useGameRoundStore((state) => state.round) const maxSelectionCount = useGameRoundStore( (state) => state.maxSelectionCount, ) const adjustBetQuantity = useGameRoundStore( (state) => state.adjustBetQuantity, ) const selections = useGameRoundStore((state) => state.selections) const clearSelections = useGameRoundStore((state) => state.clearSelections) const restoreRecentSuccessfulSelections = useGameRoundStore( (state) => state.restoreRecentSuccessfulSelections, ) const setRecentSuccessfulSelections = useGameRoundStore( (state) => state.setRecentSuccessfulSelections, ) const isAutoHosting = useGameAutoHostingStore((state) => state.isHosting) const selectChip = useGameRoundStore((state) => state.selectChip) const totalBetAmount = useGameRoundStore(selectSelectionTotal) const connectionStatus = useGameSessionStore( (state) => state.connection.status, ) const tableLimitMax = useGameSessionStore( (state) => state.dashboard.tableLimitMax, ) const shouldConnectRealtime = useGameSessionStore( (state) => state.shouldConnectRealtime, ) const authStatus = useAuthStore((state) => state.status) const currentUser = useAuthStore((state) => state.currentUser) const setCurrentUser = useAuthStore((state) => state.setCurrentUser) const setModalOpen = useModalStore((state) => state.setModalOpen) const [isSubmitting, setIsSubmitting] = useState(false) const chipItems = useMemo(() => { const items = chips.map((chip) => ({ amount: chip.amount, id: chip.id, isSelected: chip.id === activeChipId, isDisabled: tableLimitMax > 0 && chip.amount > tableLimitMax, src: CHIP_IMAGE_MAP.get(chip.id) ?? CHIP_IMAGE_OPTIONS[0]?.src ?? '', valueLabel: formatChipDisplayValue(chip.amount), })) return items.sort((left, right) => { if (left.isSelected === right.isSelected) { return left.id.localeCompare(right.id, undefined, { numeric: true }) } return left.isSelected ? 1 : -1 }) }, [activeChipId, chips, tableLimitMax]) const selectedChip = chipItems.find((chip) => chip.id === activeChipId) ?? chipItems[0] ?? null const balance = parseBalance(currentUser?.coin) const hasSelections = selections.length > 0 const hasEnteredGame = shouldConnectRealtime && connectionStatus === 'connected' const hasSubmittedCurrentRound = Boolean(round.id) && currentUser?.lastBetPeriodNo === round.id const hasInsufficientBalance = hasSelections && totalBetAmount > balance const hasExceededBetLimit = hasSelections && tableLimitMax > 0 && totalBetAmount > tableLimitMax const canIncreaseBetQuantity = useMemo(() => { if (!hasSelections) { return true } const activeChip = chips.find((chip) => chip.id === activeChipId) if (!activeChip) { return false } const nextQuantity = activeBetQuantity + 1 const nextTotalPerSelection = activeChip.amount * nextQuantity const nextTotal = nextTotalPerSelection * selections.length return tableLimitMax <= 0 || nextTotal <= tableLimitMax }, [ hasSelections, chips, activeChipId, activeBetQuantity, selections.length, tableLimitMax, ]) const confirmState: ConfirmState = isSubmitting || isAutoHosting ? 'submitting' : !hasSelections ? 'idle' : hasExceededBetLimit ? 'limit' : hasInsufficientBalance ? 'insufficient' : 'ready' const handleConfirm = useCallback(async () => { if (confirmState === 'submitting' || !hasSelections) { return } if (authStatus !== 'authenticated') { notify.warning(t('commonUi.toast.loginRequired')) setModalOpen('desktopLogin', true) return } if (hasExceededBetLimit) { notify.warning(t('commonUi.toast.betLimitExceeded')) return } if (hasInsufficientBalance) { notify.warning(t('commonUi.toast.insufficientBalance')) return } if (round.phase !== 'betting' || !round.id) { notify.warning(t('commonUi.toast.betUnavailable')) return } if (hasSubmittedCurrentRound) { notify.warning(t('commonUi.toast.betUnavailable')) return } const latestBalance = parseBalance( useAuthStore.getState().currentUser?.coin, ) if (totalBetAmount > latestBalance) { notify.warning(t('commonUi.toast.insufficientBalance')) return } const betId = toBetId(selections[0]?.chipId ?? activeChipId) const singleBetAmount = selections[0]?.amount ?? selectedChip?.amount ?? 0 if (betId === null || singleBetAmount <= 0) { notify.warning(t('commonUi.toast.betUnavailable')) return } setIsSubmitting(true) try { let latestBalance = currentUser?.coin ?? '0' const uniqueNumbers = [ ...new Set(selections.map((item) => item.cellId)), ].sort((left, right) => left - right) const formattedSingleBetAmount = formatChipDisplayValue(singleBetAmount) const result = await placeGameBet({ bet_amount: formattedSingleBetAmount, bet_id: betId, idempotency_key: createIdempotencyKey(), numbers: uniqueNumbers.join(','), period_no: round.id, single_bet_amount: formattedSingleBetAmount, }) if (result.status !== 'accepted') { throw new Error(t('commonUi.toast.betRejected')) } latestBalance = result.balance_after if (currentUser) { setCurrentUser({ ...currentUser, coin: latestBalance, lastBetPeriodNo: round.id, }) } setRecentSuccessfulSelections(selections) clearSelections() notify.success(t('commonUi.toast.betPlaced')) } catch (error) { notify.error(t('commonUi.toast.betPlaceFailed'), { description: error instanceof Error ? error.message : undefined, }) } finally { setIsSubmitting(false) } }, [ activeChipId, authStatus, clearSelections, confirmState, currentUser, hasInsufficientBalance, hasExceededBetLimit, hasSelections, hasSubmittedCurrentRound, round.id, round.phase, selections, selectedChip?.amount, setRecentSuccessfulSelections, setCurrentUser, setModalOpen, t, totalBetAmount, ]) const handleRepeatSelections = useCallback(() => { if (round.phase !== 'betting' || hasSubmittedCurrentRound) { notify.warning(t('commonUi.toast.betUnavailable')) return } const restored = restoreRecentSuccessfulSelections() if (!restored) { notify.warning(t('commonUi.toast.noRecentSuccessfulBet')) return } notify.success(t('commonUi.toast.repeatSelectionsRestored')) }, [ hasSubmittedCurrentRound, restoreRecentSuccessfulSelections, round.phase, t, ]) const handleOpenAutoSetting = useCallback(() => { if (!hasSelections) { notify.warning(t('commonUi.toast.selectNumbersBeforeAutoHosting')) return } setModalOpen('desktopAutoSetting', true) }, [hasSelections, setModalOpen, t]) return { acceptingBets: round.phase === 'betting' && !hasSubmittedCurrentRound && !isAutoHosting, actionsEnabled: hasEnteredGame && round.phase === 'betting' && !hasSubmittedCurrentRound && !isAutoHosting, canClear: selections.length > 0 && round.phase === 'betting' && !hasSubmittedCurrentRound && !isAutoHosting, canDecreaseBetQuantity: activeBetQuantity > 1, canIncreaseBetQuantity, confirmLabel: confirmState === 'idle' ? t('gameDesktop.control.selectNumbers') : confirmState === 'insufficient' ? t('gameDesktop.control.insufficientBalance') : confirmState === 'limit' ? t('gameDesktop.control.betLimitExceeded') : confirmState === 'submitting' ? t('gameDesktop.control.submitting') : t('gameDesktop.control.confirm'), confirmState, isConfirmClickable: confirmState === 'ready' && !isAutoHosting, onChipSelect: selectChip, onDecreaseBetQuantity: () => adjustBetQuantity(-1), onIncreaseBetQuantity: () => { if (!canIncreaseBetQuantity) { notify.warning(t('commonUi.toast.betLimitExceeded')) return } adjustBetQuantity(1) }, onConfirm: handleConfirm, onClearSelections: clearSelections, onOpenAutoSetting: handleOpenAutoSetting, onRepeatSelections: handleRepeatSelections, maxSelectionCountLabel: maxSelectionCount, selectedBetQuantityLabel: activeBetQuantity, selectedChipId: activeChipId, selectedCountLabel: selections.length, totalBetAmountLabel: formatChipDisplayValue(totalBetAmount), chips: chipItems, } }