//
//  ScenicQueueSettingsViewController.swift
//  suixinkan
//

import SnapKit
import UIKit

/// 排队设置页，按 Android `ScenicQueueSettingsScreen` 提供三页持久化表单。
final class ScenicQueueSettingsViewController: BaseViewController {
    private let viewModel: ScenicQueueSettingsViewModel
    private let api: ScenicQueueAPIProtocol
    private let tabStrip = ScenicQueueTabStripView(
        titles: ScenicQueueSettingsTab.allCases.map(\.title))
    private let pageController = UIPageViewController(
        transitionStyle: .scroll, navigationOrientation: .horizontal)
    private let saveBar = UIView()
    private let saveButton = UIButton(type: .system)
    private var pages: [ScenicQueueSettingsPageController] = []
    private var numericFields: [String: ScenicQueueNumericFieldView] = [:]
    private var switches: [String: ScenicQueueSwitchRow] = [:]
    private let punchSpotButton = UIButton(type: .system)
    private var selectedPunchLabel = UILabel()
    private var punchSpotOptionsLoading = false
    private var punchSpotOptionsLoaded = false
    private var startTimeButton = UIButton(type: .system)
    private var endTimeButton = UIButton(type: .system)
    private var customTextView = UITextView()
    private var customPlayButton = UIButton(type: .system)
    private var presetStack = UIStackView()
    private var presetSummaryLabel = UILabel()

