//
//  ScenicQueueViewModel.swift
//  suixinkan
//

import Foundation

/// 排队管理首页 ViewModel，对齐 Android `ScenicQueueViewModel`。
final class ScenicQueueViewModel {
    private enum Constants {
        static let pageSize = 10
        static let typeCurrent = 1
        static let typeFinished = 2
        static let prepareCallLoopGapNanos: UInt64 = 800_000_000
    }

    private(set) var summary: ScenicQueueSummary = .empty
    private(set) var currentQueue: [ScenicQueueTicket] = []
    private(set) var skippedQueue: [ScenicQueueSkippedTicket] = []
    private(set) var currentRefreshing = false
    private(set) var skippedRefreshing = false
    private(set) var currentLoadingMore = false
    private(set) var skippedLoadingMore = false
    private(set) var currentCanLoadMore = false
    private(set) var skippedCanLoadMore = false
    private(set) var queueGatePassed = false
    private(set) var selectedPunchSpotLine = "--"
    private(set) var shootingTimedQueueNo: String?
    private(set) var shootingRemainSeconds = 0
    private(set) var showStartShootingButton = true
    private(set) var quickCallButtonEnabled = false
    private(set) var prepareCallButtonEnabled = false
    private(set) var prepareCallPlaying = false
    private(set) var pendingSkip: ScenicQueuePendingSkip?
    private(set) var pendingCall: ScenicQueuePendingCall?
    private(set) var pendingFinish: ScenicQueuePendingFinish?
    private(set) var pendingMark: ScenicQueuePendingMark?
    private(set) var pendingRequeue: ScenicQueuePendingRequeue?

    var onStateChange: (() -> Void)?
    var onShowMessage: ((String) -> Void)?
    var onRequireSettings: (() -> Void)?
    var onNavigateSettings: (() -> Void)?
    var onNavigateBack: (() -> Void)?

    private let appStore: AppStore
    private let ttsManager: ScenicQueueTTSManaging
    private let socketClient: ScenicQueueSocketClient
    private var currentListTotal = 0
    private var skippedListTotal = 0
    private var currentLastLoadedPage = 0
    private var skippedLastLoadedPage = 0
    private var pendingRemoteCalledRecordIds: Set<Int64> = []
    private var handledRemoteCallEventIds: Set<String> = []
    private var shootingTask: Task<Void, Never>?
    private var prepareCallTask: Task<Void, Never>?

    init(
        appStore: AppStore = .shared,
        ttsManager: ScenicQueueTTSManaging = AliyunNLSTTSManager.shared,
        socketClient: ScenicQueueSocketClient = ScenicQueueSocketClient()
    ) {
        self.appStore = appStore
        self.ttsManager = ttsManager
        self.socketClient = socketClient
        self.socketClient.onMessage = { [weak self] message in
            self?.handleSocketMessage(message)
        }
        refreshSelectedPunchSpotLine()
        refreshQueueGateLocally()
    }

    deinit {
        stopQueueStatsPolling()
        shootingTask?.cancel()
        prepareCallTask?.cancel()
        ttsManager.stopQueueCallLoop()
        ttsManager.stopCustomTextLoop()
    }

    /// 刷新排队 gate，配置过打卡点才加载数据。
    func refreshQueueGate(api: ScenicQueueAPIProtocol) async {
        refreshSelectedPunchSpotLine()
        refreshQueueGateLocally()
        if queueGatePassed {
            await syncQueueSettingFromServerIfMatchesLocal(api: api)
            await refreshQueueStats(api: api)
            await refreshCurrentQueue(api: api, reset: true)
            await refreshSkippedQueue(api: api, reset: true)
            await ttsManager.prepareQueueTTSEngine(api: api)
        } else {
            resetQueues()
            onRequireSettings?()
        }
    }

