package com.tongyi.multimodal_dialog;

import static com.tongyi.multimodal_dialog.MultiModalDialog.isRtcUseInternalAudio;

import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.core.content.ContextCompat;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import com.aliyun.bailian.billingsignature.DeviceAuthClient;
import com.aliyun.bailian.billingsignature.bean.LicenseSignatureParams;
import com.aliyun.bailian.billingsignature.constants.PayMode;
import com.aliyun.bailian.billingsignature.identity.DeviceIdentityManager;
import com.alibaba.ty.conv.ConvConstants.DialogState;
import com.alibaba.ty.conv.ConvEvent;
import com.tongyi.multimodal_conversation.R;
import com.tongyi.multimodal_dialog.data.MultimodalParams;
import com.tongyi.multimodal_dialog.data.request.MultiModalRequestParam;
import com.tongyi.multimodal_dialog.utils.CameraManager;
import com.tongyi.multimodal_dialog.Constant.TYDebugInfoType;
import com.tongyi.multimodal_dialog.data.IDialogCallback;
import com.tongyi.multimodal_dialog.data.TYError;
import com.tongyi.multimodal_dialog.record.IRecorderCallback;
import com.tongyi.multimodal_dialog.record.TYAudioRecorder;
import com.tongyi.multimodal_dialog.util.DeviceUtil;
import com.tongyi.multimodal_dialog.util.ThreadPoolUtil;
import com.tongyi.multimodal_dialog.utils.NetworkMp3Player;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;

/**
 * 多模态对话Activity
 */
public class MultimodalConversationActivity extends AppCompatActivity {
    private static final String TAG = "MultimodalConversation";
    public static final String KEY_AUTH_PARAM = "KEY_AUTH_PARAM";
    public static final String KEY_PAY_MODE_PARAM = "KEY_PAY_MODE_PARAM";


    // UI组件
    private TextView tvLogs;
    private TextView tvLogsPending;     // 流式预览（partial 阶段写这里，不动 tvLogs，避免抖动）
    private TextView tvState;
    private TextView tvSubtitle;
    private ViewGroup btnInterrupt;
    private ViewGroup btnExit;
    private ViewGroup btnBack;
    private TextView tvInterruptText;
    private FrameLayout videoContainer;
    private ScrollView scrollLogs;
    private ViewGroup conversationDrawer;
    private Handler uiHandler;

    // 脉冲球（取代旧波形圆点）：核心球 + 中层光晕 + 外层粒子
    private View pulseCore;
    private View pulseRingInner;
    private View pulseRingOuter;
    private AnimatorSet pulseAnimatorSet;

    // 音乐播放控件
    private LinearLayout musicPlayerContainer;
    private TextView tvMusicInfo;
    private ViewGroup btnStopMusic;
    private ProgressBar progressMusic;

    // 对话气泡历史记录（HTML 形式累积渲染，仅 final 后追加，永不回退）
    private final StringBuilder bubbleHistory = new StringBuilder();
    // 当前是否有进行中的"我"流式气泡（partial 阶段为 true，final 后回 false）
    private boolean hasPendingUserBubble = false;

    // 核心组件
    private MultiModalDialog multiModalDialog;
    private MultimodalParams authParams;
    private TYAudioRecorder audioRecorder;
    private AudioPlayer audioPlayer;

    private NetworkMp3Player networkMp3Player;

    // 状态变量
    private volatile DialogState currentState = DialogState.DIALOG_IDLE;
    private boolean isVqaMode = false;
    private boolean isVideoMode = false;
    private boolean isExecutingCommand = false;
    private String dialogId;
    private String taskId;

    // 性能监控
    private long initStartTime;
    private long initEndTime;
    private long connectCost = 0;

    private boolean enableKeywordSpotting = false;
    private boolean lastAsrFinished = false; // 添加这个标志位来跟踪上一次ASR是否已完成

    //计费模式
    private String mPayMode;

    /////////////////////////////////////// 启动相关 ///////////////////////////////////////

    public static void launch(AppCompatActivity activity, MultimodalParams authParams, String payMode) {
        Intent intent = new Intent(activity, MultimodalConversationActivity.class);
        intent.putExtra(KEY_AUTH_PARAM, authParams);
        intent.putExtra(KEY_PAY_MODE_PARAM, payMode);
        activity.startActivity(intent);
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initializeActivity();
        initializeUI();
        initializeSDK();
    }

    private void initializeActivity() {
        DeviceUtil.setStatusBarColor(this);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        authParams = (MultimodalParams) getIntent().getSerializableExtra(KEY_AUTH_PARAM);
        mPayMode = getIntent().getStringExtra(KEY_PAY_MODE_PARAM);
        if (authParams == null) {
            Log.e(TAG, "Auth params is null");
            finish();
            return;
        }

        setContentView(R.layout.activity_voice_chat);
        uiHandler = new Handler();
    }