    @MainActor
    init(
        api: ScenicQueueAPIProtocol? = nil,
        viewModel: ScenicQueueSettingsViewModel? = nil
    ) {
        self.api = api ?? NetworkServices.shared.scenicQueueAPI
        self.viewModel = viewModel ?? ScenicQueueSettingsViewModel()
        super.init(nibName: nil, bundle: nil)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

    override func setupNavigationBar() {
        title = "设置"
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            title: "配置日志", style: .plain, target: self, action: #selector(logTapped))
    }

    override func setupUI() {
        view.backgroundColor = ScenicQueueTokens.pageBackground
        pageController.dataSource = self
        pageController.delegate = self
        addChild(pageController)
        view.addSubview(tabStrip)
        view.addSubview(pageController.view)
        pageController.didMove(toParent: self)

        pages = ScenicQueueSettingsTab.allCases.map { ScenicQueueSettingsPageController(tab: $0) }
        buildBasicPage(pages[0])
        buildNotifyPage(pages[1])
        buildVoicePage(pages[2])
        pageController.setViewControllers([pages[0]], direction: .forward, animated: false)

        saveBar.backgroundColor = .white
        saveBar.layer.shadowColor = UIColor.black.cgColor
        saveBar.layer.shadowOpacity = 0.08
        saveBar.layer.shadowRadius = 6
        saveBar.layer.shadowOffset = CGSize(width: 0, height: -2)
        saveButton.setTitle("保存", for: .normal)
        saveButton.setTitleColor(.white, for: .normal)
        saveButton.titleLabel?.font = ScenicQueueTokens.settingsSaveButtonFont
        saveButton.backgroundColor = ScenicQueueTokens.bannerBlue
        saveButton.layer.cornerRadius = ScenicQueueTokens.bottomSaveButtonRadius
        saveBar.addSubview(saveButton)
        view.addSubview(saveBar)
        applyState()
    }

    override func setupConstraints() {
        tabStrip.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide)
            make.leading.trailing.equalToSuperview()
            make.height.equalTo(48)
        }
        saveBar.snp.makeConstraints { make in make.leading.trailing.bottom.equalToSuperview() }
        saveButton.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(16)
            make.leading.trailing.equalToSuperview().inset(31)
            make.height.equalTo(48)
            make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-16)
        }
        pageController.view.snp.makeConstraints { make in
            make.top.equalTo(tabStrip.snp.bottom)
            make.leading.trailing.equalToSuperview()
            make.bottom.equalTo(saveBar.snp.top)
        }
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        loadPunchSpotMenuIfNeeded()
    }

    override func bindActions() {
        tabStrip.onSelect = { [weak self] index in self?.selectPage(index, animated: true) }
        saveButton.addTarget(self, action: #selector(saveTapped), for: .touchUpInside)
        viewModel.onStateChange = { [weak self] in Task { @MainActor in self?.applyState() } }
        viewModel.onShowMessage = { [weak self] message in
            Task { @MainActor in self?.showToast(message) }
        }
        viewModel.onNavigateBack = { [weak self] in
            Task { @MainActor in self?.navigationController?.popViewController(animated: true) }
        }
        viewModel.onNavigateLog = { [weak self] in
            Task { @MainActor in
                self?.navigationController?.pushViewController(
                    ScenicQueueSettingChangeLogViewController(), animated: true)
            }
        }
    }

    private func selectPage(_ index: Int, animated: Bool) {
        guard pages.indices.contains(index) else { return }
        let current = viewModel.settingsTab.rawValue
        let direction: UIPageViewController.NavigationDirection = index >= current ? .forward : .reverse
        viewModel.setSettingsTab(ScenicQueueSettingsTab(rawValue: index) ?? .basic)
        pageController.setViewControllers([pages[index]], direction: direction, animated: animated)
        tabStrip.setSelectedIndex(index, animated: animated)
    }

    private func applyState() {
        let selected = viewModel.settingsTab.rawValue
        tabStrip.setSelectedIndex(selected, animated: true)
        if pageController.viewControllers?.first !== pages[selected] {
            pageController.setViewControllers([pages[selected]], direction: .forward, animated: false)
        }
        selectedPunchLabel.text = viewModel.selectedPunchSpotDisplayText
        configurePunchSpotMenu()
        startTimeButton.setTitle(viewModel.businessStartTime, for: .normal)
        endTimeButton.setTitle(viewModel.businessEndTime, for: .normal)
        updateField(
            "queueDistanceMeter",
            ScenicQueueSettingsViewModel.kilometersDisplay(fromMeters: viewModel.queueDistanceMeter))
        updateField("shootMinute", "\(viewModel.shootMinute)")
        updateField("shootSecond", "\(viewModel.shootSecond)")
        updateField("queueTakeLimit", "\(viewModel.queueTakeLimit)")
        updateField("missCallRequeueOffset", "\(viewModel.missCallRequeueOffset)")
        updateField("autoCallAheadCount", "\(viewModel.autoCallAheadCount)")
        updateField("firstNoticeAhead", "\(viewModel.firstAheadCount)")
        updateField("secondNoticeAhead", "\(viewModel.secondAheadCount)")
        updateField("broadcastIntervalSec", "\(viewModel.broadcastIntervalSec)")
        updateField("countdownReadableThresholdSec", "\(viewModel.countdownThresholdSec)")
        setSwitch("businessOpen", viewModel.businessOpen)
        setSwitch("showShoot", viewModel.showStartShootingButton)
        setSwitch("quickCall", viewModel.quickCallButtonEnabled)
        setSwitch("prepareCall", viewModel.prepareCallButtonEnabled)
        setSwitch("firstSms", viewModel.firstSms)
        setSwitch("firstPhone", viewModel.firstPhone)
        setSwitch("secondSms", viewModel.secondSms)
        setSwitch("secondPhone", viewModel.secondPhone)
        if !customTextView.isFirstResponder { customTextView.text = viewModel.customTTSText }
        customTextView.isEditable = !viewModel.isCustomTTSLooping
        customPlayButton.setTitle(viewModel.isCustomTTSLooping ? "停止播放" : "试听", for: .normal)
        rebuildPresetRows()
        viewModel.highlightedInvalidFieldKeys.forEach { numericFields[$0]?.setInvalid(true) }
    }

    private func updateField(_ key: String, _ value: String) {
        let field = numericFields[key]
        field?.setInvalid(viewModel.highlightedInvalidFieldKeys.contains(key))
        field?.setDisplayValue(value)
    }

    private func setSwitch(_ key: String, _ value: Bool) { switches[key]?.setValue(value) }

    private func buildBasicPage(_ page: ScenicQueueSettingsPageController) {
        page.stack.addArrangedSubview(tipCard())
        page.stack.addArrangedSubview(punchSpotCard())
        page.stack.addArrangedSubview(businessCard())
        page.stack.addArrangedSubview(
            sectionCard(title: "排队距离限制") {
                let hint = self.hintLabel("顾客仅可在该距离范围内取号，0 表示不限制。合法范围 0～9999.99 千米。")
                let field = self.numericField(key: "queueDistanceMeter", unit: "千米", keyboard: .decimalPad)
                return [hint, field]
            })
        page.stack.addArrangedSubview(
            sectionCard(title: "预计拍摄时长") {
                let minute = self.numericField(key: "shootMinute", unit: "分")
                let second = self.numericField(key: "shootSecond", unit: "秒")
                let row = UIStackView(arrangedSubviews: [minute, second])
                row.axis = .horizontal
                row.spacing = 12
                row.distribution = .fillEqually
                return [self.hintLabel("总时长必须大于 0；分钟 0～30，秒 0～59。"), row]
            })
        page.stack.addArrangedSubview(
            sectionCard(title: "单人取号次数限制") {
                [
                    self.hintLabel("0 表示不限制，最大 999999 次。"),
                    self.numericField(key: "queueTakeLimit", unit: "次"),
                ]
            })
        page.stack.addArrangedSubview(
            sectionCard(title: "过号重新排队") {
                [
                    self.hintLabel("设置过号重新排队时相对队首顺延的位数，范围 1～9999。"),
                    self.numericField(key: "missCallRequeueOffset", unit: "位"),
                ]
            })
        page.stack.addArrangedSubview(
            sectionCard(title: "拍摄与叫号配置", info: true) {
                let shoot = self.switchRow(
                    key: "showShoot", title: "显示开始拍摄按钮", action: #selector(self.showShootChanged(_:)))
                let auto = self.numericField(key: "autoCallAheadCount", unit: "人")
                return [shoot, self.divider(), self.hintLabel("开始拍摄后自动通知后续 0～5 个排队号。"), auto]
            })
        page.stack.addArrangedSubview(
            sectionCard(title: "底部叫号按钮") {
                [
                    self.switchRow(
                        key: "quickCall", title: "显示快速叫号", action: #selector(self.quickCallChanged(_:))),
                    self.divider(),
                    self.switchRow(
                        key: "prepareCall", title: "显示准备叫号", action: #selector(self.prepareCallChanged(_:))),
                ]
            })
    }

    private func buildNotifyPage(_ page: ScenicQueueSettingsPageController) {
        page.stack.addArrangedSubview(
            sectionCard(title: "提前通知规则", info: true) {
                [
                    self.sectionLabel("第一次提前通知"),
                    self.hintLabel("前方人数达到阈值时触发，范围 0～50 位。"),
                    self.numericField(key: "firstNoticeAhead", unit: "位"),
                    self.switchRow(
                        key: "firstSms", title: "短信通知", action: #selector(self.firstSmsChanged(_:))),
                    self.switchRow(
                        key: "firstPhone", title: "电话通知(自动语音)", action: #selector(self.firstPhoneChanged(_:))),
                    self.divider(), self.sectionLabel("第二次提前通知"),
                    self.hintLabel("可设置更接近队首的二次提醒，范围 0～50 位。"),
                    self.numericField(key: "secondNoticeAhead", unit: "位"),
                    self.switchRow(
                        key: "secondSms", title: "短信通知", action: #selector(self.secondSmsChanged(_:))),
                    self.switchRow(
                        key: "secondPhone", title: "电话通知(自动语音)", action: #selector(self.secondPhoneChanged(_:))),
                ]
            })
    }

    private func buildVoicePage(_ page: ScenicQueueSettingsPageController) {
        page.stack.addArrangedSubview(
            sectionCard(title: "倒计时播报配置", info: true) {
                let test = self.primaryButton("播放测试声音", action: #selector(self.testSoundTapped))
                return [
                    self.sectionLabel("播报间隔时间"), self.hintLabel("从开拍起按间隔播报剩余时间，合法范围 40～60 秒。"),
                    self.numericField(key: "broadcastIntervalSec", unit: "秒"),
                    self.sectionLabel("进入读秒倒计时阈值"), self.hintLabel("到达阈值后逐秒播报，合法范围 10～30 秒。"),
                    self.numericField(key: "countdownReadableThresholdSec", unit: "秒"),
                    self.sectionLabel("测试蓝牙音响"), test,
                ]
            })
        page.stack.addArrangedSubview(customTTSCard())
        page.stack.addArrangedSubview(presetVoicesCard())
    }

    private func tipCard() -> UIView {
        let card = ScenicQueueSettingCard()
        card.backgroundColor = ScenicQueueTokens.bannerBlue
        card.layer.borderWidth = 0
        let title = sectionLabel("温馨提示")
        title.textColor = .white
        let body = hintLabel("配置信息修改后，请及时测试叫号设备状态")
        body.textColor = UIColor.white.withAlphaComponent(0.92)
        let stack = verticalStack([title, body], spacing: 6)
        card.addSubview(stack)
        stack.snp.makeConstraints {
            $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 20, left: 16, bottom: 20, right: 16))
        }
        return card
    }

    private func punchSpotCard() -> UIView {
        sectionCard(title: "打卡点信息") {
            let label = self.hintLabel("选择打卡点")
            let selector = self.punchSpotButton
            selector.backgroundColor = .white
            selector.layer.cornerRadius = ScenicQueueTokens.radiusInput
            selector.layer.borderWidth = 1
            selector.layer.borderColor = ScenicQueueTokens.fieldBorderMuted.cgColor
            selector.showsMenuAsPrimaryAction = true
            self.selectedPunchLabel.font = ScenicQueueTokens.settingsRowTitleFont
            self.selectedPunchLabel.textColor = AppColor.textPrimary
            let arrow = UILabel()
            arrow.text = "▼"
            arrow.font = ScenicQueueTokens.settingsHintFont
            arrow.textColor = AppColor.textTertiary
            selector.addSubview(self.selectedPunchLabel)
            selector.addSubview(arrow)
            self.selectedPunchLabel.snp.makeConstraints { make in
                make.leading.equalToSuperview().offset(12)
                make.centerY.equalToSuperview()
                make.trailing.lessThanOrEqualTo(arrow.snp.leading).offset(-8)
            }
            arrow.snp.makeConstraints { make in
                make.trailing.equalToSuperview().offset(-12)
                make.centerY.equalToSuperview()
            }
            selector.snp.makeConstraints { $0.height.equalTo(52) }
            let qrLabel = self.sectionLabel("点击保存打卡点排队二维码")
            let qr = self.compactPrimaryButton("保存二维码", action: #selector(self.saveQRTapped))
            let qrRow = UIStackView(arrangedSubviews: [qrLabel, qr])
            qrRow.axis = .horizontal
            qrRow.alignment = .center
            qrRow.spacing = 10
            return [label, selector, qrRow]
        }
    }

    private func businessCard() -> UIView {
        sectionCard(title: "排队状态与时间", info: true) {
            let status = self.switchRow(
                key: "businessOpen", title: "当前排队状态", action: #selector(self.businessOpenChanged(_:)))
            self.startTimeButton = self.timeButton(action: #selector(self.startTimeTapped))
            self.endTimeButton = self.timeButton(action: #selector(self.endTimeTapped))
            let times = UIStackView(arrangedSubviews: [
                self.labeledControl("营业开始时间", self.startTimeButton),
                self.labeledControl("营业结束时间", self.endTimeButton),
            ])
            times.axis = .horizontal
            times.distribution = .fillEqually
            times.spacing = 12
            return [status, self.hintLabel("开启后顾客可在营业时间内扫码取号；关闭后暂停排队。"), self.divider(), times]
        }
    }

    private func customTTSCard() -> UIView {
        sectionCard(title: "自定义文本语音播报") {
            self.customTextView.font = ScenicQueueTokens.settingsRowTitleFont
            self.customTextView.textColor = AppColor.textPrimary
            self.customTextView.layer.cornerRadius = ScenicQueueTokens.radiusInput
            self.customTextView.layer.borderWidth = 1
            self.customTextView.layer.borderColor = ScenicQueueTokens.fieldBorderMuted.cgColor
            self.customTextView.delegate = self
            self.customTextView.textContainerInset = UIEdgeInsets(top: 12, left: 8, bottom: 12, right: 8)
            self.customTextView.snp.makeConstraints { $0.height.equalTo(104) }
            self.customPlayButton = self.outlinedButton("试听", action: #selector(self.customPlayTapped))
            let save = self.primaryButton("保存", action: #selector(self.savePresetTapped))
            let row = UIStackView(arrangedSubviews: [self.customPlayButton, save])
            row.axis = .horizontal
            row.distribution = .fillEqually
            row.spacing = 12
            return [self.customTextView, row]
        }
    }

    private func presetVoicesCard() -> UIView {
        let card = ScenicQueueSettingCard()
        presetSummaryLabel.font = ScenicQueueTokens.settingsSectionTitleFont
        presetSummaryLabel.textColor = AppColor.textPrimary
        presetStack.axis = .vertical
        presetStack.spacing = 10
        let hint = hintLabel("左滑显示删除；点左侧图标循环播报该条。底部「保存」提交全部排队参数。")
        let stack = verticalStack([presetSummaryLabel, presetStack, hint], spacing: 14)
        card.addSubview(stack)
        stack.snp.makeConstraints { $0.edges.equalToSuperview().inset(16) }
        return card
    }

    private func rebuildPresetRows() {
        presetSummaryLabel.text = "已保存的语音(\(viewModel.presetVoices.count)/5)"
        presetStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
        guard !viewModel.presetVoices.isEmpty else {
            presetStack.addArrangedSubview(hintLabel("暂无预设文案，输入后点击「保存」添加到本地列表（最多 5 条）。"))
            return
        }
        for (index, phrase) in viewModel.presetVoices.enumerated() {
            let playing =
                viewModel.isCustomTTSLooping
                && viewModel.ttsLoopAnchorText?.trimmingCharacters(in: .whitespacesAndNewlines)
                    == phrase.trimmingCharacters(in: .whitespacesAndNewlines)
            let row = ScenicQueuePresetVoiceRow(text: phrase, playing: playing)
            row.tag = index
            row.onPlay = { [weak self] in
                guard let self else { return }
                if playing {
                    self.viewModel.toggleCustomTTSPlayback()
                } else {
                    self.viewModel.startPresetVoiceLoop(phrase)
                }
            }
            row.onDelete = { [weak self] in self?.viewModel.deletePresetVoice(at: index) }
            presetStack.addArrangedSubview(row)
        }
    }

    private func sectionCard(title: String, info: Bool = false, content: () -> [UIView]) -> UIView {
        let card = ScenicQueueSettingCard()
        let heading = sectionLabel(title)
        if info { heading.text = "\(title)  ⓘ" }
        let stack = verticalStack([heading] + content(), spacing: 12)
        card.addSubview(stack)
        stack.snp.makeConstraints { $0.edges.equalToSuperview().inset(16) }
        return card
    }

    private func numericField(key: String, unit: String, keyboard: UIKeyboardType = .numberPad)
        -> ScenicQueueNumericFieldView
    {
        let field = ScenicQueueNumericFieldView(key: key, unit: unit, keyboard: keyboard)
        field.delegate = self
        numericFields[key] = field
        return field
    }

    private func switchRow(key: String, title: String, action: Selector) -> ScenicQueueSwitchRow {
        let row = ScenicQueueSwitchRow(title: title)
        row.toggle.addTarget(self, action: action, for: .valueChanged)
        switches[key] = row
        return row
    }

    private func verticalStack(_ views: [UIView], spacing: CGFloat) -> UIStackView {
        let stack = UIStackView(arrangedSubviews: views)
        stack.axis = .vertical
        stack.spacing = spacing
        return stack
    }

    private func sectionLabel(_ text: String) -> UILabel {
        let label = UILabel()
        label.text = text
        label.font = ScenicQueueTokens.settingsSectionTitleFont
        label.textColor = AppColor.textPrimary
        label.numberOfLines = 0
        return label
    }

    private func hintLabel(_ text: String) -> UILabel {
        let label = UILabel()
        label.text = text
        label.font = ScenicQueueTokens.settingsHintFont
        label.textColor = AppColor.textTertiary
        label.numberOfLines = 0
        return label
    }

    private func divider() -> UIView {
        let view = UIView()
        view.backgroundColor = ScenicQueueTokens.cardOutline
        view.snp.makeConstraints { $0.height.equalTo(1 / UIScreen.main.scale) }
        return view
    }

    private func primaryButton(_ title: String, action: Selector) -> UIButton {
        let button = compactPrimaryButton(title, action: action)
        button.snp.makeConstraints { $0.height.equalTo(44) }
        return button
    }

    private func compactPrimaryButton(_ title: String, action: Selector) -> UIButton {
        let button = UIButton(type: .system)
        button.setTitle(title, for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.titleLabel?.font = ScenicQueueTokens.settingsRowTitleFont
        button.backgroundColor = ScenicQueueTokens.bannerBlue
        button.layer.cornerRadius = ScenicQueueTokens.radiusInput
        button.addTarget(self, action: action, for: .touchUpInside)
        button.setConfigurationContentInsets(
            NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
        )
        return button
    }

    private func outlinedButton(_ title: String, action: Selector) -> UIButton {
        let button = UIButton(type: .system)
        button.setTitle(title, for: .normal)
        button.setTitleColor(ScenicQueueTokens.bannerBlue, for: .normal)
        button.titleLabel?.font = ScenicQueueTokens.settingsRowTitleFont
        button.layer.cornerRadius = ScenicQueueTokens.radiusInput
        button.layer.borderWidth = 1
        button.layer.borderColor = ScenicQueueTokens.bannerBlue.cgColor
        button.addTarget(self, action: action, for: .touchUpInside)
        button.snp.makeConstraints { $0.height.equalTo(44) }
        return button
    }

    private func timeButton(action: Selector) -> UIButton {
        let button = outlinedButton("--:--", action: action)
        button.setTitleColor(AppColor.textPrimary, for: .normal)
        button.layer.borderColor = ScenicQueueTokens.fieldBorderMuted.cgColor
        return button
    }

    private func labeledControl(_ title: String, _ control: UIView) -> UIView {
        let label = hintLabel(title)
        let stack = verticalStack([label, control], spacing: 8)
        return stack
    }

    private func loadPunchSpotMenuIfNeeded() {
        guard !punchSpotOptionsLoaded, !punchSpotOptionsLoading else { return }
        punchSpotOptionsLoading = true
        configurePunchSpotMenu()
        Task {
            await viewModel.loadPunchSpotOptions(api: api)
            punchSpotOptionsLoading = false
            punchSpotOptionsLoaded = true
            configurePunchSpotMenu()
        }
    }

    private func configurePunchSpotMenu() {
        if punchSpotOptionsLoading {
            punchSpotButton.menu = UIMenu(children: [
                UIAction(title: "正在加载打卡点…", attributes: .disabled) { _ in }
            ])
            return
        }
        let actions = viewModel.punchSpotOptions.map { option in
            UIAction(
                title: option.label,
                state: option.id == viewModel.selectedPunchSpotId ? .on : .off
            ) { [weak self] _ in
                guard let self else { return }
                Task { await self.viewModel.setPunchSpot(option, api: self.api) }
            }
        }
        punchSpotButton.menu = UIMenu(
            options: .singleSelection,
            children: actions.isEmpty
                ? [UIAction(title: "暂无可选打卡点", attributes: .disabled) { _ in }]
                : actions
        )
    }

    private func presentTimePicker(initial: String, setter: @escaping (String) -> Void) {
        present(
            ScenicQueueTimePickerViewController(initialTime: initial, onConfirm: setter), animated: true)
    }

    @objc private func logTapped() { viewModel.openSettingChangeLog() }
    @objc private func saveTapped() {
        view.endEditing(true)
        Task { await viewModel.save(api: api) }
    }
    @objc private func saveQRTapped() { Task { await viewModel.saveShootQueueQRCode(api: api) } }
    @objc private func startTimeTapped() {
        presentTimePicker(initial: viewModel.businessStartTime, setter: viewModel.setBusinessStartTime)
    }
    @objc private func endTimeTapped() {
        presentTimePicker(initial: viewModel.businessEndTime, setter: viewModel.setBusinessEndTime)
    }
    @objc private func testSoundTapped() { viewModel.playBluetoothTestSound() }
    @objc private func customPlayTapped() { viewModel.toggleCustomTTSPlayback() }
    @objc private func savePresetTapped() {
        viewModel.setCustomTTSText(customTextView.text ?? "")
        viewModel.saveCurrentTextAsPresetVoiceLocal()
    }
    @objc private func businessOpenChanged(_ sender: UISwitch) { viewModel.toggleBusinessOpen() }
    @objc private func showShootChanged(_ sender: UISwitch) {
        viewModel.toggleShowStartShootingButton()
    }
    @objc private func quickCallChanged(_ sender: UISwitch) {
        viewModel.toggleQuickCallButtonEnabled()
    }
    @objc private func prepareCallChanged(_ sender: UISwitch) {
        viewModel.togglePrepareCallButtonEnabled()
    }
    @objc private func firstSmsChanged(_ sender: UISwitch) { viewModel.toggleFirstSms() }
    @objc private func firstPhoneChanged(_ sender: UISwitch) { viewModel.toggleFirstPhone() }
    @objc private func secondSmsChanged(_ sender: UISwitch) { viewModel.toggleSecondSms() }
    @objc private func secondPhoneChanged(_ sender: UISwitch) { viewModel.toggleSecondPhone() }
}

extension ScenicQueueSettingsViewController: UIPageViewControllerDataSource,
    UIPageViewControllerDelegate
{
    func pageViewController(
        _ pageViewController: UIPageViewController,
        viewControllerBefore viewController: UIViewController
    ) -> UIViewController? {
        guard let page = viewController as? ScenicQueueSettingsPageController,
            page.settingsTab.rawValue > 0
        else { return nil }
        return pages[page.settingsTab.rawValue - 1]
    }

    func pageViewController(
        _ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController
    ) -> UIViewController? {
        guard let page = viewController as? ScenicQueueSettingsPageController,
            page.settingsTab.rawValue < pages.count - 1
        else { return nil }
        return pages[page.settingsTab.rawValue + 1]
    }

    func pageViewController(
        _ pageViewController: UIPageViewController, didFinishAnimating finished: Bool,
        previousViewControllers: [UIViewController], transitionCompleted completed: Bool
    ) {
        guard completed,
            let page = pageViewController.viewControllers?.first as? ScenicQueueSettingsPageController
        else { return }
        viewModel.setSettingsTab(page.settingsTab)
        tabStrip.setSelectedIndex(page.settingsTab.rawValue, animated: true)
    }
}

extension ScenicQueueSettingsViewController: ScenicQueueNumericFieldViewDelegate {
    func numericFieldView(_ field: ScenicQueueNumericFieldView, commit raw: String, key: String)
        -> Bool
    {
        func integer(_ range: ClosedRange<Int>, setter: (Int) -> Void) -> Bool {
            guard let value = Int(raw), range.contains(value) else { return false }
            setter(value)
            return true
        }
        let valid: Bool
        switch key {
        case "queueDistanceMeter":
            if let meters = ScenicQueueSettingsViewModel.meters(fromKilometersText: raw) {
                viewModel.setQueueDistanceMeter(meters)
                valid = true
            } else {
                valid = false
            }
        case "shootMinute": valid = integer(0...30, setter: viewModel.setShootMinute)
        case "shootSecond": valid = integer(0...59, setter: viewModel.setShootSecond)
        case "queueTakeLimit": valid = integer(0...999_999, setter: viewModel.setQueueTakeLimit)
        case "missCallRequeueOffset":
            valid = integer(1...9_999, setter: viewModel.setMissCallRequeueOffset)
        case "autoCallAheadCount": valid = integer(0...5, setter: viewModel.setAutoCallAheadCount)
        case "firstNoticeAhead": valid = integer(0...50, setter: viewModel.setFirstAheadCount)
        case "secondNoticeAhead": valid = integer(0...50, setter: viewModel.setSecondAheadCount)
        case "broadcastIntervalSec": valid = integer(40...60, setter: viewModel.setBroadcastIntervalSec)
        case "countdownReadableThresholdSec":
            valid = integer(10...30, setter: viewModel.setCountdownThresholdSec)
        default: valid = false
        }
        viewModel.reportOutOfRangeNumericPending(fieldKey: key, pending: !valid)
        if !valid { showToast("输入值超出允许范围") }
        return valid
    }
}

extension ScenicQueueSettingsViewController: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        if textView.text.count > 255 { textView.text = String(textView.text.prefix(255)) }
        viewModel.setCustomTTSText(textView.text)
    }
}

/// 设置页单个分页的持久化滚动容器。
final class ScenicQueueSettingsPageController: UIViewController {
    let settingsTab: ScenicQueueSettingsTab
    let stack = UIStackView()
    private let scrollView = UIScrollView()

    init(tab: ScenicQueueSettingsTab) {
        self.settingsTab = tab
        super.init(nibName: nil, bundle: nil)
    }
    @available(*, unavailable) required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = ScenicQueueTokens.pageBackground
        scrollView.keyboardDismissMode = .onDrag
        scrollView.alwaysBounceVertical = true
        stack.axis = .vertical
        stack.spacing = 14
        view.addSubview(scrollView)
        scrollView.addSubview(stack)
        scrollView.snp.makeConstraints { $0.edges.equalToSuperview() }
        stack.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(UIEdgeInsets(top: 12, left: 16, bottom: 16, right: 16))
            make.width.equalToSuperview().offset(-32)
        }
    }
}

/// 设置页白色描边卡片。
final class ScenicQueueSettingCard: UIView {
    init() {
        super.init(frame: .zero)
        backgroundColor = .white
        layer.cornerRadius = ScenicQueueTokens.radiusCard
        layer.borderWidth = 1
        layer.borderColor = ScenicQueueTokens.cardOutline.cgColor
    }
    @available(*, unavailable) required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

/// 数字输入视图提交协议。
protocol ScenicQueueNumericFieldViewDelegate: AnyObject {
    func numericFieldView(_ field: ScenicQueueNumericFieldView, commit raw: String, key: String)
        -> Bool
}

/// Android OutlinedTextField 样式的数字与单位输入视图。
final class ScenicQueueNumericFieldView: UIView, UITextFieldDelegate {
    let key: String
    weak var delegate: ScenicQueueNumericFieldViewDelegate?
    private let textField = UITextField()
    private var invalid = false

    init(key: String, unit: String, keyboard: UIKeyboardType) {
        self.key = key
        super.init(frame: .zero)
        let unitLabel = UILabel()
        unitLabel.text = unit
        unitLabel.font = ScenicQueueTokens.settingsHintFont
        unitLabel.textColor = AppColor.textSecondary
        unitLabel.textAlignment = .right
        unitLabel.setContentHuggingPriority(.required, for: .horizontal)
        unitLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
        textField.keyboardType = keyboard
        textField.font = ScenicQueueTokens.settingsRowTitleFont
        textField.textColor = AppColor.textPrimary
        textField.delegate = self
        layer.cornerRadius = ScenicQueueTokens.radiusInput
        layer.borderWidth = 1
        layer.borderColor = ScenicQueueTokens.fieldBorderMuted.cgColor
        addSubview(textField)
        addSubview(unitLabel)
        let focusTap = UITapGestureRecognizer(target: self, action: #selector(focusInput))
        focusTap.cancelsTouchesInView = false
        addGestureRecognizer(focusTap)
        unitLabel.snp.makeConstraints { make in
            make.trailing.equalToSuperview().inset(8)
            make.centerY.equalToSuperview()
        }
        textField.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(12)
            make.trailing.equalTo(unitLabel.snp.leading).offset(-8)
            make.top.bottom.equalToSuperview()
        }
        snp.makeConstraints { $0.height.equalTo(52) }
    }
    @available(*, unavailable) required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setDisplayValue(_ value: String) {
        if !textField.isFirstResponder && !invalid { textField.text = value }
    }
    func setInvalid(_ value: Bool) {
        invalid = value
        layer.borderColor = (value ? AppColor.danger : ScenicQueueTokens.fieldBorderMuted).cgColor
    }
    @objc private func focusInput() { textField.becomeFirstResponder() }
    func textFieldDidBeginEditing(_ textField: UITextField) {
        layer.borderColor = ScenicQueueTokens.bannerBlue.cgColor
    }
    func textFieldDidEndEditing(_ textField: UITextField) {
        let ok = delegate?.numericFieldView(self, commit: textField.text ?? "", key: key) ?? false
        setInvalid(!ok)
    }
}

/// 设置页标题与开关行。
final class ScenicQueueSwitchRow: UIView {
    let toggle = UISwitch()
    init(title: String) {
        super.init(frame: .zero)
        let label = UILabel()
        label.text = title
        label.font = ScenicQueueTokens.settingsRowTitleFont
        label.textColor = AppColor.textPrimary
        label.numberOfLines = 0
        toggle.onTintColor = ScenicQueueTokens.bannerBlue
        addSubview(label)
        addSubview(toggle)
        label.snp.makeConstraints { make in
            make.leading.top.bottom.equalToSuperview()
            make.trailing.lessThanOrEqualTo(toggle.snp.leading).offset(-12)
        }
        toggle.snp.makeConstraints { make in make.trailing.centerY.equalToSuperview() }
        snp.makeConstraints { $0.height.greaterThanOrEqualTo(44) }
    }
    @available(*, unavailable) required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func setValue(_ value: Bool) { toggle.setOn(value, animated: false) }
}

/// 已保存语音的播放与左滑删除行。
final class ScenicQueuePresetVoiceRow: UIView {
    var onPlay: (() -> Void)?
    var onDelete: (() -> Void)?
    private let content = UIView()
    private let deleteButton = UIButton(type: .system)
    private var revealed = false

    init(text: String, playing: Bool) {
        super.init(frame: .zero)
        layer.cornerRadius = ScenicQueueTokens.radiusInput
        clipsToBounds = true
        deleteButton.setTitle("删除", for: .normal)
        deleteButton.setTitleColor(.white, for: .normal)
        deleteButton.backgroundColor = UIColor(hex: 0xE53935)
        deleteButton.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
        content.backgroundColor = .white
        content.layer.borderWidth = 1
        content.layer.borderColor = ScenicQueueTokens.cardOutline.cgColor
        content.layer.cornerRadius = ScenicQueueTokens.radiusInput
        let play = UIButton(type: .system)
        play.setImage(UIImage(systemName: playing ? "pause.fill" : "play.fill"), for: .normal)
        play.tintColor = ScenicQueueTokens.bannerBlue
        play.addTarget(self, action: #selector(playTapped), for: .touchUpInside)
        let label = UILabel()
        label.text = text
        label.font = ScenicQueueTokens.settingsRowTitleFont
        label.textColor = AppColor.textPrimary
        label.numberOfLines = 3
        addSubview(deleteButton)
        addSubview(content)
        content.addSubview(play)
        content.addSubview(label)
        deleteButton.snp.makeConstraints { make in
            make.trailing.top.bottom.equalToSuperview()
            make.width.equalTo(80)
        }
        content.snp.makeConstraints { $0.edges.equalToSuperview() }
        play.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(4)
            make.centerY.equalToSuperview()
            make.size.equalTo(44)
        }
        label.snp.makeConstraints { make in
            make.leading.equalTo(play.snp.trailing).offset(4)
            make.trailing.equalToSuperview().offset(-12)
            make.top.bottom.equalToSuperview().inset(12)
        }
        addGestureRecognizer(UISwipeGestureRecognizer(target: self, action: #selector(swiped(_:))))
        (gestureRecognizers?.last as? UISwipeGestureRecognizer)?.direction = .left
        let right = UISwipeGestureRecognizer(target: self, action: #selector(swiped(_:)))
        right.direction = .right
        addGestureRecognizer(right)
        snp.makeConstraints { $0.height.greaterThanOrEqualTo(52) }
    }
    @available(*, unavailable) required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    @objc private func playTapped() { onPlay?() }
    @objc private func deleteTapped() { onDelete?() }
    @objc private func swiped(_ gesture: UISwipeGestureRecognizer) {
        revealed = gesture.direction == .left
        content.snp.updateConstraints { make in
            make.leading.equalToSuperview().offset(revealed ? -80 : 0)
            make.trailing.equalToSuperview().offset(revealed ? -80 : 0)
        }
        UIView.animate(withDuration: 0.2) { self.layoutIfNeeded() }
    }
}

/// 营业时间的 Android 风格居中时间选择弹层。
final class ScenicQueueTimePickerViewController: UIViewController {
    private let onConfirm: (String) -> Void
    private let picker = UIDatePicker()
    init(initialTime: String, onConfirm: @escaping (String) -> Void) {
        self.onConfirm = onConfirm
        super.init(nibName: nil, bundle: nil)
        modalPresentationStyle = .overFullScreen
        modalTransitionStyle = .crossDissolve
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm"
        picker.date = formatter.date(from: initialTime) ?? Date()
    }
    @available(*, unavailable) required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.black.withAlphaComponent(0.42)
        let card = UIView()
        card.backgroundColor = .white
        card.layer.cornerRadius = ScenicQueueTokens.radiusDialog
        picker.datePickerMode = .time
        picker.preferredDatePickerStyle = .wheels
        picker.locale = Locale(identifier: "zh_CN")
        let cancel = button("取消", filled: false, action: #selector(cancelTapped))
        let confirm = button("确认", filled: true, action: #selector(confirmTapped))
        let row = UIStackView(arrangedSubviews: [cancel, confirm])
        row.axis = .horizontal
        row.spacing = 12
        row.distribution = .fillEqually
        view.addSubview(card)
        card.addSubview(picker)
        card.addSubview(row)
        card.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.leading.trailing.equalToSuperview().inset(32)
        }
        picker.snp.makeConstraints { make in
            make.top.leading.trailing.equalToSuperview()
            make.height.equalTo(190)
        }
        row.snp.makeConstraints { make in
            make.top.equalTo(picker.snp.bottom)
            make.leading.trailing.bottom.equalToSuperview().inset(20)
            make.height.equalTo(44)
        }
    }
    private func button(_ title: String, filled: Bool, action: Selector) -> UIButton {
        let b = UIButton(type: .system)
        b.setTitle(title, for: .normal)
        b.setTitleColor(filled ? .white : ScenicQueueTokens.bannerBlue, for: .normal)
        b.backgroundColor =
            filled ? ScenicQueueTokens.bannerBlue : ScenicQueueTokens.dialogCancelBackground
        b.layer.cornerRadius = 8
        b.addTarget(self, action: action, for: .touchUpInside)
        return b
    }
    @objc private func cancelTapped() { dismiss(animated: true) }
    @objc private func confirmTapped() {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm"
        let value = formatter.string(from: picker.date)
        dismiss(animated: true) { self.onConfirm(value) }
    }
}