    /// 开始 WebSocket 订阅。
    func startQueueStatsPolling(api: ScenicQueueAPIProtocol) {
        guard queueGatePassed,
              let scenicId = Int64("\(appStore.session.currentScenicId)"),
              let scenicSpotId = Int64(appStore.scenicQueue.punchSpotId),
              scenicId > 0,
              scenicSpotId > 0 else { return }
        Task {
            do {
                let token = try await api.socketToken().socketToken
                socketClient.connect(socketToken: token, scenicId: scenicId, scenicSpotId: scenicSpotId)
            } catch {
                onShowMessage?(error.localizedDescription)
            }
        }
    }

    /// 停止 WebSocket 订阅。
    func stopQueueStatsPolling() {
        socketClient.disconnect()
    }

    /// 打开设置页。
    func openSettings() {
        if shootingTimedQueueNo != nil {
            onShowMessage?("请先完成上一位拍摄，才可以进入设置页面。")
            return
        }
        onNavigateSettings?()
    }

    /// 退出排队模块。
    func exitQueueModule() {
        onNavigateBack?()
    }

    /// 刷新当前排队列表。
    func refreshCurrentQueue(api: ScenicQueueAPIProtocol, reset: Bool = true) async {
        await loadHomeList(api: api, type: Constants.typeCurrent, page: 1, append: false, isRefresh: reset)
    }

    /// 加载更多当前排队。
    func loadMoreCurrentQueue(api: ScenicQueueAPIProtocol) async {
        guard currentCanLoadMore, !currentRefreshing, !currentLoadingMore else { return }
        await loadHomeList(api: api, type: Constants.typeCurrent, page: currentLastLoadedPage + 1, append: true, isRefresh: false)
    }

    /// 刷新过号列表。
    func refreshSkippedQueue(api: ScenicQueueAPIProtocol, reset: Bool = true) async {
        await loadHomeList(api: api, type: Constants.typeFinished, page: 1, append: false, isRefresh: reset)
    }

    /// 加载更多过号列表。
    func loadMoreSkippedQueue(api: ScenicQueueAPIProtocol) async {
        guard skippedCanLoadMore, !skippedRefreshing, !skippedLoadingMore else { return }
        await loadHomeList(api: api, type: Constants.typeFinished, page: skippedLastLoadedPage + 1, append: true, isRefresh: false)
    }

    /// 刷新统计。
    func refreshQueueStats(api: ScenicQueueAPIProtocol) async {
        guard let ids = currentScenicAndSpotIds() else { return }
        do {
            let data = try await api.stats(scenicId: ids.scenicId, scenicSpotId: ids.scenicSpotId)
            let avg = data.avgWaitMin.isFinite ? max(0, Int(data.avgWaitMin.rounded())) : 0
            summary = ScenicQueueSummary(peopleInQueue: max(0, data.queueCount), avgWaitMinutes: avg)
            notifyStateChange()
        } catch {
            // Android ignores stats errors silently.
        }
    }

    /// 请求标记用户。
    func onTicketMarkClick(recordId: Int64, queueNo: String, uid: Int64) {
        guard recordId > 0, uid > 0, !queueNo.isEmpty,
              let ticket = currentQueue.first(where: { $0.recordId == recordId }) else { return }
        pendingMark = ScenicQueuePendingMark(
            uid: uid,
            queueNo: queueNo,
            phoneMasked: ticket.phoneMasked,
            markAsFreelancePhotog: ticket.markAsFreelancePhotog
        )
        notifyStateChange()
    }

    func dismissMarkDialog() {
        pendingMark = nil
        notifyStateChange()
    }

    /// 确认用户标记。
    func confirmUserMark(api: ScenicQueueAPIProtocol, markAsFreelancePhotog: Bool, queueBanDays: Int) async {
        guard let pendingMark else { return }
        guard appStore.session.currentScenicId > 0 else {
            onShowMessage?("请先选择景区")
            return
        }
        let request = ScenicQueueUserMarkRequest(
            uid: pendingMark.uid,
            scenicId: Int64(appStore.session.currentScenicId),
            markAsFreelancePhotog: markAsFreelancePhotog ? 1 : 0,
            queueBanDays: markAsFreelancePhotog ? min(max(queueBanDays, 0), 999) : nil,
            operatorId: Int(appStore.session.userId)
        )
        do {
            try await api.userMark(request)
            dismissMarkDialog()
            await refreshCurrentQueue(api: api, reset: true)
        } catch {
            onShowMessage?(error.localizedDescription)
        }
    }

