//
//  ChatViewController.swift
//  Dialog_Demo
//
//  语音对话主界面
//

import UIKit
import multimodal_dialog
import SnapKit
import AVFoundation
import MultimodalDialogTongyiMetathings

// MARK: - ChatViewController

class ChatViewController: UIViewController, AudioHandlerDelegate {

    // MARK: - Business Properties

    var HOST = ""
    var API_KEY = ""
    var WORKSPACE_ID = ""
    var APP_ID = ""
    /// 计费模式（payg 后付费 / license License 计费）
    var payMode: String = PayMode.payg.payMode
    var isconnected: Bool
    var conversation: MultiModalDialog?
    var isInDialog = false
    var audioManager: AudioManager?
    var image_url: String?
    var myState: DialogState?
    var useKws = false
    /// 对话模式（由 FirstViewController 选择后写入）
    var dialogMode: DialogMode = .duplex
    /// 标记是否为新的 AI 回复轮次，用于在第一条 responding 消息到来时创建 AI 条目
    private var isNewAIResponse = false

    // MARK: - UI Properties

    /// 渐变背景层（浅蓝色调）
    private lazy var gradientLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        layer.colors = [
            UIColor(red: 0xE8/255.0, green: 0xF4/255.0, blue: 0xFD/255.0, alpha: 1.0).cgColor,
            UIColor(red: 0xD6/255.0, green: 0xEA/255.0, blue: 0xF8/255.0, alpha: 1.0).cgColor,
            UIColor(red: 0xEB/255.0, green: 0xF5/255.0, blue: 0xFB/255.0, alpha: 1.0).cgColor
        ]
        layer.locations = [0, 0.5, 1.0]
        layer.startPoint = CGPoint(x: 0.5, y: 0)
        layer.endPoint = CGPoint(x: 0.5, y: 1)
        return layer
    }()

    /// 顶部导航栏容器
    private lazy var navBar: UIView = {
        let v = UIView()
        v.backgroundColor = .clear
        return v
    }()

    /// 关闭按钮
    private lazy var closeButton: UIButton = {
        let btn = UIButton(type: .system)
        btn.setImage(UIImage(systemName: "xmark"), for: .normal)
        btn.tintColor = UIColor(red: 0x33/255.0, green: 0x33/255.0, blue: 0x33/255.0, alpha: 1.0)
        btn.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
        return btn
    }()

    /// 连接状态指示灯
    private lazy var statusDot: UIView = {
        let v = UIView()
        v.layer.cornerRadius = 4
        v.backgroundColor = .systemYellow
        return v
    }()

    /// 调试按钮
    private lazy var debugButton: UIButton = {
        let btn = UIButton(type: .system)
        btn.setImage(UIImage(systemName: "ellipsis.circle"), for: .normal)
        btn.tintColor = UIColor(red: 0x33/255.0, green: 0x33/255.0, blue: 0x33/255.0, alpha: 1.0)
        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(debugLongPressed(_:)))
        btn.addGestureRecognizer(longPress)
        return btn
    }()

    /// 打开音频文件目录按钮
    private lazy var openFolderButton: UIButton = {
        let btn = UIButton(type: .system)
        btn.setImage(UIImage(systemName: "folder"), for: .normal)
        btn.tintColor = UIColor(red: 0x33/255.0, green: 0x33/255.0, blue: 0x33/255.0, alpha: 1.0)
        btn.addTarget(self, action: #selector(openVoicesFolderTapped), for: .touchUpInside)
        return btn
    }()

    /// 波形视图
    private lazy var voiceWaveView: VoiceWaveView = {
        let v = VoiceWaveView()
        return v
    }()

    /// 状态文字标签（显示在顶部导航栏中间）
    private lazy var statusLabel: UILabel = {
        let label = UILabel()
        label.text = "等待连接..."
        label.font = UIFont.systemFont(ofSize: 14, weight: .medium)
        label.textColor = UIColor(red: 0x33/255.0, green: 0x33/255.0, blue: 0x33/255.0, alpha: 1.0)
        label.textAlignment = .center
        return label
    }()

    /// 字幕视图
    private lazy var subtitleView: SubtitleView = {
        let v = SubtitleView()
        return v
    }()

    /// 底部操作栏容器
    private lazy var bottomBar: UIView = {
        let v = UIView()
        v.backgroundColor = .clear
        return v
    }()
    
    /// 长胶囊按钮（tap2talk / push2talk 模式下展示，根据状态切换文案与行为）
    private lazy var capsuleButton: UIButton = {
        let btn = UIButton(type: .system)
        btn.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
        btn.setTitleColor(.white, for: .normal)
        btn.backgroundColor = UIColor(red: 0x4A/255.0, green: 0x9E/255.0, blue: 0xFF/255.0, alpha: 1.0)
        btn.layer.cornerRadius = 28
        btn.clipsToBounds = false
        btn.layer.shadowColor = UIColor.black.cgColor
        btn.layer.shadowOpacity = 0.15
        btn.layer.shadowOffset = CGSize(width: 0, height: 2)
        btn.layer.shadowRadius = 6
        btn.isHidden = true
        return btn
    }()

    /// 结束按钮
    private lazy var endButton: UIButton = {
        let btn = UIButton(type: .system)
        btn.setImage(UIImage(systemName: "phone.down.fill"), for: .normal)
        btn.tintColor = .white
        btn.backgroundColor = UIColor.systemRed
        btn.layer.cornerRadius = 28
        btn.clipsToBounds = false
        btn.layer.shadowColor = UIColor.systemRed.cgColor
        btn.layer.shadowOpacity = 0.3
        btn.layer.shadowOffset = CGSize(width: 0, height: 2)
        btn.layer.shadowRadius = 6
        btn.addTarget(self, action: #selector(endTapped), for: .touchUpInside)
        return btn
    }()

    /// 调试信息浮层
    private lazy var debugOverlay: UIView = {
        let v = UIView()
        v.backgroundColor = UIColor.black.withAlphaComponent(0.75)
        v.layer.cornerRadius = 12
        v.clipsToBounds = true
        v.isHidden = true
        return v
    }()

    /// 调试信息文本
    private lazy var debugLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
        label.textColor = UIColor.systemGreen
        label.numberOfLines = 0
        label.text = ""
        return label
    }()

    /// 连接失败重试浮层
    private lazy var retryOverlay: UIView = {
        let v = UIView()
        v.backgroundColor = UIColor.black.withAlphaComponent(0.8)
        v.layer.cornerRadius = 16
        v.clipsToBounds = true
        v.isHidden = true
        return v
    }()

    private lazy var retryErrorLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 14, weight: .medium)
        label.textColor = .white
        label.numberOfLines = 0
        label.textAlignment = .center
        return label
    }()

    private lazy var retryButton: UIButton = {
        let btn = UIButton(type: .system)
        btn.setTitle("重试", for: .normal)
        btn.tintColor = .white
        btn.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.8)
        btn.layer.cornerRadius = 8
        btn.addTarget(self, action: #selector(retryTapped), for: .touchUpInside)
        return btn
    }()

    private lazy var backButton: UIButton = {
        let btn = UIButton(type: .system)
        btn.setTitle("返回", for: .normal)
        btn.tintColor = .white
        btn.backgroundColor = UIColor(white: 1.0, alpha: 0.2)
        btn.layer.cornerRadius = 8
        btn.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
        return btn
    }()

    /// 胶囊按钮当前模式
    private enum CapsuleMode {
        case speak      // 说话（tap2talk 点击 / push2talk 按住）
        case interrupt  // 打断（点击调用 interrupt）
    }
    private var capsuleMode: CapsuleMode = .speak

    /// 触觉反馈
    private let impactFeedback = UIImpactFeedbackGenerator(style: .medium)

    /// 是否已收到 onConversationStarted（用于在 idle 状态下区分未连接 / 已连接等待用户说话）
    private var isConversationStarted = false

    // MARK: - Initialization

    init() {
        self.isconnected = false
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit {
        self.conversation?.stop()
    }

    // MARK: - Public Configuration

    public func updateParam(url: String, apiKey: String, workSpaceId: String, appId: String, image_url: String, payMode: String = PayMode.payg.payMode) {
        self.HOST = url
        self.API_KEY = apiKey
        self.image_url = image_url
        self.WORKSPACE_ID = workSpaceId
        self.APP_ID = appId
        self.payMode = payMode

        self.audioManager = AudioManager()
        self.audioManager?.setPlayerSampleRate(16000)
        self.audioManager?.setAudioHandlerDelegate(self)

        self.conversation = MultiModalDialog(url: self.HOST, chainMode: ChainMode.WebSocket,
                                             workSpaceId: self.WORKSPACE_ID,
                                             appId: self.APP_ID, mode: self.dialogMode)
        // 打开语音唤醒功能
        if useKws {
            self.conversation?.enableKWS(enableKws: true, wakeupOnly: false)
        }
        TYVoiceChatManager.debug = true


        self.conversation?.setModel(model: "multimodal-dialog")
    }

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        print("==================viewDidLoad")
        setupDialogCallbacks()
        setupUI()
        connect()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        gradientLayer.frame = view.bounds
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        TYLogger.shared.debug("==========viewWillDisappear")
        self.isInDialog = false
        self.audioManager?.cleanPlayerBuffer()
        self.audioManager?.stopPlaying()
        self.audioManager?.stopRecording(notify: false)
        self.conversation?.stop()
        voiceWaveView.stopAnimating()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        TYLogger.shared.debug("==========viewDidDisappear")
    }

    override var preferredStatusBarStyle: UIStatusBarStyle { .darkContent }

    // MARK: - UI Setup

    private func setupUI() {
        // 渐变背景
        view.layer.insertSublayer(gradientLayer, at: 0)

        // 顶部导航栏
        view.addSubview(navBar)
        navBar.addSubview(closeButton)
        navBar.addSubview(openFolderButton)
        navBar.addSubview(statusDot)
        navBar.addSubview(statusLabel)

        navBar.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide)
            make.leading.trailing.equalToSuperview()
            make.height.equalTo(44)
        }
        closeButton.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(16)
            make.centerY.equalToSuperview()
            make.width.height.equalTo(44)
        }
        openFolderButton.snp.makeConstraints { make in
            make.trailing.equalToSuperview().offset(-16)
            make.centerY.equalToSuperview()
            make.width.height.equalTo(44)
        }
        statusDot.snp.makeConstraints { make in
            make.centerY.equalToSuperview()
            make.trailing.equalTo(statusLabel.snp.leading).offset(-6)
            make.width.height.equalTo(8)
        }
        statusLabel.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }

        // VoiceWaveView
        view.addSubview(voiceWaveView)
        voiceWaveView.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.top.equalTo(view.safeAreaLayoutGuide).offset(100)
        }
        voiceWaveView.startAnimating()

        // 底部操作栏
        view.addSubview(bottomBar)
        bottomBar.addSubview(capsuleButton)
        bottomBar.addSubview(endButton)

        bottomBar.snp.makeConstraints { make in
            make.leading.trailing.equalToSuperview()
            make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-20)
            make.height.equalTo(56)
        }

        if dialogMode == .duplex {
            // duplex 模式：只显示居中的结束键
            capsuleButton.isHidden = true
            endButton.snp.makeConstraints { make in
                make.center.equalToSuperview()
                make.width.height.equalTo(56)
            }
        } else {
            // tap2talk / push2talk：左侧长胶囊 + 右侧结束键
            capsuleButton.isHidden = false
            endButton.snp.makeConstraints { make in
                make.centerY.equalToSuperview()
                make.trailing.equalToSuperview().offset(-20)
                make.width.height.equalTo(56)
            }
            capsuleButton.snp.makeConstraints { make in
                make.centerY.equalToSuperview()
                make.leading.equalToSuperview().offset(20)
                make.trailing.equalTo(endButton.snp.leading).offset(-12)
                make.height.equalTo(56)
            }
        }

        // 根据模式配置胶囊按钮行为
        configureCapsuleButton()

        // SubtitleView（紧接在球体下方，填充到底部操作栏上方）
        view.addSubview(subtitleView)
        subtitleView.snp.makeConstraints { make in
            make.leading.trailing.equalToSuperview()
            make.top.equalTo(voiceWaveView.snp.bottom).offset(30)
            make.bottom.equalTo(bottomBar.snp.top).offset(-12)
        }

        // 调试信息浮层
        view.addSubview(debugOverlay)
        debugOverlay.addSubview(debugLabel)
        debugOverlay.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(16)
            make.trailing.equalToSuperview().offset(-16)
            make.bottom.equalTo(subtitleView.snp.top).offset(-12)
            make.height.greaterThanOrEqualTo(60)
        }
        debugLabel.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(12)
        }

        // 连接失败重试浮层
        view.addSubview(retryOverlay)
        retryOverlay.addSubview(retryErrorLabel)
        retryOverlay.addSubview(retryButton)
        retryOverlay.addSubview(backButton)

        retryOverlay.snp.makeConstraints { make in
            make.center.equalToSuperview()
            make.width.equalTo(280)
        }
        retryErrorLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(24)
            make.leading.trailing.equalToSuperview().inset(20)
        }
        retryButton.snp.makeConstraints { make in
            make.top.equalTo(retryErrorLabel.snp.bottom).offset(20)
            make.leading.equalToSuperview().offset(20)
            make.trailing.equalTo(retryOverlay.snp.centerX).offset(-8)
            make.height.equalTo(40)
            make.bottom.equalToSuperview().offset(-20)
        }
        backButton.snp.makeConstraints { make in
            make.top.equalTo(retryErrorLabel.snp.bottom).offset(20)
            make.leading.equalTo(retryOverlay.snp.centerX).offset(8)
            make.trailing.equalToSuperview().offset(-20)
            make.height.equalTo(40)
            make.bottom.equalToSuperview().offset(-20)
        }

        // 启动状态指示灯闪烁动画
        startStatusDotBlinking()
    }

    // MARK: - Status Dot Animation

    private func startStatusDotBlinking() {
        UIView.animate(withDuration: 0.8, delay: 0, options: [.autoreverse, .repeat, .allowUserInteraction]) {
            self.statusDot.alpha = 0.3
        }
    }

    private func stopStatusDotBlinking() {
        statusDot.layer.removeAllAnimations()
        statusDot.alpha = 1.0
    }

    private func updateStatusDot(connected: Bool) {
        DispatchQueue.main.async {
            if connected {
                self.stopStatusDotBlinking()
                self.statusDot.backgroundColor = .systemGreen
            } else {
                self.statusDot.backgroundColor = .systemRed
                self.stopStatusDotBlinking()
            }
        }
    }

    // MARK: - Status Label Updates

    private func updateStatusText(for state: DialogState) {
        let text: String
        switch state {
        case .idle:
            // 未收到 Started 前显示“等待连接”；收到 Started 后即使回到 idle（未开始说话）也展示“已连接”
            text = isConversationStarted ? "已连接" : "等待连接..."
        case .listening:
            text = "正在聆听..."
        case .thinking:
            text = "思考中..."
        case .responding:
            text = "正在回复..."
        }
    
        UIView.transition(with: statusLabel, duration: 0.3, options: .transitionCrossDissolve) {
            self.statusLabel.text = text
        }
    }

    // MARK: - Button State Updates

    // MARK: - Capsule Button

    /// 配置胶囊按钮的事件绑定（仅 tap2talk / push2talk 模式）
    private func configureCapsuleButton() {
        guard dialogMode != .duplex else {
            capsuleButton.isHidden = true
            return
        }
        capsuleButton.isHidden = false
        capsuleButton.addTarget(self, action: #selector(capsuleTouchDown), for: .touchDown)
        capsuleButton.addTarget(self, action: #selector(capsuleTouchUpOutside),
                                for: [.touchUpOutside, .touchCancel])
        capsuleButton.addTarget(self, action: #selector(capsuleTapped), for: .touchUpInside)
        // 初始进入页面：说话模式
        setCapsuleMode(.speak)
    }

    /// 根据对话状态更新胶囊按钮展示
    private func applyCapsuleState(for state: DialogState) {
        guard dialogMode != .duplex else { return }
        DispatchQueue.main.async {
            switch state {
            case .idle, .listening:
                self.setCapsuleMode(.speak)
                self.capsuleButton.isEnabled = true
                self.capsuleButton.alpha = 1.0
            case .responding:
                self.setCapsuleMode(.interrupt)
                self.capsuleButton.isEnabled = true
                self.capsuleButton.alpha = 1.0
            case .thinking:
                // 思考中：保持当前模式但禁用
                self.capsuleButton.isEnabled = false
                self.capsuleButton.alpha = 0.6
            }
        }
    }

    /// 切换胶囊按钮的模式（文案 + 颜色）
    private func setCapsuleMode(_ mode: CapsuleMode) {
        capsuleMode = mode
        let title: String
        let bgColor: UIColor
        switch mode {
        case .speak:
            switch dialogMode {
            case .push2talk:
                title = "按住说话"
            case .tap2talk:
                title = "点击开始说话"
            default:
                title = ""
            }
            bgColor = UIColor(red: 0x4A/255.0, green: 0x9E/255.0, blue: 0xFF/255.0, alpha: 1.0)
        case .interrupt:
            title = "点击打断"
            bgColor = UIColor.systemOrange
        }
        UIView.transition(with: capsuleButton, duration: 0.2, options: .transitionCrossDissolve) {
            self.capsuleButton.setTitle(title, for: .normal)
            self.capsuleButton.backgroundColor = bgColor
        }
    }

    private func animateCapsulePress(pressed: Bool) {
        UIView.animate(withDuration: 0.1) {
            self.capsuleButton.transform = pressed
                ? CGAffineTransform(scaleX: 0.98, y: 0.98)
                : .identity
        }
    }

    // MARK: - Connection

    func connect() {
        MultiModalDialog.enableDebug(enable: true)
        self.conversation?.stop()
        isInDialog = false

        let promptParam = ["user_name": "大米"]

        // 为 license 模式预生成 taskId，保证签名与 start 使用同一个 taskId
        let taskId = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased()

        // 计算 License 签名（仅 license 模式）
        var licenseSignatureMap: [String: Any]? = nil
        var deviceUuid: String = "12345"
        if payMode == PayMode.license.payMode {
            let deviceName = DeviceInfoUtils.shared.getDeviceId() ?? "unknown_device"
            deviceUuid = deviceName
            let licenseParams = LicenseSignatureParams(appId: self.APP_ID, deviceName: deviceName, taskId: taskId)
            let signData = DeviceAuthClient.companion.instance.getLicenseSignature(params: licenseParams)
            TYLogger.shared.debug("license signData: \(signData)")
            if let data = signData.data(using: .utf8),
               let signatureJson = try? JSONSerialization.jsonObject(with: data) {
                licenseSignatureMap = ["signature": signatureJson]
            }
        }

        let params = MultiModalRequestParam { multiBuilder in
            multiBuilder.upStream = MultiModalRequestParam.UpStream(builder: { upstreamBuilder in
                upstreamBuilder.mode = self.dialogMode.rawValue
                upstreamBuilder.type = "AudioAndVideo"
            })
            multiBuilder.clientInfo = MultiModalRequestParam.ClientInfo(builder: { clientInfoBuilder in
                clientInfoBuilder.userId = "test-ios-user"
                clientInfoBuilder.device = MultiModalRequestParam.ClientInfo.Device(uuid: deviceUuid)
                if let map = licenseSignatureMap {
                    clientInfoBuilder.passThroughParams = map
                }
            })
            multiBuilder.downStream = MultiModalRequestParam.DownStream(builder: { downStreamBuilder in
                downStreamBuilder.sampleRate = 16000
                downStreamBuilder.voice = "longanhuan"
            })
            multiBuilder.bizParams = MultiModalRequestParam.BizParams(builder: { bizBuilder in
                bizBuilder.userPromptParams = promptParam
            })
        }

        self.conversation?.start(apiKey: self.API_KEY, params: params, taskId: taskId, completion: { success, error in
            if success {
                print("success")
                self.isconnected = true
                DispatchQueue.main.async {
                    self.retryOverlay.isHidden = true
                }
                if self.useKws {
                    self.audioManager?.startRecording()
                }
            } else {
                self.isconnected = false
                if let e = error {
                    print("连接失败，错误：\(String(describing: e))")
                    DispatchQueue.main.async {
                        self.showRetryOverlay(message: e.message ?? "连接失败")
                    }
                } else {
                    print("连接失败，无错误信息")
                    DispatchQueue.main.async {
                        self.showRetryOverlay(message: "连接失败，无错误信息")
                    }
                }
            }
        })
    }

    // MARK: - SDK Callbacks

    private func setupDialogCallbacks() {
        self.conversation?.onConnected = {
            print("callback: onConnectionReady")
        }

        self.conversation?.onVolumeChanged = { [weak self] volume, type in
            guard let self = self else { return }
            if type == .mic {
                // 将音量映射到 0~1
                let normalized = min(max(volume / 100.0, 0), 1.0)
                DispatchQueue.main.async {
                    self.voiceWaveView.updateAmplitude(normalized)
                }
            }
        }

        self.conversation?.onConversationStarted = { [weak self] in
            guard let self = self else { return }
            TYLogger.shared.debug("callback: onConversationStarted")
            self.isConversationStarted = true
            // 立即刷新顶部状态文案（若当前仍是 idle，则会由“等待连接”切为“已连接”）
            DispatchQueue.main.async {
                self.updateStatusText(for: self.myState ?? .idle)
            }
            // 仅 duplex 全双工模式自动开拾音；
            // tap2talk / push2talk 由用户主动触发 startSpeech 才开录音
            if self.dialogMode == .duplex {
                self.audioManager?.startRecording()
            }
        }

        self.conversation?.onKeywordSpotted = { [weak self] word, type in
            guard let self = self else { return }
            DispatchQueue.main.async {
                TYLogger.shared.debug("onKeywordSpotted::: \(word), \(type)")
            }
        }

        self.conversation?.onConversationEvent = { [weak self] event in
            guard let self = self else { return }
            switch event {
            case .RespondingStarted:
                self.audioManager?.startPlaying()
                self.conversation?.sendHeartBeat()
                // 标记新的 AI 回复轮次，等第一条 responding 消息到来时再创建条目
                self.isNewAIResponse = true
            case .RespondingEnded:
                self.audioManager?.drainPlayer()
            case .InterruptAccepted:
                TYLogger.shared.debug("InterruptAccepted")
                self.audioManager?.cleanPlayerBuffer()
            case .SpeechStarted:
                // 用户开始说话，创建新的用户消息条目
                DispatchQueue.main.async {
                    self.subtitleView.addUserMessage("")
                }
            case .SpeechEnded:
                if self.dialogMode != .duplex {
                    self.audioManager?.stopRecording(notify: false)
                }
            default:
                break
            }
            print("callback: onConversationEvent ", event)
        }

        self.conversation?.onMessageReceived = { [weak self] message, type in
            guard let self = self else { return }
            let payload = message?["payload"] as? [String: Any]
            let output = payload?["output"] as? [String: Any]

            let dialogId = output?["dialog_id"] as? String
            var debugInfo = "dialog_id: \(dialogId ?? "")"

            if type == ResponsetMessageType.speaking {
                let text = output?["text"] as? String
                DispatchQueue.main.async {
                    self.subtitleView.updateLastUserMessage(text ?? "")
                }
            } else {
                let spoken = output?["spoken"] as? String
                let llmRequestId = output?["llm_request_id"] as? String
                let roundId = output?["round_id"] as? String

                DispatchQueue.main.async {
                    if self.isNewAIResponse {
                        self.isNewAIResponse = false
                        self.subtitleView.addAIMessage(spoken ?? "")
                    } else {
                        self.subtitleView.updateLastAIMessage(spoken ?? "")
                    }
                }
                let _ = self.handleCommand(output: output)

                debugInfo += "\nllm_request_id: \(llmRequestId ?? "")"
                debugInfo += "\nround_id: \(roundId ?? "")"
            }

            DispatchQueue.main.async {
                self.debugLabel.text = debugInfo
            }
            TYLogger.shared.debug("onMessageReceived handled over")
        }

        self.conversation?.onConversationStatechanged = { [weak self] state in
            guard let self = self else { return }
            self.myState = state

            DispatchQueue.main.async {
                self.updateStatusText(for: state)
                // 映射 DialogState -> VoiceState
                let voiceState: VoiceState
                switch state {
                case .idle:
                    voiceState = .idle
                case .listening:
                    voiceState = .listening
                case .thinking:
                    voiceState = .thinking
                case .responding:
                    voiceState = .responding
                }
                self.voiceWaveView.setState(voiceState)
            }

            switch state {
            case .idle:
                self.applyCapsuleState(for: state)
            case .listening:
                self.applyCapsuleState(for: state)
                // 仅 duplex 模式下 listening 自动拾音；
                // tap2talk / push2talk 模式下（包括打断后回到 listening）不自动开录音，
                // 等用户主动点击 / 按住胶囊按钮再拾音
                if self.dialogMode == .duplex {
                    self.audioManager?.startRecording()
                }
            case .responding:
                self.applyCapsuleState(for: state)
            case .thinking:
                self.applyCapsuleState(for: state)
            }
            print("callback: onConversationStatechanged ", state)
        }

        self.conversation?.onErrorReceived = { [weak self] err in
            guard let self = self else { return }
            print("callback: onErrorReceived: ", err.key, err.message ?? "")
            let msg = err.key + (err.message ?? "")
            self.showAlert(msg: msg)
        }

        self.conversation?.onConnectionStatusChanged = { [weak self] state in
            guard let self = self else { return }
            print("onConnectionStatusChanged ::::::", state)
            switch state {
            case .failed:
                self.conversation?.stop()
                self.updateStatusDot(connected: false)
            case .inited:
                break
            case .disconnected:
                self.updateStatusDot(connected: false)
            case .connected:
                print("==================connected")
                self.isInDialog = true
                self.updateStatusDot(connected: true)
            case .connecting:
                break
            case .reconnecting:
                break
            @unknown default:
                break
            }
        }

        self.conversation?.onSynthesizedData = { [weak self] data, len in
            guard let self = self else { return }
            TYLogger.shared.info("callback tts \(len)")
            let audio = Data(bytes: UnsafeRawPointer(data), count: Int(len))
            self.audioManager?.writeAudioData(audio)
            TYLogger.shared.info("callback tts \(len) write done")
        }
    }

    // MARK: - AudioHandlerDelegate

    func playerDidStart() {
        self.conversation?.sendLocalRespondingStarted()
    }

    func playerDidStop() {
        self.conversation?.sendLocalRespondingEnded()
    }

    func didReceiveRecordingData(_ buffer: UnsafeMutablePointer<UInt8>?, length: Int32) {
        guard let data = buffer else { return }
        self.conversation?.sendAudioData(data: data, length: length)
    }

    func didReceivePlayingingData(_ buffer: UnsafeMutablePointer<UInt8>?, length: Int32) {
        guard let data = buffer else { return }
        print("send ref data length: \(length)")
        self.conversation?.sendRefData(data: data, length: length)
    }

    // MARK: - Action Handlers

    @objc private func closeTapped() {
        self.conversation?.stop()
        self.audioManager?.stopPlaying()
        self.audioManager?.stopRecording(notify: false)
        dismiss(animated: true)
    }

    // MARK: - Speech Button Handlers

    /// 按下胶囊按钮
    @objc private func capsuleTouchDown() {
        animateCapsulePress(pressed: true)
        // push2talk 模式下的说话态：按下即开始拾音
        if dialogMode == .push2talk, capsuleMode == .speak {
            impactFeedback.impactOccurred()
            TYLogger.shared.info("capsuleTouchDown (push2talk) -> startSpeech")
            startSpeech()
        }
    }

    /// 胶囊按钮按压后滑出 / 取消
    @objc private func capsuleTouchUpOutside() {
        animateCapsulePress(pressed: false)
        if dialogMode == .push2talk, capsuleMode == .speak {
            TYLogger.shared.info("capsuleTouchUpOutside (push2talk) -> stopSpeech")
            stopSpeech()
        }
    }

    /// 胶囊按钮点击（touchUpInside）
    @objc private func capsuleTapped() {
        animateCapsulePress(pressed: false)
        switch capsuleMode {
        case .speak:
            switch dialogMode {
            case .tap2talk:
                impactFeedback.impactOccurred()
                TYLogger.shared.info("capsuleTapped (tap2talk) -> startSpeech")
                startSpeech()
            case .push2talk:
                // push2talk 下 touchDown 已 startSpeech，内动 -> 抬起结束拾音
                TYLogger.shared.info("capsuleTapped (push2talk) -> stopSpeech")
                stopSpeech()
            default:
                break
            }
        case .interrupt:
            guard myState == .responding else { return }
            impactFeedback.impactOccurred()
            TYLogger.shared.info("capsuleTapped -> interrupt")
            self.conversation?.interrupt()
            self.audioManager?.cleanPlayerBuffer()
        }
    }

    @objc private func endTapped() {
        let alert = UIAlertController(title: "结束对话", message: "确定要结束当前对话吗？", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "取消", style: .cancel))
        alert.addAction(UIAlertAction(title: "结束", style: .destructive) { [weak self] _ in
            self?.closeTapped()
        })
        present(alert, animated: true)
    }

    @objc private func interruptTapped() {
        guard myState == .responding else { return }
        impactFeedback.impactOccurred()
        TYLogger.shared.info("interruptTapped")
        self.conversation?.interrupt()
        self.audioManager?.cleanPlayerBuffer()
    }

    @objc private func debugLongPressed(_ gesture: UILongPressGestureRecognizer) {
        guard gesture.state == .began else { return }
        debugOverlay.isHidden.toggle()
    }

    @objc private func openVoicesFolderTapped() {
        let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let voicesURL = documentsURL.appendingPathComponent("voices")

        // 确保目录存在
        if !FileManager.default.fileExists(atPath: voicesURL.path) {
            showAlert(msg: "voices 目录不存在")
            return
        }

        // 获取目录下所有文件
        guard let files = try? FileManager.default.contentsOfDirectory(at: voicesURL, includingPropertiesForKeys: nil),
              !files.isEmpty else {
            showAlert(msg: "voices 目录为空")
            return
        }

        // 用系统分享面板打开，用户可选择"存储到文件"在 Files app 中浏览
        let activityVC = UIActivityViewController(activityItems: files, applicationActivities: nil)
        activityVC.popoverPresentationController?.sourceView = openFolderButton
        activityVC.popoverPresentationController?.sourceRect = openFolderButton.bounds
        present(activityVC, animated: true)
    }

    @objc private func retryTapped() {
        retryOverlay.isHidden = true
        connect()
    }

    // MARK: - Retry Overlay

    private func showRetryOverlay(message: String) {
        retryErrorLabel.text = message
        retryOverlay.isHidden = false
        updateStatusDot(connected: false)
    }

    // MARK: - Alert

    private func showAlert(msg: String) {
        let alert = UIAlertController(title: "error", message: msg, preferredStyle: .alert)
        let okAction = UIAlertAction(title: "确定", style: .default, handler: nil)
        alert.addAction(okAction)
        DispatchQueue.main.async {
            if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
               let sceneDelegate = windowScene.delegate as? SceneDelegate,
               let window = sceneDelegate.window,
               let rootVC = window.rootViewController {
                var topController = rootVC
                while let presentedController = topController.presentedViewController {
                    topController = presentedController
                }
                topController.present(alert, animated: true)
            }
        }
    }

    // MARK: - Helpers

    private func createImageParams() -> [String: Any] {
        let imageObject: [String: Any] = [
            "type": "url",
            "value": self.image_url ?? ""
        ]
        let images = [imageObject]

        let updateParam = MultiModalRequestParam { multiBuilder in
            multiBuilder.images = images
        }
        return updateParam.parameters
    }

    private func updateA2AParams() {
        let userPromptParam: [String: Any] = [
            "age": 8,
            "text": "abc"
        ]
        let a2aParams: [String: Any] = ["tpa_2009506107285893120": userPromptParam]

        let updateParam = MultiModalRequestParam { multiBuilder in
            multiBuilder.bizParams = MultiModalRequestParam.BizParams(builder: { bizBuilder in
                bizBuilder.userDefinedParams = a2aParams
            })
        }
        self.conversation?.updateInfo(params: updateParam.parameters)
    }

    // MARK: - Speech Controls

    private func startSpeech() {
        // 非 duplex 模式：开始说话时才启动录音硬件
        if dialogMode != .duplex {
            audioManager?.startRecording()
        }
        conversation?.startSpeech()
    }

    private func stopSpeech() {
        conversation?.stopSpeech()
        // push2talk：抬起立即关闭录音硬件
        // tap2talk：由 SDK SpeechEnded 回调统一关闭（onConversationEvent .SpeechEnded）
        if dialogMode == .push2talk {
            audioManager?.stopRecording(notify: false)
        }
    }

    // MARK: - Command Handling

    private func handleCommand(output: [String: Any]?) -> [[String: Any]]? {
        guard let output = output,
              let extraInfo = output["extra_info"] as? [String: Any],
              let commandsString = extraInfo["commands"] as? String,
              let commandsData = commandsString.data(using: .utf8) else {
            return nil
        }

        do {
            if let commands = try JSONSerialization.jsonObject(with: commandsData) as? [[String: Any]] {
                for command in commands {
                    if let name = command["name"] as? String, name == "visual_qa" {
                        print("before vqa send img")
                        self.conversation?.requestToRespond(type: "prompt", text: "", params: self.createImageParams())
                        print("vqa send img")
                    }
                }
                return commands
            }
            return nil
        } catch {
            print("解析commands时出错: \(error)")
            return nil
        }
    }
}

// MARK: - Dictionary Extension

extension [String: Any] {
    func toUTF8String() -> String? {
        do {
            let data = try JSONSerialization.data(withJSONObject: self, options: .prettyPrinted)
            return String(data: data, encoding: .utf8)
        } catch {
            return nil
        }
    }
}