    @SuppressLint("ClickableViewAccessibility")
    private void initializeUI() {
        // 顶部栏
        btnBack = findViewById(R.id.btn_back);
        tvSubtitle = findViewById(R.id.tv_subtitle);

        // 中央脉冲球
        pulseCore = findViewById(R.id.pulse_core);
        pulseRingInner = findViewById(R.id.pulse_ring_inner);
        pulseRingOuter = findViewById(R.id.pulse_ring_outer);
        tvState = findViewById(R.id.tv_state);

        // 对话抽屉
        conversationDrawer = findViewById(R.id.conversation_drawer);
        tvLogs = findViewById(R.id.tv_logs);
        tvLogsPending = findViewById(R.id.tv_logs_pending);
        scrollLogs = findViewById(R.id.scroll_logs);
        // 内容增长时自动锁定到底部（OnLayoutChangeListener 比 ScrollView.post 更准、不会先跳顶再跳底）
        scrollLogs.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, or, ob) -> {
            int childBottom = scrollLogs.getChildAt(0) != null
                    ? scrollLogs.getChildAt(0).getBottom() : 0;
            int delta = childBottom - (scrollLogs.getHeight() + scrollLogs.getScrollY());
            if (delta > 0) scrollLogs.scrollBy(0, delta);
        });
        musicPlayerContainer = findViewById(R.id.music_player_container);
        tvMusicInfo = findViewById(R.id.tv_music_info);
        btnStopMusic = findViewById(R.id.btn_stop_music);
        progressMusic = findViewById(R.id.progress_music);

        // 底部胶囊控制条
        btnInterrupt = findViewById(R.id.btn_interrupt);
        btnExit = findViewById(R.id.btn_exit);
        tvInterruptText = findViewById(R.id.tv_interrupt_text);

        // 视频容器
        videoContainer = findViewById(R.id.video_container);

        // 设置点击事件
        setupClickListeners();

        // 初始状态：进入即显示空闲态外观
        updateUIState("准备中…");
        applyStateAppearance(DialogState.DIALOG_IDLE);
        // 全双工模式默认隐藏麦克风按钮（Push2Talk 模式后续会重新置 VISIBLE）
    }

    private void setupClickListeners() {
        // 打断/说话按钮
        btnInterrupt.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        handleInterruptDown();
                        // 按下时让按钮缩放回弹，提供物理反馈
                        v.animate().scaleX(0.92f).scaleY(0.92f).setDuration(80).start();
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        handleInterruptUp();
                        v.animate().scaleX(1f).scaleY(1f).setDuration(120).start();
                        break;
                }
                return true;
            }
        });

        // 退出按钮
        btnExit.setOnClickListener(v -> {
            showToast("正在退出…");
            finish();
        });

        // 顶部返回按钮 = 退出对话
        if (btnBack != null) {
            btnBack.setOnClickListener(v -> finish());
        }

        // 停止音乐按钮
        btnStopMusic.setOnClickListener(v -> {
            if (networkMp3Player.isPlaying()) {
                networkMp3Player.stop();
                updateMusicPlayerUI(false);
            }
        });

        // 长按对话抽屉清除日志（替代旧的"清除按钮"）
        if (conversationDrawer != null) {
            conversationDrawer.setOnLongClickListener(v -> {
                bubbleHistory.setLength(0);
                tvLogs.setText("等待对话开始…");
                showToast("已清除对话记录");
                return true;
            });
        }
    }

    private void initializeSDK() {
        initStartTime = SystemClock.uptimeMillis();

        // 初始化音频录制器
        audioRecorder = new TYAudioRecorder("", this, audioRecorderCallback, 7);
        networkMp3Player = new NetworkMp3Player();

        // 初始化音频播放器
        audioPlayer = new AudioPlayer(getAudioPlayerCallback(), 48000);

        // 初始化相机管理器
        CameraManager.getInstance().init(this, 1280, 720);

        // 初始化多模态对话
        try {
            initializeMultiModalDialog();
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }

        initEndTime = SystemClock.uptimeMillis();
        Log.d(TAG, "SDK初始化耗时: " + (initEndTime - initStartTime) + "ms");

        // 启动对话
        startConversation();
    }

    private void initializeMultiModalDialog() throws JSONException {
        MultiModalDialog.wsUseInternalAEC = true;
        multiModalDialog = new MultiModalDialog(
                this,
                authParams.getUrl(),
                authParams.getChainMode(),
                authParams.getWorkspaceId(),
                authParams.getAppid(),
                authParams.getDialogMode()
        );
        if (!TextUtils.isEmpty(authParams.getModelId())) {
            multiModalDialog.setModel(authParams.getModelId());
        }

        multiModalDialog.setDialogTimeout(10 * 1000);

        // 启用唤醒，默认唤醒词为"小云小云"
//        if (enableKeywordSpotting){
//            //如果开启唤醒，那么需要先启动录音
//            MultiModalDialog.wsUseInternalVAD = true;
//            multiModalDialog.enableKWS(true, false);
//        }
        multiModalDialog.createConversation(buildRequestParams(), dialogCallback);
    }

    private void startConversation() {
        if (!multiModalDialog.isPush2TalkMode()) {
            ThreadPoolUtil.runOnSubThread(() -> audioRecorder.resume());
        }

        updateUIState("连接中...");
        multiModalDialog.start(Objects.requireNonNull(authParams.getApiKey()), "",taskId);

    }

    /////////////////////////////////////// 对话回调 ///////////////////////////////////////

    private final IDialogCallback dialogCallback = new IDialogCallback() {

        @Override
        public void onStartResult(boolean isSuccess, TYError errorInfo) {
            Log.d(TAG, "连接结果: " + isSuccess + ", 错误: " + errorInfo);

            if (isSuccess) {
                connectCost = SystemClock.uptimeMillis() - initEndTime;
                appendLogMessage("已连接 · " + formatCostMs(connectCost));
                runOnUiThread(() -> btnInterrupt.setVisibility(View.VISIBLE));
            } else {
                appendLogMessage("连接失败");
            }
        }

        @Override
        public void onConvStateChangedCallback(@NonNull DialogState state) {
            currentState = state;
            runOnUiThread(() -> updateStateUI(state));
        }

        @Override
        public void onConvEventCallback(@Nullable ConvEvent event) {
            if (event == null) return;

            dialogId = event.getDialogId();
            taskId = event.getTaskId();

            handleConversationEvent(event);
        }

        @Override
        public void onSynthesizedSpeech(@NonNull byte[] bytes) {
            audioPlayer.setAudioData(bytes);
        }

        @Override
        public void onErrorReceived(@NonNull TYError errorInfo) {
            Log.e(TAG, "收到错误: " + errorInfo);
            appendLogMessage("错误: " + errorInfo.getMessage());
        }

        @Override
        public void onKeyWordSpotted(@NonNull String word, @NonNull Constant.KeyWordsType type) {
            showToast("唤醒: " + word);
        }

        @Override
        public void onSpeechTimeout(long timeout) {
            Log.d(TAG, "语音超时: " + timeout);
        }

        @Override
        public void onInterruptResult(boolean isSuccess, @Nullable TYError errorInfo) {
            Log.d(TAG, "打断结果: " + isSuccess);
            if (isSuccess) {
                audioPlayer.stop(true, false);
            }
        }

        @Override
        public void onReadyToSpeech(@NonNull String dialogId) {
            // 准备说话
            Log.i(TAG, "dialog ready: " + dialogId);
        }

        @Override
        public void onConvSoundLevelCallback(float audioLevel, @NonNull Constant.TYVolumeSourceType audioType) {
            // 音量回调
        }

        @Override
        public void onGotRenderView(@NonNull android.view.SurfaceView renderView) {
            // 视频渲染
        }

        @Override
        public void onDebugInfoTrack(int level, @NonNull TYDebugInfoType debugInfoType, @NonNull String debugInfo) {
            // 调试信息
        }

        @Override
        public void onPlaybackAudioData(@NonNull byte[] bytes) {
            // 播放音频数据
        }
    };

    /////////////////////////////////////// 事件处理 ///////////////////////////////////////

    private void handleConversationEvent(ConvEvent event) {
        switch (event.getEventType()) {
            case EVENT_CONVERSATION_STARTED:
                handleConversationStarted();
                break;
            case EVENT_HUMAN_SPEAKING_DETAIL:
                handleSpeakingDetail(event.getResponse(), true);
                break;
            case EVENT_RESPONDING_DETAIL:
                handleSpeakingDetail(event.getResponse(), false);
                handleResponseCommand(event.getResponse());
                break;
            case EVENT_SENTENCE_BEGIN:
                Log.d(TAG, "开始说话");
                break;
            case EVENT_SENTENCE_END:
                handleSentenceEnd();
                break;
            case EVENT_DATA_OUTPUT_STARTED:
                handleOutputStarted();
                break;
            case EVENT_DATA_OUTPUT_COMPLETED:
                handleOutputCompleted();
                break;
        }
    }

    private void handleConversationStarted() {
        long readyTime = SystemClock.uptimeMillis() - initEndTime - connectCost;
        appendLogMessage("就绪 · " + formatCostMs(readyTime));

        runOnUiThread(() -> updateUIState("请开始说话"));

        if (multiModalDialog.isPush2TalkMode()) {
            runOnUiThread(() -> btnInterrupt.setVisibility(View.VISIBLE));
        }

        // 启动视频模式
        if (authParams.getChainMode() == Constant.ChainMode.RTC) {
            startVideoMode();
        }
    }

    private void handleSpeakingDetail(String response, boolean isHuman) {
        Pair<String, Boolean> detail = parseTextDetail(response);
        if (detail != null) {
            String text = detail.first;
            boolean finished = detail.second;
            String messageFrom = isHuman ? "我" : "AI";

            if (isHuman) {
                // 处理ASR结果的流式显示
                handleAsrStreamingDisplay(text, finished, messageFrom);
            } else {
                // AI回复直接显示
                appendLogMessage(messageFrom + ": " + text);
            }
        }
    }

    /**
     * 处理 ASR 流式显示。
     *
     * @param text         ASR 识别文本
     * @param finished     是否为最终结果
     * @param messageFrom  消息来源（"我" / "你"）
     */
    private void handleAsrStreamingDisplay(String text, boolean finished, String messageFrom) {
        if (text == null) text = "";
        final String safeText = text;
        runOnUiThread(() -> {
            if (!finished) {
                // partial：只更新轻量预览区，不动历史
                String display = messageFrom + ": " + safeText + "…";
                tvLogsPending.setText(Html.fromHtml(buildBubbleHtml(display)));
                if (tvLogsPending.getVisibility() != View.VISIBLE) {
                    tvLogsPending.setVisibility(View.VISIBLE);
                }
                hasPendingUserBubble = true;
            } else {
                // final：把这条消息正式 append 到历史，清空预览区
                bubbleHistory.append(buildBubbleHtml(messageFrom + ": " + safeText));
                tvLogs.setText(Html.fromHtml(bubbleHistory.toString()));
                tvLogsPending.setText("");
                tvLogsPending.setVisibility(View.GONE);
                hasPendingUserBubble = false;
            }
            lastAsrFinished = finished;
        });
    }

    private void handleResponseCommand(String response) {
        String command = parseResponseCommand(response);
        if (command != null) {
            isExecutingCommand = true;
            ThreadPoolUtil.runOnSubThread(() -> executeCommand(command));
        }
    }

    private void handleSentenceEnd() {
        if (!multiModalDialog.isDuplexMode()) {
            Log.d(TAG, "暂停录音");
            if (authParams.getChainMode() != Constant.ChainMode.RTC && !enableKeywordSpotting) {
                audioRecorder.pause();
            }
        }
    }

    private void handleOutputStarted() {
        audioPlayer.pause(true);
        audioPlayer.play();

        if (authParams.getChainMode() == Constant.ChainMode.RTC) {
            //RTC 模式下，tts 合成数据速度为正常比例
            multiModalDialog.sendResponseStarted();
        }
    }

    private void handleOutputCompleted() {
        Log.d(TAG, "输出完成");
        audioPlayer.isFinishSend(true);
        if (authParams.getChainMode() == Constant.ChainMode.RTC) {
            multiModalDialog.sendResponseEnded();
            audioPlayer.isFinishSend(true);
        }
    }

    /////////////////////////////////////// UI更新 ///////////////////////////////////////

    private void updateStateUI(DialogState state) {
        String stateMsg;
        boolean showInterrupt = false;

        switch (state) {
            case DIALOG_IDLE:
                stateMsg = "空闲";
                break;
            case DIALOG_LISTENING:
                if (!isExecutingCommand) {
                    stateMsg = "你说，我正在听";
                    if (authParams.getChainMode() != Constant.ChainMode.RTC) {
                        audioRecorder.resume();
                    }
                } else {
                    stateMsg = "处理中...";
                }
                break;
            case DIALOG_RESPONDING:
                stateMsg = multiModalDialog.isDuplexMode() ?
                        "我正在说，可随时打断" : "我正在说，暂不支持语音打断";
                showInterrupt = !multiModalDialog.isDuplexMode();
                break;
            case DIALOG_THINKING:
                stateMsg = "我正在想";
                break;
            default:
                stateMsg = "未知状态";
                break;
        }

        runOnUiThread(() -> {
            tvState.setText(stateMsg);
            applyStateAppearance(state);
        });

        if (multiModalDialog.isPush2TalkMode()) {
            // 按键说话模式：始终显示麦克风按钮
            btnInterrupt.setVisibility(View.VISIBLE);
            tvInterruptText.setText((state == DialogState.DIALOG_IDLE || state == DialogState.DIALOG_LISTENING) ? "按住说话 \n松开结束" : "点击打断");
        } else if (multiModalDialog.isDuplexMode()) {
            // 双工模式：用户可语音随时打断，无需打断按钮 → GONE 让胶囊自然收缩，避免"✕ + 大片空白"的怪异感
            btnInterrupt.setVisibility(View.GONE);
        } else {
            // 半双工模式：按需显示打断按钮，INVISIBLE 占位避免胶囊宽度跳变
            btnInterrupt.setVisibility(showInterrupt ? View.VISIBLE : View.INVISIBLE);
        }
    }

    /**
     * 根据对话状态切换脉冲球颜色 + 呼吸动画 + 顶部副标题。
     * - IDLE：灰色，静止
     * - LISTENING / THINKING / RESPONDING：对应状态色，呼吸扩散
     */
    private void applyStateAppearance(DialogState state) {
        if (pulseCore == null) return;
        int colorRes;
        String subtitleText;
        boolean animate;
        switch (state) {
            case DIALOG_LISTENING:
                colorRes = R.color.state_listening;
                subtitleText = "● 正在聆听";
                animate = true;
                break;
            case DIALOG_THINKING:
                colorRes = R.color.state_thinking;
                subtitleText = "● 思考中";
                animate = true;
                break;
            case DIALOG_RESPONDING:
                colorRes = R.color.state_responding;
                subtitleText = "● 回答中";
                animate = true;
                break;
            case DIALOG_IDLE:
            default:
                colorRes = R.color.state_idle;
                subtitleText = "● 已连接";
                animate = false;
                break;
        }
        int color = ContextCompat.getColor(this, colorRes);
        tintPulseLayers(color);
        tvState.setTextColor(color);
        // 顶部副标题颜色 + 文案同步
        if (tvSubtitle != null) {
            tvSubtitle.setText(subtitleText);
            tvSubtitle.setTextColor(color);
        }
        // 状态文字淡入
        tvState.setAlpha(0.5f);
        tvState.animate().alpha(1f).setDuration(220)
                .setInterpolator(new AccelerateDecelerateInterpolator()).start();
        if (animate) {
            startPulseAnimation();
        } else {
            stopPulseAnimation();
        }
    }

    /**
     * 给脉冲球三层（核心 + 中层光晕 + 外层粒子）上色，
     * 用 PorterDuff.SRC_IN 让背景 drawable 跟随状态变色。
     */
    private void tintPulseLayers(int color) {
        for (View layer : new View[]{pulseCore, pulseRingInner, pulseRingOuter}) {
            if (layer == null) continue;
            Drawable bg = layer.getBackground();
            if (bg != null) {
                bg.mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN);
            }
        }
    }

    /**
     * 启动脉冲球呼吸动画：
     * - 外层粒子：1.0 → 1.15 → 1.0，alpha 0.2 → 0.6 → 0.2（最大幅度，最慢）
     * - 中层光晕：1.0 → 1.10 → 1.0，alpha 0.4 → 0.8 → 0.4（中等）
     * - 核心球  ：1.0 → 1.05 → 1.0（最小幅度，最快，更"活")
     */
    private void startPulseAnimation() {
        if (pulseCore == null) return;
        stopPulseAnimation();
        AnimatorSet set = new AnimatorSet();
        set.playTogether(
                buildPulseAnimator(pulseRingOuter, 1f, 1.15f, 0.2f, 0.6f, 1800, 0),
                buildPulseAnimator(pulseRingInner, 1f, 1.10f, 0.4f, 0.85f, 1500, 100),
                buildPulseAnimator(pulseCore, 1f, 1.05f, 1f, 1f, 1200, 200)
        );
        pulseAnimatorSet = set;
        set.start();
    }

    private ObjectAnimator buildPulseAnimator(View view, float scaleFrom, float scaleTo,
                                              float alphaFrom, float alphaTo,
                                              long duration, long delay) {
        ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(view,
                android.animation.PropertyValuesHolder.ofFloat("scaleX", scaleFrom, scaleTo, scaleFrom),
                android.animation.PropertyValuesHolder.ofFloat("scaleY", scaleFrom, scaleTo, scaleFrom),
                android.animation.PropertyValuesHolder.ofFloat("alpha", alphaFrom, alphaTo, alphaFrom));
        anim.setDuration(duration);
        anim.setStartDelay(delay);
        anim.setRepeatCount(ValueAnimator.INFINITE);
        anim.setInterpolator(new AccelerateDecelerateInterpolator());
        return anim;
    }

    private void stopPulseAnimation() {
        if (pulseAnimatorSet != null) {
            pulseAnimatorSet.cancel();
            pulseAnimatorSet = null;
        }
        for (View layer : new View[]{pulseCore, pulseRingInner, pulseRingOuter}) {
            if (layer == null) continue;
            layer.setScaleX(1f);
            layer.setScaleY(1f);
        }
        // 静态显示时维持各层的视觉层次
        if (pulseRingOuter != null) pulseRingOuter.setAlpha(0.4f);
        if (pulseRingInner != null) pulseRingInner.setAlpha(0.7f);
        if (pulseCore != null) pulseCore.setAlpha(1f);
    }

    private void updateUIState(String message) {
        runOnUiThread(() -> tvState.setText(message));
    }

    /**
     * 把每条原始日志渲染为消息气泡，规则：
     * - "你: xxx" / 以 "你" 开头 -> 用户气泡（右）
     * - "AI:" / "🤖" / "🔊" / "助手" -> AI 气泡（左）
     * - 其他（系统/状态信息）-> 系统提示（居中浅灰）
     * 整体仍写入同一个 tvLogs，复用现有 append 调用点。
     */
    private void appendLogMessage(String message) {
        if (TextUtils.isEmpty(message)) return;
        runOnUiThread(() -> {
            // 兜底：若 ASR 还在 pending（极少数 SDK 漏 final），把当前预览文本一并固化进历史，避免丢失
            if (hasPendingUserBubble && tvLogsPending.getText().length() > 0) {
                bubbleHistory.append("<font color=\"#1F2937\">")
                        .append(tvLogsPending.getText().toString().replace("…", ""))
                        .append("</font><br/>");
                tvLogsPending.setText("");
                tvLogsPending.setVisibility(View.GONE);
                hasPendingUserBubble = false;
            }
            bubbleHistory.append(buildBubbleHtml(message));
            tvLogs.setText(Html.fromHtml(bubbleHistory.toString()));
        });
    }

    private String buildBubbleHtml(String raw) {
        String trimmed = raw.trim();
        BubbleRole role = detectRole(trimmed);
        String content = stripRolePrefix(trimmed, role);
        String escaped = escapeHtml(content);
        // Android 的 Html.fromHtml 不支持 CSS class / div align，只能用内联颜色 + table 模拟
        switch (role) {
            case USER:
                // 用户：保留前空行让上下文分组清晰
                return "<br/><font color=\"#667EEA\"><b> 我</b></font><br/>"
                        + "<font color=\"#1F2937\">" + escaped + "</font><br/>";
            case AI:
                return "<br/><font color=\"#764BA2\"><b> 助手</b></font><br/>"
                        + "<font color=\"#1F2937\">" + escaped + "</font><br/>";
            case SYSTEM:
            default:
                // 系统：单行显示、无前后空行、字号更小、颜色更淡，避免占据视觉空间
                return "<font color=\"#B0B6BF\"><small>" + escaped + "</small></font><br/>";
        }
    }

    /**
     * 把毫秒压缩为更紧凑的展示：&lt; 1000 显示 "456ms"，&gt;= 1000 显示 "1.2s"。
     */
    private String formatCostMs(long ms) {
        if (ms < 1000) return ms + "ms";
        return String.format(java.util.Locale.US, "%.1fs", ms / 1000f);
    }

    private enum BubbleRole { USER, AI, SYSTEM }

    private BubbleRole detectRole(String text) {
        if (text.startsWith("你:") || text.startsWith("你：") || text.startsWith("用户")
                || text.startsWith("我:") || text.startsWith("我：")
                || text.startsWith("🙋") || text.startsWith("🎤") || text.startsWith("user")) {
            return BubbleRole.USER;
        }
        if (text.startsWith("AI") || text.startsWith("ai") || text.startsWith("助手")
                || text.startsWith("🤖") || text.startsWith("🔊") || text.startsWith("Assistant")) {
            return BubbleRole.AI;
        }
        return BubbleRole.SYSTEM;
    }

    private String stripRolePrefix(String text, BubbleRole role) {
        if (role == BubbleRole.SYSTEM) return text;
        // 去掉常见前缀（"你:", "AI:", "🤖" 等），让气泡正文更干净
        String[] prefixes = {"你:", "你：", "我:", "我：", "用户:", "用户：",
                "AI:", "AI：", "ai:", "ai：", "助手:", "助手：",
                "🙋", "🎤", "🤖", "🔊", "user:", "Assistant:"};
        for (String p : prefixes) {
            if (text.startsWith(p)) {
                return text.substring(p.length()).trim();
            }
        }
        return text;
    }

    private String escapeHtml(String s) {
        if (s == null) return "";
        return s.replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\n", "<br/>");
    }

    /**
     * 已废弃：滚动锁底改由 scrollLogs 的 OnLayoutChangeListener 触发，
     * 比 post 时机更准，不会先跳顶再跳底。
     */
    @Deprecated
    private void scrollToBottom() {
        // no-op
    }

    /////////////////////////////////////// 交互处理 ///////////////////////////////////////

    private void handleInterruptDown() {
        if (!multiModalDialog.isDuplexMode()) {
            if (currentState != DialogState.DIALOG_IDLE && currentState != DialogState.DIALOG_LISTENING) {
                Log.d(TAG, "执行打断");
                audioPlayer.stop(true, false);
                multiModalDialog.interrupt();
            }else if (multiModalDialog.isPush2TalkMode()) {
                //push2talk 模式下，按住说话
                multiModalDialog.startSpeech();
            }
        }
    }

    private void handleInterruptUp() {
        if (multiModalDialog.isPush2TalkMode()) {
            //push2talk 模式下，松开结束
            multiModalDialog.stopSpeech();
        }
    }

    /////////////////////////////////////// 命令执行 ///////////////////////////////////////

    private void executeCommand(String command) {
        Log.d(TAG, "执行命令: " + command);

        try {
            JSONObject commandObj =  new JSONArray(command).getJSONObject(0);
            if (commandObj.has("name")) {
                // multimodal app response
                String cmdName = commandObj.getString("name");
                switch (cmdName) {
                    case "visual_qa":
                        executeVQACommand();
                        break;
                    case "quit_videochat":
                        stopVideoMode();
                        break;
                    case "play_music":
                        handleMusicRadioCommand(commandObj);
                        break;
                    default:
                        executeDefaultCommand();
                        break;
                }
            }else if (commandObj.has("id")) {
                // voice app response
                String cmdId = commandObj.getString("id");
                switch (cmdId) {
                    case "music_radio":
                        handleMusicRadioCommand(commandObj);
                        break;
                    default:
                        break;
                }
            }



        } catch (Exception e) {
            Log.e(TAG, "命令执行失败", e);
            isExecutingCommand = false;
        }
    }

    private void handleMusicRadioCommand(JSONObject commandObj) throws JSONException {
        if (commandObj.has("function")) {
            //play music in voice app
            JSONObject function = commandObj.getJSONObject("function");
            if (function.has("arguments")){
                String arguments = function.getString("arguments");
                Log.d(TAG, "arguments: " + arguments);
                JSONArray args = new JSONArray(arguments);
                for (int i = 0; i < args.length(); i++) {
                    if (args.get(i) instanceof JSONObject) {
                        JSONObject arg = (JSONObject) args.get(i);
                        if (arg.has("music_info")) {
                            String musicInfo = arg.getString("music_info");
                            playMp3Url(musicInfo);
                        }
                    }
                }
            }
        }else if (commandObj.has("params")) {
            //play music in multimodal app
            JSONArray params = commandObj.getJSONArray("params");
            Log.d(TAG, "params: " + params.toString());
            JSONObject param  = (JSONObject) params.get(0);
            if (param.has("normValue")) {
                String musicInfo = param.getString("normValue");
                playMp3Url(musicInfo);
            }

        }
    }

    private void playMp3Url(String jsonString) throws JSONException {
        JSONObject info  = new JSONObject(jsonString);
        if (info.has("audios")) {
            String audioMp3 = info.getString("audios");
            // play mp3 by url，like  "https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/FreePD_mp3s/Miscellaneaous_Chill_mp3/Tahistaeva%20Aeg.mp3"
            final String musicUrl = audioMp3;
            final String musicName = info.getString("songName");
            networkMp3Player.play(musicUrl, new NetworkMp3Player.OnPlayCallback(){
                @Override
                public void onPlayStart() {
                    Log.d(TAG, "mp3开始播放");
                    // 更新音乐播放UI
                    runOnUiThread(() -> {
                        musicPlayerContainer.setVisibility(View.VISIBLE);
                        tvMusicInfo.setText("正在播放: "+ musicName);
                        progressMusic.setVisibility(View.VISIBLE);
                    });
                }

                @Override
                public void onPlayComplete() {
                    Log.d(TAG, "mp3播放完成");
                    // 隐藏音乐播放UI
                    runOnUiThread(() -> {
                        musicPlayerContainer.setVisibility(View.GONE);
                        progressMusic.setVisibility(View.GONE);
                    });
                }

                @Override
                public void onPlayError(String error) {
                    isExecutingCommand = false;
                    // 隐藏音乐播放UI
                    runOnUiThread(() -> {
                        musicPlayerContainer.setVisibility(View.GONE);
                        progressMusic.setVisibility(View.GONE);
                        showToast("音乐播放出错: " + error);
                    });
                }
            });
        }
    }

    private void executeVQACommand() {
        showToast("开始拍照");
        isVqaMode = true;

        // 目前移动端拍照需要上传图片到oss，或其他第三方文件服务，生成图片url地址调用图文理解Agent
        // 以下TakePictureAndUploadOSS是基于阿里云OSS的实现，您可以替换为自有或三方服务
        // TakePictureAndUploadOSS 基于阿里云OSS SDK实现，需要开通OSS服务，并在这个类中修改OSS接入
        // 需要的AK，SK，Endpoint，BucketName等信息
        // TakePictureAndUploadOSS takePictureAndUploadOSS = new TakePictureAndUploadOSS(mContext, url -> {
        //      //下方发送请求
        // });
        // takePictureAndUploadOSS.takePicture();

        //除了上传 URL 之外，VQA 也支持通过 base64编码格式直接传图片。这种方式支持尺寸小于 180KB 的图片.type为“base64”,value为图片base64编码
        runOnUiThread(() ->
        {
            //增加请求延时。通过实际拍照实现可以忽略
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            MultiModalRequestParam updateParams = MultiModalRequestParam
                    .builder()
                    .build();
            JSONObject imageObject = new JSONObject();
            try{
                imageObject.put("type", "url");
                imageObject.put("value", authParams.getVqaImageLink());
                List<JSONObject> images = new ArrayList<>();
                images.add(imageObject);

                updateParams.setImages(images);
            }catch (JSONException e){
                e.printStackTrace();
            }
            multiModalDialog.requestToRespond("prompt", "",updateParams.getParametersAsJson());
            isExecutingCommand = false;
        });
    }

    private void startVideoMode() {
        if (isVideoMode) return;

        try {
            MultiModalRequestParam updateParams = MultiModalRequestParam.builder().build();
            JSONObject videoObj = new JSONObject();
            videoObj.put("action", "connect");
            videoObj.put("type", "voicechat_video_channel");

            List<JSONObject> videos = new ArrayList<>();
            videos.add(videoObj);

            updateParams.setBizParams(MultiModalRequestParam.BizParams.builder()
                    .videos(videos).build());

            multiModalDialog.requestToRespond("prompt", "", updateParams.getParametersAsJson());
            multiModalDialog.setVideoContainer(videoContainer, uiHandler);

            isVideoMode = true;
            runOnUiThread(() -> videoContainer.setVisibility(View.VISIBLE));

        } catch (JSONException e) {
            Log.e(TAG, "启动视频模式失败", e);
        }
    }

    private void stopVideoMode() {
        isVideoMode = false;
        runOnUiThread(() -> videoContainer.setVisibility(View.GONE));

        multiModalDialog.stop();
        isExecutingCommand = false;
    }

    private void executeDefaultCommand() {
        multiModalDialog.requestToRespond("transcript", "执行成功", null);
        isExecutingCommand = false;
    }

    /////////////////////////////////////// 工具方法 ///////////////////////////////////////

    private Pair<String, Boolean> parseTextDetail(String response) {
        try {
            JSONObject payload = new JSONObject(response).getJSONObject("payload");
            if (payload.has("output")) {
                JSONObject output = payload.getJSONObject("output");
                String text = output.getString("text");
                boolean finished = output.getBoolean("finished");
                return new Pair<>(text, finished);
            }
        } catch (Exception e) {
            Log.e(TAG, "解析文本详情失败", e);
        }
        return null;
    }

    private String parseResponseCommand(String response) {
        try {
            JSONObject payload = new JSONObject(response).getJSONObject("payload");
            if (payload.has("output")) {
                JSONObject output = payload.getJSONObject("output");
                if (output.has("extra_info") && !output.isNull("extra_info")) {
                    JSONObject extraInfo = new JSONObject(output.getString("extra_info"));
                    if (extraInfo.has("commands")) {
                        String commands = extraInfo.getString("commands");
                        if (commands.length() > 6) { // 过滤空命令
                            return commands;
                        }
                    }else if (extraInfo.has("tool_calls")) {
                        String tool_calls = extraInfo.getString("tool_calls");
                        if (tool_calls.length() > 6) { // 过滤空命令
                            return tool_calls;
                        }
                    }
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "解析命令失败", e);
        }
        return null;
    }

    //通过指令直连 Agent，后续所有的识别结果都直接走此 Agent 处理。通常为 A2A Agent
    private JSONArray directConnectToAgent(String agentId){
        JSONArray commands = new JSONArray();
        try {
            JSONObject command = new JSONObject();
            command.put("name", "agent_command");

            JSONObject execParams = new JSONObject();
            execParams.put("app_id", agentId);
            execParams.put("intent", "open"+ agentId);
            command.put("exec_params", execParams);
            commands.put(command);

        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
        return commands;
    }

    // 更新 Agent 参数。在必要的时候进行更新操作。服务端会将这些参数在下次文本请求 agent 时传入
    private void updateAgentParams(){
        try {
            JSONObject userAgentPromptParams = new JSONObject();
            userAgentPromptParams.put("age", 8);
            userAgentPromptParams.put("text","今天是 2025年 1 月 9 日");
            JSONObject agentIdParams = new JSONObject();
            agentIdParams.put("user_prompt_params", userAgentPromptParams);

            JSONObject userDefinedParams = new JSONObject();
            userDefinedParams.put("tpa_2009506107285893120", agentIdParams); //key为app_id


            MultiModalRequestParam updateParams = MultiModalRequestParam
                    .builder()
                    .bizParams(MultiModalRequestParam.BizParams.builder()
                            .userDefinedParams(userDefinedParams)
                            .build())
                    .build();

            Log.i(TAG, "Update Agent bizParams: " + updateParams.getParametersAsJson());
            multiModalDialog.updateInfo(updateParams.getParametersAsJson());
        } catch (JSONException e) {
            throw new RuntimeException(e);
        }
    }

    /** userId 用于对话历史关联，正式场景请替换为真实用户唯一标识 */
    private static final String DEFAULT_USER_ID = "123";
    /** 上行音视频流类型 */
    private static final String UPSTREAM_TYPE_AUDIO_AND_VIDEO = "AudioAndVideo";
    /** 下行 TTS 采样率 */
    private static final int DOWNSTREAM_SAMPLE_RATE = 48000;
    /** TTS 音色 */
    private static final String VOICE_NAME = "longanhuan";

    private MultiModalRequestParam buildRequestParams() {
        //DeviceUUIDUtil.getAndroidId 已被弃用，已有版本建议逐步建议到新版本
        //String deviceName = DeviceUUIDUtil.getAndroidId(this);
        String deviceName = DeviceIdentityManager.getInstance().getDeviceIdentity(this).getId();
        
        taskId = UUID.randomUUID().toString().replace("-", "");

        boolean isLicenseMode = Objects.equals(mPayMode, PayMode.LICENSE.getPayMode());

        // UpStream：ASR 文本替换规则（"1加1" → "一加一"）
        MultiModalRequestParam.UpStream upStream = MultiModalRequestParam.UpStream.builder()
                .type(UPSTREAM_TYPE_AUDIO_AND_VIDEO)
                .asrPostProcessing(Collections.singletonList(buildAsrReplaceRule()))
                .build();

        // DownStream：不同计费模式使用不同的 TTS 音色
        MultiModalRequestParam.DownStream downStream = MultiModalRequestParam.DownStream.builder()
                .voice(VOICE_NAME)
                .sampleRate(DOWNSTREAM_SAMPLE_RATE)
                .build();

        // ClientInfo：License 模式额外携带 signature 透传参数
        MultiModalRequestParam.ClientInfo clientInfo = isLicenseMode
                ? MultiModalRequestParam.ClientInfo.builder()
                        .device(buildDevice(deviceName))
                        .userId(DEFAULT_USER_ID)
                        .passThroughParams(buildLicenseSignaturePayload(deviceName))
                        .build()
                : MultiModalRequestParam.ClientInfo.builder()
                        .device(buildDevice(deviceName))
                        .userId(DEFAULT_USER_ID)
                        .build();

        // 如需通过指令直连 Agent，可追加：
        // .bizParams(MultiModalRequestParam.BizParams.builder()
        //         .userCustomParams(Collections.singletonMap("commands", directConnectToAgent("demo_a2a_agent_id")))
        //         .build())
        return MultiModalRequestParam.builder()
                .clientInfo(clientInfo)
                .upStream(upStream)
                .downStream(downStream)
                // .bizParams()
                .build();
    }

    /**
     * 构建 Device 信息（仅包含 UUID，正式场景请配置真实设备 UUID）。
     */
    private MultiModalRequestParam.ClientInfo.Device buildDevice(String deviceName) {
        return MultiModalRequestParam.ClientInfo.Device.builder()
                .uuid(deviceName)
                .build();
    }

    /**
     * 构建 License 计费签名透传参数。
     * 仅在 License 计费模式下使用，签名由 DeviceAuthClient 生成后通过 passThroughParams 上送服务端。
     */
    private Map<String, Object> buildLicenseSignaturePayload(String deviceName) {
        LicenseSignatureParams params = new LicenseSignatureParams(authParams.getAppid(), deviceName, taskId);
        JSONObject licenseInfoSignature = DeviceAuthClient.getInstance().getLicenseSignature(params);
        Map<String, Object> payload = new HashMap<>();
        payload.put("signature", licenseInfoSignature);
        return payload;
    }

    /**
     * 构建 ASR 后处理替换规则：将识别结果中的 "1加1" 部分匹配替换为 "一加一"。
     */
    private MultiModalRequestParam.UpStream.ReplaceWord buildAsrReplaceRule() {
        MultiModalRequestParam.UpStream.ReplaceWord replaceWord = new MultiModalRequestParam.UpStream.ReplaceWord();
        replaceWord.setTarget("一加一");
        replaceWord.setSource("1加1");
        replaceWord.setMatchMode("partial");
        return replaceWord;
    }

    /////////////////////////////////////// 音频相关 ///////////////////////////////////////

    private final IRecorderCallback audioRecorderCallback = new IRecorderCallback() {
        @Override
        public void onRecorderStart() {
            Log.d(TAG, "录音开始");
        }

        @Override
        public void onRecorderStop() {
            Log.d(TAG, "录音停止");
        }

        @Override
        public int onRecorderData(byte[] data, int len, boolean firstPack) {
            if (multiModalDialog != null && authParams.getChainMode() != Constant.ChainMode.RTC  ) {
                multiModalDialog.sendAudioData(data);
            }else if (authParams.getChainMode() == Constant.ChainMode.RTC && !isRtcUseInternalAudio()) {
                multiModalDialog.sendAudioData(data);
            }
            return 0;
        }

        @Override
        public void onRecordStateChanged(TYAudioRecorder.RecordState state) {
            Log.d(TAG, "录音状态变化: " + state);
        }
    };

    private AudioPlayerCallback getAudioPlayerCallback() {
        return new AudioPlayerCallback() {
            @Override
            public void playStart() {
                Log.d(TAG, "播放开始");
                multiModalDialog.sendResponseStarted();
            }

            @Override
            public void playOver(boolean interrupt, int delay_ms) {
                Log.d(TAG, "播放结束");
                if (multiModalDialog != null) {
                    multiModalDialog.sendResponseEnded();
                }
            }

            @Override
            public void playSoundLevel(int level) {
                // 音量级别
            }

            @Override
            public void showLog(String text, String tag) {
                // 显示日志
            }

            @Override
            public int onPlayerData(byte[] data, int len) {
                if (multiModalDialog != null && multiModalDialog.isDuplexMode()) {
                    multiModalDialog.sendRefData(data);
                }
                return 0;
            }

            @Override
            public void playerStateChanged(int action) {
                // 播放状态变化
            }
        };
    }

    /////////////////////////////////////// 生命周期 ///////////////////////////////////////

    @Override
    protected void onStop() {
        super.onStop();
        if (isFinishing()) {
            ThreadPoolUtil.runOnSubThread(() -> {
                audioRecorder.pause();
            });
            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "Activity销毁");

        if (multiModalDialog != null) {
            multiModalDialog.destroy();
        }
        if (audioPlayer != null) {
            audioPlayer.stop(true, false);
            audioPlayer.releaseAudioTrack(true);
        }

        if (audioRecorder != null) {
            audioRecorder.pause();
        }

        if (networkMp3Player != null) {
            networkMp3Player.release();
        }

        CameraManager.getInstance().destroy();

        if (uiHandler != null) {
            uiHandler.removeCallbacksAndMessages(null);
        }
    }

    /**
     * DEMO 流程未调用。演示在 websocket 链路中实现 liveAI
     * 启动视频帧流，每 500ms 发送一次图片帧
     */
    private void startVideoFrameStreaming() {
        Thread videoStreamingThread = new Thread(() -> {
            try {
                while ( !Thread.currentThread().isInterrupted()) {
                    Thread.sleep(500);

                    JSONObject extraObject = new JSONObject();
                    extraObject.put("images", getMockOSSImage());

                    multiModalDialog.updateInfo(extraObject);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        });

        videoStreamingThread.setDaemon(true);
        videoStreamingThread.setName("LiveAI-VideoStreaming");
        videoStreamingThread.start();

    }

    /**
     * build images list request
     * */
    private static JSONArray getMockOSSImage() {
        JSONObject imageObject = new JSONObject();
        JSONArray images = new JSONArray();
        try{

            imageObject.put("type", "base64");
            imageObject.put("value", getLocalImageBase64());

            images.put(imageObject);

        }catch (Exception e){
            e.printStackTrace();
        }
        return images;
    }

    //read local image to base64
    private static String getLocalImageBase64(){
        return "";
    }

    /////////////////////////////////////// 工具方法 ///////////////////////////////////////

    private void showToast(String message) {
        runOnUiThread(() -> Toast.makeText(this, message, Toast.LENGTH_SHORT).show());
    }

    private void updateMusicPlayerUI(boolean isPlaying) {
        runOnUiThread(() -> {
            if (isPlaying) {
                musicPlayerContainer.setVisibility(View.VISIBLE);
                progressMusic.setVisibility(View.VISIBLE);
            } else {
                musicPlayerContainer.setVisibility(View.GONE);
                progressMusic.setVisibility(View.GONE);
            }
        });
    }
}