    func requestSkipTicket(recordId: Int64, queueNo: String) {
        guard recordId > 0, !queueNo.isEmpty else { return }
        pendingSkip = ScenicQueuePendingSkip(recordId: recordId, queueNo: queueNo)
        notifyStateChange()
    }

    func dismissSkipDialog() {
        pendingSkip = nil
        notifyStateChange()
    }

    /// 确认过号。
    func confirmSkipTicket(api: ScenicQueueAPIProtocol) async {
        guard let pending = pendingSkip else { return }
        dismissSkipDialog()
        ttsManager.stopQueueCallLoop()
        do {
            _ = try await api.pass(recordId: pending.recordId)
            clearShootingSession(for: pending.queueNo)
            removeTicketFromCurrentList(recordId: pending.recordId)
            await refreshQueueStats(api: api)
            await refreshSkippedQueue(api: api, reset: true)
        } catch {
            onShowMessage?(error.localizedDescription)
        }
    }

    func requestFinishConfirm(recordId: Int64, queueNo: String) {
        guard recordId > 0, !queueNo.isEmpty else { return }
        if let active = shootingTimedQueueNo, active != queueNo {
            onShowMessage?("请先完成上一位拍摄。")
            return
        }
        pendingFinish = ScenicQueuePendingFinish(recordId: recordId, queueNo: queueNo)
        notifyStateChange()
    }

    func dismissFinishDialog() {
        pendingFinish = nil
        notifyStateChange()
    }

    /// 确认完成。
    func confirmFinishTicket(api: ScenicQueueAPIProtocol) async {
        guard let pending = pendingFinish else { return }
        dismissFinishDialog()
        await finishTicket(api: api, pending: pending)
    }

    func requestRequeueConfirm(recordId: Int64, queueNo: String) {
        guard recordId > 0, !queueNo.isEmpty else { return }
        pendingRequeue = ScenicQueuePendingRequeue(recordId: recordId, queueNo: queueNo)
        notifyStateChange()
    }

    func dismissRequeueDialog() {
        pendingRequeue = nil
        notifyStateChange()
    }

    /// 确认重新排队。
    func confirmRequeueTicket(api: ScenicQueueAPIProtocol) async {
        guard let pending = pendingRequeue else { return }
        dismissRequeueDialog()
        guard let operatorId = Int(appStore.session.userId), operatorId > 0 else {
            onShowMessage?("请先登录")
            return
        }
        do {
            try await api.requeueInsertBefore(recordId: pending.recordId, operatorId: operatorId)
            await refreshQueueStats(api: api)
            await refreshCurrentQueue(api: api, reset: true)
            await refreshSkippedQueue(api: api, reset: true)
        } catch {
            onShowMessage?(error.localizedDescription)
        }
    }

    func requestCallConfirm(recordId: Int64, queueNo: String, isRecall: Bool = false) {
        if shootingTimedQueueNo != nil {
            onShowMessage?("请先完成上一位拍摄，再进行叫号。")
            return
        }
        guard recordId > 0, !queueNo.isEmpty else { return }
        pendingCall = ScenicQueuePendingCall(recordId: recordId, queueNo: queueNo, isRecall: isRecall)
        notifyStateChange()
    }

    func dismissCallConfirmDialog() {
        pendingCall = nil
        notifyStateChange()
    }

    /// 确认叫号。
    func confirmCallTicket(api: ScenicQueueAPIProtocol) async {
        guard let pending = pendingCall else { return }
        dismissCallConfirmDialog()
        stopPrepareCallPlayback()
        await callTicketDirectly(api: api, recordId: pending.recordId, queueNo: pending.queueNo)
    }

    /// 快速叫号第一位。
    func quickCallFirstTicket(api: ScenicQueueAPIProtocol) async {
        if shootingTimedQueueNo != nil {
            onShowMessage?("请先完成上一位拍摄，再进行叫号。")
            return
        }
        guard let first = currentQueue.first else {
            onShowMessage?("暂无可叫号的排队用户")
            return
        }
        stopPrepareCallPlayback()
        await callTicketDirectly(api: api, recordId: first.recordId, queueNo: first.queueNo)
    }

    /// 切换准备叫号播报。
    func togglePrepareCallPlayback() {
        if prepareCallPlaying {
            stopPrepareCallPlayback()
            return
        }
        let queueNos = prepareCallQueueNos()
        guard !queueNos.isEmpty else {
            onShowMessage?("暂无可播报的排队用户")
            return
        }
        prepareCallTask?.cancel()
        prepareCallPlaying = true
        notifyStateChange()
        prepareCallTask = Task { [weak self] in
            guard let self else { return }
            while !Task.isCancelled {
                let latest = self.prepareCallQueueNos()
                if latest.isEmpty { break }
                _ = await self.ttsManager.speakPlainTextOnce(self.prepareCallAnnouncementText(latest))
                try? await Task.sleep(nanoseconds: Constants.prepareCallLoopGapNanos)
            }
            self.prepareCallPlaying = false
            self.prepareCallTask = nil
            self.notifyStateChange()
        }
    }

    /// 开始拍摄倒计时。
    func startShooting(api: ScenicQueueAPIProtocol, recordId: Int64, queueNo: String) {
        guard recordId > 0, !queueNo.isEmpty, shootingTimedQueueNo == nil else { return }
        stopPrepareCallPlayback()
        ttsManager.stopQueueCallLoop()
        let timing = resolveShootingTiming()
        guard timing.totalSeconds > 0 else {
            onShowMessage?("请先在排队配置中设置拍摄时长")
            return
        }
        onShowMessage?("开始计时拍摄")
        shootingTimedQueueNo = queueNo
        shootingRemainSeconds = timing.totalSeconds
        notifyStateChange()
        shootingTask?.cancel()
        shootingTask = Task { [weak self] in
            guard let self else { return }
            await self.speakFollowingTickets(after: queueNo)
            let start = Date()
            while !Task.isCancelled {
                let elapsed = Int(Date().timeIntervalSince(start).rounded(.down))
                let remain = max(0, timing.totalSeconds - elapsed)
                self.shootingRemainSeconds = remain
                self.notifyStateChange()
                if remain <= 0 { break }
                if remain == timing.readableThresholdSeconds {
                    _ = await self.ttsManager.speakPlainTextOnce("当前剩余拍摄时间还剩\(remain)秒")
                } else if remain < timing.readableThresholdSeconds && remain > 0 {
                    _ = await self.ttsManager.speakPlainTextOnce("\(remain)")
                } else if timing.intervalSeconds > 0 && elapsed > 0 && elapsed % timing.intervalSeconds == 0 {
                    _ = await self.ttsManager.speakPlainTextOnce(self.shootingRemainAnnouncementText(queueNo: queueNo, remainSeconds: remain))
                }
                try? await Task.sleep(nanoseconds: 1_000_000_000)
            }
            self.ttsManager.interruptShootingAnnouncement()
            self.shootingTimedQueueNo = nil
            self.shootingRemainSeconds = 0
            self.shootingTask = nil
            self.notifyStateChange()
            if !Task.isCancelled {
                await self.finishTicket(api: api, pending: ScenicQueuePendingFinish(recordId: recordId, queueNo: queueNo))
            }
        }
    }

    private func refreshSelectedPunchSpotLine() {
        selectedPunchSpotLine = appStore.scenicQueue.hasPunchSpotSaved()
            ? appStore.scenicQueue.punchSpotName.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "--"
            : "--"
        refreshShootingCallConfig()
    }

    private func refreshQueueGateLocally() {
        queueGatePassed = appStore.scenicQueue.hasPunchSpotSaved()
    }

    private func refreshShootingCallConfig() {
        let snapshot = appStore.scenicQueue.settingsSnapshot() ?? ScenicQueueSettingsSnapshot()
        showStartShootingButton = snapshot.showStartShootingButton
        quickCallButtonEnabled = snapshot.quickCallButtonEnabled
        prepareCallButtonEnabled = snapshot.prepareCallButtonEnabled
    }

    private func syncQueueSettingFromServerIfMatchesLocal(api: ScenicQueueAPIProtocol) async {
        guard let ids = currentScenicAndSpotIds() else { return }
        do {
            let data = try await api.setting(scenicId: ids.scenicId, scenicSpotId: ids.scenicSpotId)
            guard data.exists, let setting = data.setting,
                  setting.scenicId == nil || setting.scenicId == ids.scenicId,
                  setting.scenicSpotId == nil || setting.scenicSpotId == ids.scenicSpotId else { return }
            let local = appStore.scenicQueue.settingsSnapshot() ?? ScenicQueueSettingsSnapshot()
            appStore.scenicQueue.saveSettingsSnapshot(setting.toSnapshot(fallback: local))
            appStore.scenicQueue.remark = setting.remark ?? ""
            if let voices = setting.voiceBroadcasts {
                appStore.scenicQueue.savePresetVoices(
                    voices
                        .sorted { ($0.sortOrder ?? Int.max) < ($1.sortOrder ?? Int.max) }
                        .compactMap { $0.content?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty }
                )
            }
            if let name = setting.scenicSpotName?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty {
                appStore.scenicQueue.punchSpotName = name
                selectedPunchSpotLine = name
            }
            refreshShootingCallConfig()
            notifyStateChange()
        } catch {
            // Android keeps local snapshot if sync fails.
        }
    }

    private func loadHomeList(api: ScenicQueueAPIProtocol, type: Int, page: Int, append: Bool, isRefresh: Bool) async {
        guard let ids = currentScenicAndSpotIds() else { return }
        let isCurrent = type == Constants.typeCurrent
        if isRefresh {
            if isCurrent {
                guard !currentRefreshing else { return }
                currentRefreshing = true
            } else {
                guard !skippedRefreshing else { return }
                skippedRefreshing = true
            }
        } else {
            if isCurrent {
                guard !currentLoadingMore else { return }
                currentLoadingMore = true
            } else {
                guard !skippedLoadingMore else { return }
                skippedLoadingMore = true
            }
        }
        notifyStateChange()
        defer {
            if isRefresh {
                if isCurrent { currentRefreshing = false } else { skippedRefreshing = false }
            } else {
                if isCurrent { currentLoadingMore = false } else { skippedLoadingMore = false }
            }
            notifyStateChange()
        }

        do {
            let data = try await api.home(
                scenicId: ids.scenicId,
                scenicSpotId: ids.scenicSpotId,
                type: type,
                page: page,
                pageSize: Constants.pageSize
            )
            guard let block = data.list else { return }
            if isCurrent {
                let mapped = block.list.map { $0.toCurrentTicket() }
                currentQueue = append ? appendUnique(existing: currentQueue, incoming: mapped, key: \.recordId) : mapped
                currentListTotal = max(0, block.total)
                currentLastLoadedPage = max(1, block.page)
                currentCanLoadMore = currentQueue.count < currentListTotal && !block.list.isEmpty
                consumePendingRemoteCalledTickets()
            } else {
                let mapped = block.list.map { $0.toSkippedTicket() }
                skippedQueue = append ? appendUnique(existing: skippedQueue, incoming: mapped, key: \.recordId) : mapped
                skippedListTotal = max(0, block.total)
                skippedLastLoadedPage = max(1, block.page)
                skippedCanLoadMore = skippedQueue.count < skippedListTotal && !block.list.isEmpty
            }
        } catch {
            onShowMessage?(error.localizedDescription)
        }
    }

    private func handleSocketMessage(_ message: ScenicQueueSocketMessage) {
        guard let params = message.data?.params,
              let changedSpotId = params.scenicSpotId,
              let currentSpotId = Int64(appStore.scenicQueue.punchSpotId),
              changedSpotId == currentSpotId else { return }
        switch message.data?.action {
        case ScenicQueueSocketAction.queueUpdated.rawValue:
            NotificationCenter.default.post(name: NotificationName.scenicQueueDidUpdate, object: nil)
        case ScenicQueueSocketAction.ticketCalled.rawValue:
            handleRemoteTicketCalled(params)
            NotificationCenter.default.post(name: NotificationName.scenicQueueDidUpdate, object: nil)
        default:
            break
        }
    }

    private func handleRemoteTicketCalled(_ params: ScenicQueueSocketParams) {
        guard let recordId = params.recordId, recordId > 0 else { return }
        if let myUid = Int64(appStore.session.userId), let operatorUid = params.operatorUid, myUid == operatorUid {
            return
        }
        let eventId = params.eventId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        if !eventId.isEmpty, handledRemoteCallEventIds.contains(eventId) {
            return
        }
        if !eventId.isEmpty {
            handledRemoteCallEventIds.insert(eventId)
        }
        pendingRemoteCalledRecordIds.insert(recordId)
    }

    private func consumePendingRemoteCalledTickets() {
        guard let called = currentQueue.first(where: { pendingRemoteCalledRecordIds.contains($0.recordId) && $0.status == .called }) else { return }
        pendingRemoteCalledRecordIds.remove(called.recordId)
        stopPrepareCallPlayback()
        ttsManager.startQueueCallLoop(queueNo: called.queueNo)
    }

    private func callTicketDirectly(api: ScenicQueueAPIProtocol, recordId: Int64, queueNo: String) async {
        do {
            let data = try await api.call(recordId: recordId)
            applyCallSuccessLocally(recordId: recordId, queueNo: queueNo, statusText: data.statusText)
        } catch {
            onShowMessage?(error.localizedDescription)
        }
    }

    private func applyCallSuccessLocally(recordId: Int64, queueNo: String, statusText: String) {
        let shouldShowStartButton = currentShowStartShootingButton()
        if shouldShowStartButton {
            ttsManager.startQueueCallLoop(queueNo: queueNo)
        }
        currentQueue = currentQueue.map { ticket in
            ticket.recordId == recordId || ticket.queueNo == queueNo
                ? ticket.markedCalled(statusText: statusText)
                : ticket
        }
        notifyStateChange()
    }

    private func finishTicket(api: ScenicQueueAPIProtocol, pending: ScenicQueuePendingFinish) async {
        do {
            let data = try await api.finish(recordId: pending.recordId)
            ttsManager.stopQueueCallLoop()
            clearShootingSession(for: pending.queueNo)
            removeTicketFromCurrentList(recordId: pending.recordId)
            if !data.statusText.isEmpty {
                onShowMessage?(data.statusText)
            }
            await refreshQueueStats(api: api)
            await refreshSkippedQueue(api: api, reset: true)
        } catch {
            onShowMessage?(error.localizedDescription)
        }
    }

    private func removeTicketFromCurrentList(recordId: Int64) {
        currentQueue.removeAll { $0.recordId == recordId }
        currentListTotal = max(0, currentListTotal - 1)
        currentCanLoadMore = currentListTotal > 0 && currentQueue.count < currentListTotal
        notifyStateChange()
    }

    private func clearShootingSession(for queueNo: String) {
        guard shootingTimedQueueNo == queueNo else { return }
        shootingTask?.cancel()
        shootingTask = nil
        ttsManager.interruptShootingAnnouncement()
        shootingTimedQueueNo = nil
        shootingRemainSeconds = 0
        notifyStateChange()
    }

    private func stopPrepareCallPlayback() {
        prepareCallTask?.cancel()
        prepareCallTask = nil
        ttsManager.interruptShootingAnnouncement()
        prepareCallPlaying = false
        notifyStateChange()
    }

    private func prepareCallQueueNos() -> [String] {
        let active = shootingTimedQueueNo?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        return currentQueue
            .map { $0.queueNo.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty && (active.isEmpty || $0 != active) }
            .prefix(3)
            .map { $0 }
    }

    private func prepareCallAnnouncementText(_ queueNos: [String]) -> String {
        let joined = queueNos.map(AliyunNLSTTSManager.formatQueueNoForTTS).joined(separator: "、")
        return "\(joined)，请提前做好拍摄准备。"
    }

    private func resolveShootingTiming() -> (totalSeconds: Int, intervalSeconds: Int, readableThresholdSeconds: Int) {
        let snapshot = appStore.scenicQueue.settingsSnapshot() ?? ScenicQueueSettingsSnapshot()
        let total = max(0, snapshot.shootMinute * 60 + snapshot.shootSecond)
        let interval = (40...60).contains(snapshot.broadcastIntervalSec) ? snapshot.broadcastIntervalSec : 50
        let readable = (10...30).contains(snapshot.countdownThresholdSec) ? snapshot.countdownThresholdSec : 15
        return (total, interval, readable)
    }

    private func currentShowStartShootingButton() -> Bool {
        refreshShootingCallConfig()
        return showStartShootingButton
    }

    private func speakFollowingTickets(after queueNo: String) async {
        let count = (appStore.scenicQueue.settingsSnapshot() ?? ScenicQueueSettingsSnapshot()).autoCallAheadCount.clamped(to: 0...5)
        guard count > 0, let index = currentQueue.firstIndex(where: { $0.queueNo == queueNo }) else { return }
        let nextNos = currentQueue.dropFirst(index + 1).prefix(count).map(\.queueNo).filter { !$0.isEmpty }
        guard !nextNos.isEmpty else { return }
        _ = await ttsManager.speakPlainTextOnce("请\(nextNos.map(AliyunNLSTTSManager.formatQueueNoForTTS).joined(separator: "、"))提前做好拍摄准备。")
    }

    private func shootingRemainAnnouncementText(queueNo: String, remainSeconds: Int) -> String {
        "\(AliyunNLSTTSManager.formatQueueNoForTTS(queueNo))，当前剩余拍摄时间还剩\(remainSeconds)秒"
    }

    private func currentScenicAndSpotIds() -> (scenicId: Int64, scenicSpotId: Int64)? {
        let scenicId = Int64(appStore.session.currentScenicId)
        guard scenicId > 0,
              let scenicSpotId = Int64(appStore.scenicQueue.punchSpotId),
              scenicSpotId > 0 else { return nil }
        return (scenicId, scenicSpotId)
    }

    private func resetQueues() {
        summary = .empty
        currentQueue = []
        skippedQueue = []
        currentListTotal = 0
        skippedListTotal = 0
        currentLastLoadedPage = 0
        skippedLastLoadedPage = 0
        currentCanLoadMore = false
        skippedCanLoadMore = false
        notifyStateChange()
    }

    private func appendUnique<Item, Key: Hashable>(existing: [Item], incoming: [Item], key: KeyPath<Item, Key>) -> [Item] {
        var seen = Set(existing.map { $0[keyPath: key] })
        return existing + incoming.filter { seen.insert($0[keyPath: key]).inserted }
    }

    private func notifyStateChange() {
        onStateChange?()
    }
}

private extension String {
    var nonEmpty: String? {
        isEmpty ? nil : self
    }
}

private extension Int {
    func clamped(to range: ClosedRange<Int>) -> Int {
        Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
    }
}
