/* eslint-disable prefer-const */
/* eslint-disable @typescript-eslint/no-unused-vars */

/**
 * @typedef {Object} TrroClientConfig
 * @property {string} cloudMode - can be `'public'`, `'public_intl'` or `'private'`
 * @property {string[]} serverIp
 * @property {number} port
 * @property {boolean} https
 * @property {string[]} mqttURL
 * @property {number} mqttPort
 * @property {boolean} mqttForceLogin
 * @property {string} sdkMode - can be `'server'` or `'server2'`
 * @property {string|number} projectId
 * @property {string} remoteDeviceId
 * @property {string} password
 * @property {boolean} showLog
 * @property {boolean} reportLog
 */

/**
 * Sanitize a device ID for use in DOM element IDs.
 * Replaces characters that are problematic in CSS selectors (e.g. `/`) with `-`.
 * @param {string} id - Raw device ID
 * @returns {string} Sanitized ID safe for DOM element IDs and CSS selectors
 */
const sanitizeIdForDom = (id) => id.replace(/[^a-zA-Z0-9_-]/g, '_');

class TrroClient {
  client = null;
  /** @type {TrroClientConfig} */
  config = {};

  gatewayList = [];
  /** @type {string[]} */
  connectedDeviceIds = [];
  /** @type {{ [deviceId: string]: 'MASTER'|'GUEST'; }} */
  permissions = {};
  /** @type {{ [streamId: string]: { mediaType: 'VIDEO'|'AUDIO'; streamId: number; mount: string }[]; }} */
  subscribedStreamIds = {};

  /** @type {boolean} */
  diagnosisReportRequesting = false;
  /** @type {any} */
  diagnosisReport;

  /**
   * @param {TrroClientConfig} config
   * @param {object} [callbacks]
   * @param {(id: string) => void} [callbacks.onConnected]
   * @param {(id: string) => void} [callbacks.onDisconnected]
   * @param {(code: number, reason: string) => void} [callbacks.onKicked]
   * @param {(id: string, permission: 'MASTER'|'GUEST') => void} [callbacks.onFieldDevicePermissionChange]
   * @param {(code: number, message: string) => void} [callbacks.onError]
   */
  constructor(config, callbacks) {
    this.config = config;
    const {
      cloudMode,
      serverIp,
      port,
      https,
      mqttURL,
      mqttPort,
      mqttForceLogin,
      sdkMode,
      projectId,
      remoteDeviceId,
      password,
      showLog,
      reportLog,
    } = config;
    this.client = new TRROSDK({
      cloudMode,
      ...(cloudMode === 'private' && serverIp?.length ? { serverIp } : {}),
      ...(cloudMode === 'private' && port !== undefined ? { port } : {}),
      ...(cloudMode === 'private' ? { https } : {}),
      mqttOptions: {
        ...(mqttURL?.length ? { url: mqttURL } : {}),
        ...(mqttPort && Number.isInteger(mqttPort) ? { port: mqttPort } : {}),
        ...(mqttForceLogin !== undefined && mqttForceLogin !== null ? { forceLogin: mqttForceLogin } : {}),
      },
      ...(!!sdkMode ? { sdkMode } : {}),
      projectId,
      remoteDeviceId,
      password,
      observer: {
        onConnected: ({ deviceId }) => {
          console.info(`[TRROSDK.onConnected] ${deviceId}`);
          callbacks?.onConnected?.(deviceId);
        },
        onDisconnected: ({ deviceId }) => {
          console.info(`[TRROSDK.onDisconnected] ${deviceId}`);
          callbacks?.onDisconnected?.(deviceId);
        },
        onKick: ({ code, reason }) => {
          console.info(`[TRROSDK.onKick] code: ${code}, reason: ${reason}`);
          callbacks?.onKicked?.(code, reason);
        },
        onMqttConnectionState: ({ state, message }) => {
          console.info(`[TRROSDK.onMqttConnectionState] state: ${state}, message: ${message ?? '-'}`);
        },
        onSignalingState: ({ roomId, state, data }) => {
          console.info(`[TRROSDK.onSignalingState] roomId: ${roomId}, state: ${state}, data: ${data}`);
        },
        onPeerConnectionState: ({ deviceId, streamId, mediaType, code, state, message, data }) => {
          console.info(
            `[TRROSDK.onPeerConnectionState] deviceId: ${deviceId}, streamId: ${streamId}, mediaType: ${mediaType}, code: ${code}, state: ${state}, message: ${message}`,
          );
          // Auto-apply buffer configuration after connection established
          if (code === 0 && state === 'connected' && mediaType === 'video') {
            autoApplyBufferConfig(deviceId, streamId);
          }
        },
        onTrackPublishState: ({ state, deviceId, streamId, mediaType }) => {
          console.info(
            `[TRROSDK.onTrackPublishState] state: ${state}, deviceId: ${deviceId}, streamId: ${streamId}, mediaType: ${mediaType}`,
          );
        },
        onControlData: ({ deviceId, data, ordered, channelType }) => {
          console.info(
            `[TRROSDK.onControlData] deviceId: ${deviceId}, data: ${data}, ordered: ${ordered}, channelType: ${channelType}`,
          );

          let targetDeviceId = null;
          // Try to find the device in the connected device list first
          const deviceIndex = this.connectedDeviceIds.findIndex(
            (id) => deviceId === `${this.config.projectId}/${id}` || deviceId === id,
          );

          if (deviceIndex !== -1) {
            targetDeviceId = this.connectedDeviceIds[deviceIndex];
          } else {
            // If the device is not connected through connect(),
            // for example, only using MQTT to send and receive messages,
            // directly use the deviceId in the callback to display the message
            targetDeviceId = deviceId;
          }

          if (targetDeviceId) {
            if (typeof data === 'string') {
              updateFieldDeviceMessage(targetDeviceId, 'string', data, channelType);
            } else if (data instanceof ArrayBuffer) {
              updateFieldDeviceMessage(targetDeviceId, 'ArrayBuffer', data, channelType);
            }
          }
        },
        onPermissionStateChange: ({ deviceId, userId, permission }) => {
          console.info(
            `[TRROSDK.onPermissionStateChange] deviceId: ${deviceId}, userId: ${userId}, permission: ${permission}`,
          );
          const isConnectedId = this.connectedDeviceIds.find(
            (id) => deviceId === `${this.config.projectId}/${id}` || deviceId === id,
          );
          if (!!isConnectedId) {
            callbacks?.onFieldDevicePermissionChange?.(isConnectedId, permission === 'master' ? 'MASTER' : 'GUEST');
          }
        },
        onEvent: ({ type, data }) => {
          if (type === 'autoplay') {
            if (data.code === 0 && data.mediaType === 'audio') {
              const audioContainer = document.getElementById(`field-audio-${sanitizeIdForDom(data.fieldDeviceId)}`);
              const audioElem = audioContainer?.querySelector?.('audio');

              if (audioElem) {
                audioElem.controls = true;
              }
            }
          }
          if (type === 'localTrackUnpublished') {
            isLocalAudioTrackPublished = false;
          }
          if (type === 'webrtcStats') {
            if (Array.isArray(data)) {
              data.forEach((s) => {
                if (s?.mediaType !== 'video') return;
                const fid = s.srcUserId;
                const tid = s.trackId;
                if (!fid || !tid) return;
                if (!streamStatsMap[fid]) streamStatsMap[fid] = {};
                streamStatsMap[fid][tid] = {
                  streamId: s.streamId,
                  jitterBuffer: s.jitterBuffer,
                  jitterBufferTarget: s.jitterBufferTarget,
                  rtt: s.rtt,
                  lost: s.lost,
                  fps: s.fps,
                };
              });
            }
            if (!streamStatsRenderTimer) {
              streamStatsRenderTimer = setTimeout(() => {
                streamStatsRenderTimer = null;
                renderAllStreamStatsCards();
              }, 1000);
            }
          }
          if (type === 'gatewayStats') {
            if (data?.fieldDeviceId && Array.isArray(data?.stats)) {
              if (!gatewayStatsMap[data.fieldDeviceId]) gatewayStatsMap[data.fieldDeviceId] = {};
              data.stats.forEach((s) => {
                if (s?.stream === undefined && s?.stream === null) return;
                gatewayStatsMap[data.fieldDeviceId][s.stream] = {
                  rtt: s.rtt,
                  lost: s.lost,
                  fps: s.fps,
                  bps: s.bps,
                  jitter: s.jitter,
                };
              });
            }
          }
          if (type === 'latencyReport') {
            // latencyReport 为帧级触发，回调频率与视频帧率一致（例如 60fps 时每秒 60 次）
            // 数据结构：Array<{ fieldDeviceId, streamId, trackId, latency, vcct, rcct }>
            // 默认不打印以避免日志过多；接入方可按需取消下一行注释开启调试打印
            // data.forEach((item) => { console.log('[TRROSDK.onEvent.latencyReport]', item); });
          }
        },
        onError: ({ code, message }) => {
          console.info(`[TRROSDK.onError] code: ${code}, message: ${message}`);

          // Map error code to localized message
          let errorMessage = '';
          if (code === 20101) {
            errorMessage = t('error_code_20101');
          } else if (code === 20102) {
            errorMessage = t('error_code_20102');
          } else if (code === 20199) {
            errorMessage = t('error_code_20199');
          } else {
            errorMessage = t('error_code_unknown', { code });
          }

          // Show error notification
          notify({
            title: t('sdk_error_title'),
            description: `${errorMessage} (${code})`,
          });
        },
      },
      loggerOption: {
        showLog,
        reportLog,
      },
    });
  }

  async init() {
    try {
      const { code, message } = await this.client.init();
      if (code !== 0) {
        console.error(`[TRROSDK.init] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      console.log(`[TRROSDK.init] ${message} (${code})`);
      return { flag: true, message: `${message} (${code})` };
    } catch (error) {
      console.error(`[TrroClient.init] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  async destroy() {
    try {
      const { code, message } = await this.client.destroy();
      this.client = null;
      if (code !== 0) {
        console.error(`[TRROSDK.destroy] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      console.log(`[TRROSDK.destroy] ${message} (${code})`);
      return { flag: true, message: `${message} (${code})` };
    } catch (error) {
      console.error(`[TrroClient.destroy] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  /**
   * @param {string} fieldDeviceId
   */
  async connect(fieldDeviceId) {
    if (this.connectedDeviceIds.includes(fieldDeviceId)) {
      console.log(`[TrroClient.connect] ${fieldDeviceId} is already connected`);
      return { flag: true, message: `${fieldDeviceId} is already connected` };
    }
    try {
      const { code, message } = await this.client.connect(fieldDeviceId);
      if (code !== 0) {
        console.error(`[TRROSDK.connect] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      console.log(`[TrroClient.connect] Successfully connected to ${fieldDeviceId}`);
      this.connectedDeviceIds.push(fieldDeviceId);
      return { flag: true, message: `Successfully connected to ${fieldDeviceId}` };
    } catch (error) {
      console.error(`[TrroClient.connect] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  /**
   * @param {string} fieldDeviceId
   */
  async disconnect(fieldDeviceId) {
    if (!this.connectedDeviceIds.includes(fieldDeviceId)) {
      console.error(`[TrroClient.disconnect] ${fieldDeviceId} is not connected`);
      return { flag: false, message: `${fieldDeviceId} is not connected` };
    }
    try {
      const { code, message } = await this.client.disconnect(fieldDeviceId);
      if (code !== 0) {
        console.error(`[TRROSDK.disconnect] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      console.log(`[TrroClient.disconnect] Successfully disconnected from ${fieldDeviceId}`);
      this.connectedDeviceIds.splice(this.connectedDeviceIds.indexOf(fieldDeviceId), 1);
      return { flag: true, message: `Successfully disconnected from ${fieldDeviceId}` };
    } catch (error) {
      console.error(`[TrroClient.disconnect] ${error.name}: ${error.message}`);
      return { flag: true, message: `${error.name}: ${error.message}` };
    }
  }

  /**
   * @param {string} fieldDeviceId
   * @param {{ mediaType: 'VIDEO'|'AUDIO'; streamId: number; mount: string; callback?: (...params: any[]) => void }[]} streams
   */
  async subscribe(fieldDeviceId, streams) {
    if (!this.connectedDeviceIds.includes(fieldDeviceId)) {
      console.error(`[TrroClient.subscribe] ${fieldDeviceId} is not connected`);
      return { flag: false, message: `${fieldDeviceId} is not connected` };
    }
    if (!streams.length) {
      console.error(`[TrroClient.subscribe] No streams to subscribe`);
      return { flag: false, message: `No streams to subscribe` };
    }
    try {
      const audioStream = streams.find((stream) => stream.mediaType === 'AUDIO');
      const videoStreams = streams.filter((stream) => stream.mediaType === 'VIDEO');
      const { code, message } = await this.client.subscribe({
        fieldDeviceId,
        containers: videoStreams.map(({ streamId, mount, callback }) => ({
          streamId,
          mount,
          ...(callback ? { callback } : {}),
        })),
        ...(audioStream ? { audioContainer: audioStream.mount } : {}),
      });
      if (code !== 0 && code !== 4) {
        console.error(`[TRROSDK.subscribe] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      this.subscribedStreamIds[fieldDeviceId] = streams.map(({ mediaType, streamId, mount }) => ({
        mediaType,
        streamId,
        mount,
      }));
      console.log(`[TrroClient.subscribe] Successfully subscribed streams from ${fieldDeviceId}, code: ${code}`);
      return { flag: true, message: `Successfully subscribed streams from ${fieldDeviceId}` };
    } catch (error) {
      console.error(`[TrroClient.subscribe] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  /**
   * @param {string} fieldDeviceId
   * @param {{ mediaType: 'VIDEO'|'AUDIO'; streamId: number; }[]} streams
   */
  async unsubscribe(fieldDeviceId, streams) {
    if (!this.connectedDeviceIds.includes(fieldDeviceId)) {
      console.error(`[TrroClient.unsubscribe] ${fieldDeviceId} is not connected`);
      return { flag: false, message: `${fieldDeviceId} is not connected` };
    }
    if (!streams.length) {
      console.error(`[TrroClient.unsubscribe] No streams to unsubscribe`);
      return { flag: false, message: `No streams to unsubscribe` };
    }
    try {
      const audioStream = streams.find((stream) => stream.mediaType === 'AUDIO');
      const videoStreams = streams.filter((stream) => stream.mediaType === 'VIDEO');
      const { code, message } = await this.client.unsubscribe({
        fieldDeviceId,
        streamIds: videoStreams.map(({ streamId }) => streamId),
        ...(audioStream ? { audioStream: true } : { audioStream: false }),
      });
      if (code !== 0) {
        console.error(`[TRROSDK.unsubscribe] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      this.subscribedStreamIds[fieldDeviceId] = this.subscribedStreamIds[fieldDeviceId].filter(
        (stream) =>
          !streams.find(({ mediaType, streamId }) => stream.mediaType === mediaType && stream.streamId === streamId),
      );
      console.log(`[TrroClient.unsubscribe] Successfully unsubscribed streams from ${fieldDeviceId}`);
      return { flag: true, message: `Successfully unsubscribed streams from ${fieldDeviceId}` };
    } catch (error) {
      console.error(`[TrroClient.unsubscribe] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  /**
   * @param {MediaStreamTrack} audioStreamTrack
   */
  async publishAudioTrack(audioStreamTrack) {
    try {
      const { code, message } = await this.client.publish({
        audioTrack: audioStreamTrack,
      });
      if (code === 8) {
        console.error(`[TRROSDK.publish] No permission to publish audio track.`);
        return { flag: false, message: 'No permission to publish audio track.' };
      }
      if (code === 4) {
        console.error(`[TRROSDK.publish] Must subscribe a video stream before publishing audio track.`);
        return { flag: false, message: 'Must subscribe a video stream before publishing audio track.' };
      }
      if (code !== 0) {
        console.error(`[TRROSDK.publish] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      console.log(`[TrroClient.publishAudioTrack] Successfully published audio track`);
      return { flag: true, message: `Successfully published audio track` };
    } catch (error) {
      console.error(`[TrroClient.publishAudioTrack] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  async unpublishAudioTrack() {
    try {
      const { code, message } = await this.client.unpublish();
      if (code !== 0) {
        console.error(`[TRROSDK.unpublish] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      console.log(`[TrroClient.unpublishAudioTrack] Successfully unpublished audio track`);
      return { flag: true, message: `Successfully unpublished audio track` };
    } catch (error) {
      console.error(`[TrroClient.unpublishAudioTrack] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  async muteLocalAudioTrack() {
    try {
      const { code, message } = await this.client.muteAudio({ deviceId: this.config.remoteDeviceId, mute: true });
      if (code !== 0) {
        console.error(`[TRROSDK.muteAudio] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      console.log(`[TrroClient.muteLocalAudioTrack] Successfully muted audio track`);
      return { flag: true, message: `Successfully muted audio track` };
    } catch (error) {
      console.error(`[TrroClient.muteLocalAudioTrack] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  async unmuteLocalAudioTrack() {
    try {
      const { code, message } = await this.client.muteAudio({ deviceId: this.config.remoteDeviceId, mute: false });
      if (code !== 0) {
        console.error(`[TRROSDK.muteAudio] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      console.log(`[TrroClient.unmuteLocalAudioTrack] Successfully unmuted audio track`);
      return { flag: true, message: `Successfully unmuted audio track` };
    } catch (error) {
      console.error(`[TrroClient.unmuteLocalAudioTrack] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  async getGatewayList() {
    try {
      const { code, data, message } = await this.client.pullGatewayList();
      if (code !== 0) {
        console.error(`[TRROSDK.pullGatewayList] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      if (!Array.isArray(data)) {
        console.error(`[TrroClient.getGatewayList] Invalid gateway list data`);
        return { flag: false, message: `Invalid gateway list data` };
      }
      console.log(`[TrroClient.getGatewayList] Successfully updated gateway list`);
      this.gatewayList = data.map((gateway) => ({
        ...gateway,
        fieldDeviceId: gateway.deviceId.startsWith(`${this.config.projectId}/`)
          ? gateway.deviceId.slice(this.config.projectId.length + 1)
          : gateway.deviceId,
      }));
      return { flag: true, message: `Successfully updated gateway list` };
    } catch (error) {
      console.error(`[TrroClient.getGatewayList] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  /**
   * @param {string} fieldDeviceId
   * @param {object} options
   * @param {'MASTER'|'GUEST'} options.permission
   * @returns {Promise<{ flag: boolean; message: string }>}
   */
  async requestPermission(fieldDeviceId, { permission }) {
    try {
      const { code, data, message } = await this.client.requestPermission(fieldDeviceId, {
        permission: permission.toLowerCase(),
      });
      if (code !== 0 || data?.permission !== permission.toLowerCase()) {
        console.error(`[TRROSDK.requestPermission] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      this.permissions[fieldDeviceId] = permission;
      console.log(`[TrroClient.requestPermission] Successfully requested permission from ${fieldDeviceId}`);
      return { flag: true, message: `Successfully requested permission from ${fieldDeviceId}` };
    } catch (error) {
      console.error(`[TrroClient.requestPermission] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  /**
   * @param {string} fieldDeviceId
   * @returns {Promise<{ flag: boolean; message: string; report?: any }>}
   */
  async requestDiagnosisReport(fieldDeviceId) {
    if (this.diagnosisReportRequesting) {
      console.warn(`[TrroClient.requestDiagnosisReport] Diagnosis report is already requesting`);
      return { flag: false, message: `Diagnosis report is already requesting` };
    }
    try {
      this.diagnosisReportRequesting = true;
      const report = await this.client.requestDiagnosisReport(fieldDeviceId);
      this.diagnosisReportRequesting = false;
      this.diagnosisReport = report;
      return { flag: true, message: `Successfully requested diagnosis report`, report };
    } catch (error) {
      this.diagnosisReportRequesting = false;
      this.diagnosisReport = undefined;
      console.error(`[TrroClient.requestDiagnosisReport] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  /**
   * @param {string} fieldDeviceId
   * @param {string} data
   * @param {boolean} ordered
   * @param {'sctp'|'mqtt'} channelType
   */
  async sendControlData(fieldDeviceId, data, ordered, channelType = 'sctp') {
    try {
      const { code, message } = await this.client.sendControlData(fieldDeviceId, {
        data,
        ordered: channelType === 'sctp' ? ordered : undefined,
        channelType,
      });
      if (code === 42) {
        console.error(`[TRROSDK.sendControlData] No permission to send control data.`);
        return { flag: false, message: 'No permission to send control data.' };
      }
      if (code === 45) {
        console.error(`[TRROSDK.sendControlData] Must subscribe a video stream before sending control data.`);
        return { flag: false, message: 'Must subscribe a video stream before sending control data.' };
      }
      if (code !== 0) {
        console.error(`[TRROSDK.sendControlData] ${message} (${code})`);
        return { flag: false, message: `${message} (${code})` };
      }
      console.log(`[TrroClient.sendControlData] Successfully sent control data to ${fieldDeviceId} via ${channelType}`);
      return { flag: true, message: `Successfully sent control data to ${fieldDeviceId} via ${channelType}` };
    } catch (error) {
      console.error(`[TrroClient.sendControlData] ${error.name}: ${error.message}`);
      return { flag: false, message: `${error.name}: ${error.message}` };
    }
  }

  /** @param {TrroClientConfig} */
  compareConfigs(config) {
    for (const key of Object.keys(this.config)) {
      if (this.config[key] !== config[key]) {
        return false;
      }
    }
    return true;
  }
}

// ====================
// Global Variables
// ====================

/** @type {TrroClientConfig} */
const initConfig = {
  cloudMode: 'public',
  serverIp: [],
  port: null,
  https: true,
  mqttURL: [],
  mqttPort: null,
  mqttForceLogin: false,
  sdkMode: 'server2',
  projectId: '',
  remoteDeviceId: '',
  password: '',
  showLog: true,
  reportLog: true,
};

/** @type {TrroClient} TRRO client instance */
let trroClient = null;

/** @type {string[]} Field device IDs list */
let fieldDeviceIds = [];

/** @type {number[]} Stream IDs list */
let streamIds = [];

/** Whether to subscribe audio stream */
let audioStream = false;

/** Custom message content */
let customMessage = '';

/** Whether to use ordered data channel */
let orderedDataChannel = true;

/** Channel type for sending control data: 'sctp' or 'mqtt' */
let channelType = 'sctp';

/** Whether microphone permission is granted */
let hasMicrophonePermission = false;

/** @type {{ id: string; label: string; }[]} Local audio device list */
let localAudioDeviceList = [];

/** Selected local audio device ID */
let selectedLocalAudioDeviceId = '';

/** @type {MediaStreamTrack} Local audio track */
let localAudioTrack = null;

/** Whether local audio track is published */
let isLocalAudioTrackPublished = false;

/** Currently diagnosing field device ID */
let diagnosingFieldDeviceId = '';

/** Session permission token */
let sessionPermissionToken = '';

/** Video buffer configuration (shared by the per-stream stats card) */
const videoBufferConfig = {
  method: 'playoutDelayHint', // Default method; switched by the UI card
  target: null, // null means use default value
};

/**
 * Per-stream webrtcStats cache; video streams only.
 * Structure: { [fieldDeviceId: string]: { [trackId: string]: { streamId, jitterBuffer, jitterBufferTarget, rtt, lost, fps } } }
 * UI refreshes at most once per second to avoid high-frequency DOM updates.
 */
const streamStatsMap = {};
/**
 * Per-field-device gatewayStats cache.
 * Structure: { [fieldDeviceId: string]: { [streamId: number]: { rtt, lost, fps, bps, jitter } } }
 * Populated by gatewayStats observer events; read on each render cycle.
 */
const gatewayStatsMap = {};
/** @type {number | null} */
let streamStatsRenderTimer = null;
/** Per-stream jitterBufferTarget draft values (trackId -> number) */
const streamStatsJbtDraft = {};

// HTML DOM Elements Listeners

$('#server-ip-input').on('change', (e) => {
  const { value } = e.target;
  initConfig.serverIp = value
    .split(',')
    .map((v) => v.trim())
    .filter((v) => v?.length);
});
$('#port-input').on('change', (e) => {
  const { value } = e.target;
  initConfig.port = Number(value);
});
$('#project-id-input').on('change', (e) => {
  const { value } = e.target;
  initConfig.projectId = value;
});
$('#remote-device-id-input').on('change', (e) => {
  const { value } = e.target;
  initConfig.remoteDeviceId = value;
});
$('#password-input').on('change', (e) => {
  const { value } = e.target;
  initConfig.password = value;
});
$('#mqtt-force-login-input').on('change', (e) => {
  const { checked } = e.target;
  initConfig.mqttForceLogin = checked;
});
$('#field-device-id-input').on('change', (e) => {
  const { value } = e.target;
  const ids = value
    .split(',')
    .map((v) => v.trim())
    .filter((v) => v?.length);
  setFieldDeviceList({ fieldDeviceIdList: ids, mode: 'REPLACE', from: 'INPUT' });
});
$('#stream-id-input').on('change', (e) => {
  const { value } = e.target;
  const ids = value
    .split(',')
    .map((v) => v.trim())
    .filter((v) => v?.length)
    .map((value) => Number(value));
  streamIds = ids;
});
$('#audio-stream-input').on('change', (e) => {
  const { checked } = e.target;
  audioStream = checked;
});
$('#remote-device-custom-message-input').on('change', (e) => {
  const { value } = e.target;
  customMessage = value;
});
$('#ordered-dc-input').on('change', (e) => {
  const { checked } = e.target;
  orderedDataChannel = checked;
});
$('#channel-type-select')?.on('change', (e) => {
  const { value } = e.target;
  channelType = value;

  // If channel type is set to MQTT, disable ordered data channel option
  if (value === 'mqtt') {
    $('#ordered-dc-input').prop('disabled', true);
  } else {
    $('#ordered-dc-input').prop('disabled', false);
  }
});
$('#local-audio-device-select').on('change', (e) => {
  const { value } = e.target;
  selectedLocalAudioDeviceId = value;
});
$('#diagnosing-field-device-select').on('change', (e) => {
  const { value } = e.target;
  diagnosingFieldDeviceId = value;
});
$('#session-permission-token-input').on('change', (e) => {
  const { value } = e.target;
  sessionPermissionToken = value;
});

/**
 * Set session permission token after login
 */
const setSessionPermissionToken = async () => {
  try {
    if (!trroClient?.client) {
      console.warn('[setSessionPermissionToken] TRROSDK instance not initialized.');
      notify({
        title: t('error_title'),
        description: t('error_description_not_logged_in'),
      });
      return;
    }

    const token = $('#session-permission-token-input').val();
    if (!token?.trim()) {
      console.warn('[setSessionPermissionToken] Session permission token is empty.');
      notify({
        title: t('error_title'),
        description: t('set_session_permission_token_error_empty'),
      });
      return;
    }

    // Update the session permission token variable
    sessionPermissionToken = token.trim();

    console.log('[setSessionPermissionToken] Session permission token updated:', sessionPermissionToken);
    notify({
      title: t('success_title'),
      description: t('set_session_permission_token_success'),
    });
  } catch (error) {
    console.error(`[setSessionPermissionToken] ${error.name}: ${error.message}`);
    notify({
      title: t('error_title'),
      description: `${error.name}: ${error.message}`,
    });
  }
};

/**
 * Generate device temporary password
 * @param {string} deviceId - Device ID
 * @param {string} key - Project-shared key
 * @param {number} expireTime - Expiration timestamp
 * @returns {Promise<string>} Generated temporary password
 */
async function generateDevicePassword(deviceId, key, expireTime) {
  const content = deviceId + expireTime.toString();
  console.info(`[generateDevicePassword] plainText: ${content}`);
  console.info(`[generateDevicePassword] key: ${key}`);
  const encoder = new TextEncoder();
  const keyData = encoder.encode(key);
  const contentData = encoder.encode(content);

  const cryptoKey = await window.crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, [
    'sign',
  ]);

  const signature = await window.crypto.subtle.sign('HMAC', cryptoKey, contentData);

  // Convert ArrayBuffer to base64 safely
  const bytes = new Uint8Array(signature);
  let binary = '';
  for (const byte of bytes) {
    binary += String.fromCharCode(byte);
  }
  const base64 = btoa(binary);
  console.info(`[generateDevicePassword] base64: ${base64}`);
  const cleaned = base64.replace(/\+/g, '').replace(/\//g, '').replace(/=/g, '');
  console.info(`[generateDevicePassword] cleaned: ${cleaned}`);

  return `${cleaned}_${expireTime}`;
}

/**
 * Generate session authorization code
 * @param {string} remoteDeviceId - Remote device ID
 * @param {string} fieldDeviceId - Field device ID
 * @param {string} key - Shared key
 * @param {number} expireTime - Expiration timestamp
 * @returns {Promise<string>} Generated authorization code
 */
async function generateSessionAuthCode(remoteDeviceId, fieldDeviceId, key, expireTime) {
  const content = remoteDeviceId + fieldDeviceId + expireTime.toString();
  console.info(`[generateSessionAuthCode] plainText: ${content}`);
  console.info(`[generateSessionAuthCode] key: ${key}`);
  const encoder = new TextEncoder();
  const keyData = encoder.encode(key);
  const contentData = encoder.encode(content);

  const cryptoKey = await window.crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, [
    'sign',
  ]);

  const signature = await window.crypto.subtle.sign('HMAC', cryptoKey, contentData);

  // Convert ArrayBuffer to base64 safely
  const bytes = new Uint8Array(signature);
  let binary = '';
  for (const byte of bytes) {
    binary += String.fromCharCode(byte);
  }
  const base64 = btoa(binary);
  console.info(`[generateSessionAuthCode] base64: ${base64}`);
  const cleaned = base64.replace(/\+/g, '').replace(/\//g, '').replace(/=/g, '');
  console.info(`[generateSessionAuthCode] cleaned: ${cleaned}`);

  return `${cleaned}_${expireTime}`;
}

/**
 * Toggle credentials generator visibility
 */
const toggleCredentialsGenerator = () => {
  const content = $('#credentials-generator-content');
  const arrow = $('#credentials-generator-arrow');

  if (content.is(':visible')) {
    content.hide();
    arrow.text('▶');
  } else {
    content.show();
    arrow.text('▼');
  }
};

/**
 * Toggle session permission token input visibility
 */
const toggleSessionPermissionToken = () => {
  const content = $('#session-permission-token-content');
  const arrow = $('#session-permission-token-arrow');

  if (content.is(':visible')) {
    content.hide();
    arrow.text('▶');
  } else {
    content.show();
    arrow.text('▼');
  }
};

/**
 * Auto expand session permission token section
 */
const expandSessionPermissionToken = () => {
  const content = $('#session-permission-token-content');
  const arrow = $('#session-permission-token-arrow');

  if (!content.is(':visible')) {
    content.show();
    arrow.text('▼');
  }
};

/**
 * Generate temporary credentials (field device password, remote device password, and session authorization code)
 */
async function generateTempCredentials() {
  try {
    const sharedKey = $('#shared-key-input').val();
    const tempProjectId = $('#temp-project-id-input').val();
    let tempFieldDeviceId = $('#temp-field-device-id-input').val();
    let tempRemoteDeviceId = $('#temp-remote-device-id-input').val();
    let tempFieldDeviceIdWithPrefix = tempFieldDeviceId;
    let tempRemoteDeviceIdWithPrefix = tempRemoteDeviceId;
    const expireTimeMinutes = parseInt($('#expire-time-input').val(), 10);

    if (!sharedKey) {
      notify({
        title: t('error_title'),
        description: t('generate_temp_credentials_error_empty_shared_key'),
      });
      return;
    }

    if (!tempFieldDeviceId) {
      notify({
        title: t('error_title'),
        description: t('generate_temp_credentials_error_empty_field_device_id'),
      });
      return;
    }

    if (!tempRemoteDeviceId) {
      notify({
        title: t('error_title'),
        description: t('generate_temp_credentials_error_empty_remote_device_id'),
      });
      return;
    }

    if (!expireTimeMinutes || expireTimeMinutes <= 0) {
      notify({
        title: t('error_title'),
        description: t('generate_temp_credentials_error_invalid_expire_time'),
      });
      return;
    }

    // Use tempProjectId from the generator if provided, otherwise fall back to initConfig.projectId
    const effectiveProjectId = tempProjectId?.trim() || initConfig.projectId;

    if (initConfig.cloudMode === 'public' || initConfig.cloudMode === 'public_intl') {
      // In public cloud mode, project ID is required for generating device ID prefixes
      if (!effectiveProjectId) {
        notify({
          title: t('error_title'),
          description: t('generate_temp_credentials_error_empty_project_id'),
        });
        return;
      }

      // If cloud mode is public or public_intl, the project ID prefix must be added to the device ID
      if (!tempFieldDeviceId.startsWith(`${effectiveProjectId}/`)) {
        tempFieldDeviceIdWithPrefix = `${effectiveProjectId}/${tempFieldDeviceId}`;
      }
      if (!tempRemoteDeviceId.startsWith(`${effectiveProjectId}/`)) {
        tempRemoteDeviceIdWithPrefix = `${effectiveProjectId}/${tempRemoteDeviceId}`;
      }
    }

    const currentTime = Math.floor(Date.now() / 1000 / 60);
    const expireTimeMinutesTimestamp = currentTime + expireTimeMinutes;

    // Generate field device temporary password (for field device initialization)
    const fieldDevicePassword = await generateDevicePassword(
      tempFieldDeviceIdWithPrefix,
      sharedKey,
      expireTimeMinutesTimestamp,
    );

    // Generate remote device temporary password (for remote device login)
    const remoteDevicePassword = await generateDevicePassword(
      tempRemoteDeviceIdWithPrefix,
      sharedKey,
      expireTimeMinutesTimestamp,
    );

    // Generate session authorization code
    const authCode = await generateSessionAuthCode(
      tempRemoteDeviceIdWithPrefix,
      tempFieldDeviceIdWithPrefix,
      sharedKey,
      expireTimeMinutesTimestamp,
    );

    // Sync the effective project ID to the login form if it was provided in the generator
    if (tempProjectId?.trim()) {
      $('#project-id-input').val(tempProjectId.trim());
      initConfig.projectId = tempProjectId.trim();
    }
    $('#remote-device-id-input').val(tempRemoteDeviceId);
    $('#password-input').val(remoteDevicePassword);
    $('#field-device-id-input').val(tempFieldDeviceId);
    $('#session-permission-token-input').val(authCode);

    // Update config variables
    initConfig.remoteDeviceId = tempRemoteDeviceId;
    initConfig.password = remoteDevicePassword;
    sessionPermissionToken = authCode;
    setFieldDeviceList({
      fieldDeviceIdList: [tempFieldDeviceId],
      mode: 'REPLACE',
      from: 'GENERATOR',
    });

    // Display generated credentials
    $('#generated-field-password-display').text(fieldDevicePassword);
    $('#generated-remote-password-display').text(remoteDevicePassword);
    $('#generated-authcode-display').text(authCode);
    $('#generated-credentials').removeClass('hidden');

    // Auto expand session permission token section
    expandSessionPermissionToken();

    notify({
      title: t('success_title'),
      description: t('generate_temp_credentials_success', {
        expireTime: new Date(expireTimeMinutesTimestamp * 60 * 1000).toLocaleString(),
      }),
    });

    console.log('[generateTempCredentials] Generated credentials:', {
      fieldDeviceId: tempFieldDeviceId,
      remoteDeviceId: tempRemoteDeviceId,
      fieldDevicePassword,
      remoteDevicePassword,
      authCode,
      expireTime: expireTimeMinutesTimestamp,
      expireTimeReadable: new Date(expireTimeMinutesTimestamp * 60 * 1000).toLocaleString(),
    });
  } catch (error) {
    console.error('[generateTempCredentials] Error:', error);
    notify({
      title: t('error_title'),
      description: t('generate_temp_credentials_failed', { error: error.message }),
    });
  }
}

// Business Logics

$('#sdk-version').text(TRROSDK.version || '');

const login = async () => {
  if (trroClient) {
    const isSameConfig = trroClient.compareConfigs(initConfig);
    if (trroClient.client && isSameConfig) {
      return;
    }
    // If the client is already initialized, or the config passed in is different from the current config, destroy the current client and create a new one.
    if (trroClient.client || !isSameConfig) {
      await trroClient.destroy();
    }
    trroClient = null;
  }
  try {
    updateLoginStatus({ status: 'LOADING' });
    trroClient = new TrroClient(initConfig, {
      onKicked: (code, reason) => {
        updateLoginStatus({ status: 'KICKED', message: `${code}: ${reason}` });
      },
      onError: (code, message) => {
        // SDK error will be handled internally by observer.onError
        // No additional handling needed here
        console.info(`[login] SDK error callback received, code: ${code}, message: ${message}`);
      },
      onFieldDevicePermissionChange: () => {
        const newPermissions = Object.entries(trroClient?.permissions ?? []).map(([id, perm]) => ({
          fieldDeviceId: id,
          permission: perm,
        }));
        if (newPermissions?.length) {
          updateFieldDevicePermissionState(newPermissions);
        }
      },
    });
    console.log(`[login] TRROSDK instance initialized.`);
    const initRes = await trroClient.init();
    if (!initRes.flag) {
      let errorMessage = initRes.message || '';
      if (initRes.message.toLowerCase().includes('bad username or password')) {
        errorMessage = t('login_error_auth');
      } else if (initRes.message.toLowerCase().includes('identifier rejected')) {
        errorMessage = t('login_error_multisession');
      }
      console.error(`[login] TRRO service initialization failed, ${errorMessage}`);
      notify({
        title: t('login_error'),
        description: errorMessage,
      });
      // 初始化失败兜底：销毁脏实例并清空全局引用，避免下次点击登录被 isSameConfig 短路
      try {
        await trroClient.destroy();
      } catch (destroyError) {
        console.warn(`[login] Cleanup after init failure failed, ${destroyError?.name}: ${destroyError?.message}`);
      }
      trroClient = null;
      updateLoginStatus({ status: 'FAILED', message: errorMessage });
      return;
    }
    console.log(`[login] TRRO service initialized.`);

    // Set session permission token if provided
    if (sessionPermissionToken && fieldDeviceIds.length > 0) {
      for (const fieldDeviceId of fieldDeviceIds) {
        try {
          trroClient.client.setSessionPermissionToken(fieldDeviceId, sessionPermissionToken);
          console.log(`[login] Session permission token set for ${fieldDeviceId}`);
        } catch (error) {
          console.error(`[login] Failed to set session permission token for ${fieldDeviceId}:`, error);
        }
      }
    }

    updateLoginStatus({ status: 'SUCCESS' });
  } catch (error) {
    console.error(`[login] ${error.name}: ${error.message}`);
    notify({
      title: t('login_error'),
      description: `${error.name}: ${error.message}`,
    });
    // 异常分支同样兜底清理，确保用户可以再次点击登录直接重试
    if (trroClient) {
      try {
        await trroClient.destroy();
      } catch (destroyError) {
        console.warn(`[login] Cleanup after exception failed, ${destroyError?.name}: ${destroyError?.message}`);
      }
      trroClient = null;
    }
    updateLoginStatus({ status: 'FAILED', message: `${error.name}: ${error.message}` });
  }
};

const logout = async () => {
  try {
    if (!trroClient?.client) {
      console.warn('[logout] TRROSDK instance not initialized.');
      return;
    }
    const destroyRes = await trroClient.destroy();
    if (!destroyRes.flag) {
      console.warn(`[logout] Failed to destroy TRROSDK instance, ${destroyRes.message}`);
    }
    trroClient = null;
    updateLoginStatus({ status: 'CLOSE' });
  } catch (error) {
    console.warn(`[logout] ${error.name}: ${error.message}`);
    trroClient = null;
    updateLoginStatus({ status: 'CLOSE' });
  }
};

const getGatewayList = async () => {
  try {
    if (!trroClient?.client) {
      console.warn('[getGatewayList] TRROSDK instance not initialized.');
      return;
    }
    const gatewayListRes = await trroClient.getGatewayList();
    if (!gatewayListRes.flag) {
      console.error(`[getGatewayList] Failed to update gateway list, ${gatewayListRes.message}`);
      notify({
        title: t('update_gateway_list_failed'),
        description: gatewayListRes.message,
      });
      return;
    }
    updateGatewayTable(trroClient.gatewayList);
  } catch (error) {
    console.error(`[getGatewayList] ${error.name}: ${error.message}`);
    notify({
      title: t('update_gateway_list_failed'),
      description: `${error.name}: ${error.message}`,
    });
  }
};

const queryGatewayInfo = async () => {
  try {
    if (!trroClient?.client) {
      console.warn('[queryGatewayInfo] TRROSDK instance not initialized.');
      notify({
        title: t('query_gateway_info_failed'),
        description: t('error_description_not_logged_in'),
      });
      return;
    }

    const fieldDeviceInput = $('#field-device-id-input').val();
    if (!fieldDeviceInput?.trim()) {
      console.warn('[queryGatewayInfo] Field device ID is empty.');
      notify({
        title: t('query_gateway_info_failed'),
        description: t('error_description_empty_field_device_id_input'),
      });
      return;
    }

    // Query the first device ID by default
    const deviceId = fieldDeviceInput.split(',')[0].trim();

    // Display loading message
    $('#gateway-info-display').html(`<span class="text-blue-500">${t('query_gateway_info_loading')}</span>`);

    const { code, data, message } = await trroClient.client.getGatewayInfo(deviceId);

    if (code !== 0) {
      console.error(`[queryGatewayInfo] Failed to query gateway info, ${message} (${code})`);
      $('#gateway-info-display').html(
        `<span class="text-red-500">${t('query_gateway_info_failed')}: ${message}</span>`,
      );
      notify({
        title: t('query_gateway_info_failed'),
        description: message,
      });
      return;
    }

    // Display gateway status
    const statusText =
      data?.status === 'ready'
        ? `<span class="text-green-500">${t('gateway_status_online')}</span>`
        : `<span class="text-gray-500">${data?.status || t('gateway_status_unknown')}</span>`;

    const infoText = `
      ${t('gateway_status')}: ${statusText} | 
      ${t('gateway_version')}: ${data?.version || '-'} | 
      ${t('gateway_stream_count')}: ${data?.streamCount ?? '-'}
    `;

    $('#gateway-info-display').html(infoText);

    console.log('[queryGatewayInfo] Gateway info:', data);
    notify({
      title: t('query_gateway_info_success'),
      description: `${deviceId}: ${data?.status}`,
    });
  } catch (error) {
    console.error(`[queryGatewayInfo] ${error.name}: ${error.message}`);
    $('#gateway-info-display').html(`<span class="text-red-500">${t('query_gateway_info_failed')}</span>`);
    notify({
      title: t('query_gateway_info_failed'),
      description: `${error.name}: ${error.message}`,
    });
  }
};

const startConnection = async () => {
  try {
    if (!trroClient?.client) {
      console.warn('[startConnection] TRROSDK instance not initialized.');
      notify({
        title: t('start_connection_failed'),
        description: t('error_description_not_logged_in'),
      });
      return;
    }
    if (!fieldDeviceIds.length) {
      console.warn('[startConnection] Empty field device ID list.');
      notify({
        title: t('start_connection_failed'),
        description: t('error_description_empty_field_device_id_input'),
      });
      return;
    }
    if (!streamIds.length) {
      console.warn('[startConnection] Empty stream ID list.');
      notify({
        title: t('start_connection_failed'),
        description: t('error_description_empty_stream_id_input'),
      });
      return;
    }
    const connected = fieldDeviceIds.filter((id) => trroClient.connectedDeviceIds.includes(id));
    console.info(`[startConnection] already connected: ${connected.join(', ')}`);
    const needToDisconnect = trroClient.connectedDeviceIds.filter((id) => !fieldDeviceIds.includes(id));
    console.info(`[startConnection] need to disconnect: ${needToDisconnect.join(', ')}`);
    const needToConnect = fieldDeviceIds.filter((id) => !trroClient.connectedDeviceIds.includes(id));
    console.info(`[startConnection] need to connect: ${needToConnect.join(', ')}`);
    for (const id of connected) {
      const currentStreams = trroClient.subscribedStreamIds[id];
      const currentVideoSreamIds = currentStreams
        .filter(({ mediaType }) => mediaType === 'VIDEO')
        .map(({ streamId }) => streamId);
      const audioStreamSubscribed = !!currentStreams.find(({ mediaType }) => mediaType === 'AUDIO');
      const needToUnsubscibe = [];
      for (const streamId of currentVideoSreamIds) {
        if (!streamIds.includes(streamId)) {
          needToUnsubscibe.push({
            mediaType: 'VIDEO',
            streamId,
          });
        }
      }
      if (!audioStream && audioStreamSubscribed) {
        needToUnsubscibe.push({
          mediaType: 'AUDIO',
          streamId: 0,
        });
      }
      if (needToUnsubscibe.length) {
        await trroClient.unsubscribe(id, needToUnsubscibe);
      }
      const safeId = sanitizeIdForDom(id);
      const streams = [
        // eslint-disable-next-line no-loop-func
        ...streamIds.map((i) => ({
          mediaType: 'VIDEO',
          streamId: i,
          mount: `#field-video-${safeId}-${i}`,
          callback: (data) => {
            const { latency } = data;
            if (latency !== undefined && latency !== null) {
              const infoElem = $(`#field-video-${safeId}-${i}-info`);
              if (infoElem) {
                infoElem.text(
                  t('remote_media_video_stream_title_with_latency', {
                    streamId: i,
                    latency,
                  }),
                );
              }
            }
          },
        })),
        ...(audioStream
          ? [
              {
                mediaType: 'AUDIO',
                streamId: 0,
                mount: `#field-audio-${safeId}`,
                callback: () => {},
              },
            ]
          : []),
      ];
      updateMediaWrapper(id, streams);
      await trroClient.subscribe(id, streams);
    }
    for (const id of needToDisconnect) {
      await trroClient.disconnect(id);
    }
    for (const id of needToConnect) {
      await trroClient.connect(id);
      const safeId = sanitizeIdForDom(id);
      const streams = [
        // eslint-disable-next-line no-loop-func
        ...streamIds.map((i) => ({
          mediaType: 'VIDEO',
          streamId: i,
          mount: `#field-video-${safeId}-${i}`,
          callback: (data) => {
            const { latency } = data;
            if (latency !== undefined && latency !== null) {
              const infoElem = $(`#field-video-${safeId}-${i}-info`);
              if (infoElem) {
                infoElem.text(
                  t('remote_media_video_stream_title_with_latency', {
                    streamId: i,
                    latency,
                  }),
                );
              }
            }
          },
        })),
        ...(audioStream
          ? [
              {
                mediaType: 'AUDIO',
                streamId: 0,
                mount: `#field-audio-${safeId}`,
                callback: () => {},
              },
            ]
          : []),
      ];
      updateMediaWrapper(id, streams);
      await trroClient.subscribe(id, streams);
    }
    updateFieldDeviceConnectionState({
      data: trroClient.connectedDeviceIds.map((id) => ({
        fieldDeviceId: id,
        state: 'CONNECTED',
      })),
    });
    notify({ title: t('start_connection_success') });
  } catch (error) {
    console.error(`[startConnection] ${error.name}: ${error.message}`);
    notify({
      title: t('start_connection_failed'),
      description: `${error.name}: ${error.message}`,
    });
  }
};

const endConnection = async () => {
  try {
    if (!trroClient?.client) {
      console.warn(`[endConnection] TRROSDK instance not initialized.`);
      return;
    }
    const currentConnectedIds = trroClient.connectedDeviceIds;
    for (const id of currentConnectedIds) {
      await trroClient.disconnect(id);
    }
    updateFieldDeviceConnectionState({
      data: currentConnectedIds.map((id) => ({
        fieldDeviceId: id,
        state: 'DISCONNECTED',
      })),
    });
    notify({ title: t('end_connection_success') });
  } catch (error) {
    console.error(`[endConnection] ${error.name}: ${error.message}`);
    notify({
      title: t('end_connection_failed'),
      description: `${error.name}: ${error.message}`,
    });
  }
};

/**
 * @param {'MASTER'|'GUEST'} permission
 */
const requestPermmision = async (permission) => {
  try {
    if (!trroClient?.client) {
      console.warn(`[requestPermmision] TRROSDK instance not initialized.`);
      notify({
        title: t('request_permission_failed'),
        description: t('error_description_not_logged_in'),
      });
      return;
    }

    if (!fieldDeviceIds.length) {
      console.warn(`[requestPermmision] No field device IDs selected.`);
      notify({
        title: t('request_permission_failed'),
        description: t('error_description_empty_field_device_id_input'),
      });
      return;
    }

    let success = true;
    for (const id of fieldDeviceIds) {
      const { flag, message } = await trroClient.requestPermission(id, { permission });
      if (!flag) {
        success = false;
        notify({
          title: t('request_permission_failed'),
          description: `${id}, ${message}`,
        });
      }
    }
    const newPermissions = Object.entries(trroClient?.permissions ?? []).map(([id, perm]) => ({
      fieldDeviceId: id,
      permission: perm,
    }));
    if (newPermissions?.length) {
      updateFieldDevicePermissionState({ data: newPermissions });
    }
    if (success) {
      notify({ title: t('request_permission_success') });
    }
  } catch (error) {
    console.error(`[requestPermmision] ${error.name}: ${error.message}`);
    notify({
      title: t('request_permission_failed'),
      description: `${error.name}: ${error.message}`,
    });
  }
};

/**
 * @param {object} options
 * @param {string[]} options.fieldDeviceIdList
 * @param {'ADD'|'REPLACE'} options.mode
 * @param {'INPUT'|'SELECT'} options.from
 */
/**
 * Handle individual gateway checkbox change
 * @param {string} fieldDeviceId - Field device ID
 * @param {boolean} checked - Checkbox checked state
 */
const handleGatewayCheckboxChange = (fieldDeviceId, checked) => {
  if (checked) {
    // Add device
    setFieldDeviceList({ fieldDeviceIdList: [fieldDeviceId], mode: 'ADD', from: 'SELECT' });
  } else {
    // Remove device
    setFieldDeviceList({ fieldDeviceIdList: [fieldDeviceId], mode: 'REMOVE', from: 'SELECT' });
  }
};

/**
 * Handle gateway table "select all" checkbox change
 * @param {boolean} checked - Checkbox checked state
 */
const handleGatewayCheckAllChange = (checked) => {
  const allFieldDeviceIds = trroClient?.gatewayList?.map((gateway) => gateway.fieldDeviceId) ?? [];
  if (checked) {
    // Add all devices
    setFieldDeviceList({ fieldDeviceIdList: allFieldDeviceIds, mode: 'ADD', from: 'SELECT' });
  } else {
    // Remove all devices
    setFieldDeviceList({ fieldDeviceIdList: allFieldDeviceIds, mode: 'REMOVE', from: 'SELECT' });
  }
};

const setFieldDeviceList = async ({ fieldDeviceIdList, mode, from }) => {
  if (!fieldDeviceIdList?.length) {
    return;
  }
  console.info(`[setFieldDeviceList] fieldDeviceIdList: ${fieldDeviceIdList}, mode: ${mode}, from: ${from}`);
  let newList;
  if (mode === 'ADD') {
    newList = [...new Set([...fieldDeviceIds, ...fieldDeviceIdList])];
  } else if (mode === 'REMOVE') {
    newList = fieldDeviceIds.filter((id) => !fieldDeviceIdList.includes(id));
  } else {
    newList = fieldDeviceIdList;
  }
  fieldDeviceIds = newList;
  const inputText = $('#field-device-id-input').val();
  const inputArr = inputText
    .split(',')
    .map((v) => v.trim())
    .filter((v) => v?.length);
  if (JSON.stringify(inputArr.sort()) !== JSON.stringify(newList.sort())) {
    $('#field-device-id-input').val(newList.join(','));
    $('#diagnosing-field-device-select').empty();
    for (const id of newList) {
      $('#diagnosing-field-device-select').append(`
        <option value="${id}">${id}</option>
      `);
    }
    diagnosingFieldDeviceId = $('#diagnosing-field-device-select').val();
  }
  if (trroClient?.gatewayList) {
    let maxStreamCount = 0;
    let inGatewayListCount = 0;
    for (const { fieldDeviceId, deviceId, streamCount } of trroClient.gatewayList) {
      if (newList.includes(fieldDeviceId) || newList.includes(deviceId)) {
        maxStreamCount = Math.max(maxStreamCount, streamCount);
        inGatewayListCount += 1;
        if ($(`#gateway-table-check-${fieldDeviceId}`).prop('checked') !== true) {
          $(`#gateway-table-check-${fieldDeviceId}`).prop('checked', true);
        }
      } else {
        if ($(`#gateway-table-check-${fieldDeviceId}`).prop('checked') !== false) {
          $(`#gateway-table-check-${fieldDeviceId}`).prop('checked', false);
        }
      }
    }
    if (maxStreamCount > 0) {
      streamIds = Array.from({ length: maxStreamCount }, (_, i) => i);
      $('#stream-id-input').val(streamIds.join(','));
    }
    if (inGatewayListCount === trroClient.gatewayList.length) {
      if ($('#gateway-table-check-all').prop('checked') !== true) {
        $('#gateway-table-check-all').prop('checked', true);
      }
    } else {
      if ($('#gateway-table-check-all').prop('checked') !== false) {
        $('#gateway-table-check-all').prop('checked', false);
      }
    }
  }
};

const sendMessage = async () => {
  try {
    if (!trroClient?.client) {
      console.warn(`[sendMessage] TRROSDK instance not initialized.`);
      notify({
        title: t('send_message_failed'),
        description: t('error_description_not_logged_in'),
      });
      return;
    }
    if (!customMessage.trim()) {
      console.warn(`[sendMessage] customMessage is empty.`);
      notify({
        title: t('send_message_failed'),
        description: t('error_description_empty_messages'),
      });
      return;
    }

    const targetDeviceIds = channelType === 'mqtt' ? fieldDeviceIds : trroClient.connectedDeviceIds;

    if (!targetDeviceIds.length) {
      console.warn(`[sendMessage] No ${channelType === 'mqtt' ? 'field device' : 'connected'} IDs.`);
      notify({
        title: t('send_message_failed'),
        description:
          channelType === 'mqtt' ? t('error_field_device_id_required') : t('error_description_no_connected_devices'),
      });
      return;
    }

    let success = true;
    for (const id of targetDeviceIds) {
      const { flag, message } = await trroClient.sendControlData(id, customMessage, orderedDataChannel, channelType);
      if (!flag) {
        success = false;
        if (message.includes('No permission')) {
          notify({
            title: t('send_message_failed'),
            description: t('error_description_no_master_permission'),
          });
        } else if (message.includes('Must subscribe') || message.includes('no subscribed streams')) {
          notify({
            title: t('send_message_failed'),
            description:
              channelType === 'mqtt'
                ? t('error_description_mqtt_channel_error', { message })
                : t('error_description_no_subscibed_video_stream'),
          });
        } else {
          notify({
            title: t('send_message_failed'),
            description: `${id}, ${message}`,
          });
        }
      }
    }
    if (success) {
      notify({ title: t('send_message_success') });
    }
  } catch (error) {
    console.error(`[sendMessage] ${error.name}: ${error.message}`);
    notify({
      title: t('send_message_failed'),
      description: `${error.name}: ${error.message}`,
    });
  }
};

const getLocalAudioDeviceList = async () => {
  try {
    if (!hasMicrophonePermission) {
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: true,
        video: false,
      });
      stream.getTracks().forEach((track) => track.stop());
    }
    const devices = await navigator.mediaDevices.enumerateDevices();
    const deviceList = devices
      .filter(({ kind }) => kind === 'audioinput')
      .map(({ deviceId, label }) => ({ id: deviceId, label }));
    console.log(`[getLocalAudioDeviceList] deviceList: ${JSON.stringify(deviceList, null, 2)}`);
    notify({ title: t('get_audio_device_list_success') });
    localAudioDeviceList = deviceList;
    $('#local-audio-device-select').empty();
    for (const { id, label } of deviceList) {
      $('#local-audio-device-select').append(`
        <option value="${id}">${label}</option>
      `);
    }
    selectedLocalAudioDeviceId = $('#local-audio-device-select').val();
  } catch (err) {
    console.error(`[getLocalAudioDeviceList] ${err.name}: ${err.message}`);
    notify({
      title: t('get_audio_device_list_failed'),
      description: err.message,
    });
  }
};

const createLocalAudioTrack = async () => {
  try {
    if (localAudioTrack) {
      console.warn('[createLocalAudioTrack] Already created');
      notify({
        title: t('create_local_audio_track_failed'),
        description: t('error_description_already_created_audio_track'),
      });
      return;
    }
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: !!selectedLocalAudioDeviceId ? { deviceId: selectedLocalAudioDeviceId } : true,
      video: false,
    });
    const [track] = stream.getAudioTracks();
    track.addEventListener('ended', onLocalAudioTrackEnded);
    console.log('[createLocalAudioTrack] Successfully created local audio track');
    notify({
      title: t('create_local_audio_track_success'),
      description: track.label,
    });
    localAudioTrack = track;
    $('#local-audio-device-select').attr('disabled', true);
  } catch (error) {
    console.error(`[createLocalAudioTrack] ${error.name}: ${error.message}`);
    notify({
      title: t('create_local_audio_track_failed'),
      description: `${error.name}: ${error.message}`,
    });
  }
};

const onLocalAudioTrackEnded = () => {
  if (isLocalAudioTrackPublished) {
    unpublishLocalAudioTrack();
  }
};

const destroyLocalAudioTrack = async () => {
  if (!localAudioTrack) {
    console.warn('[destroyLocalAudioTrack] Already destroyed');
    notify({
      title: t('destroy_local_audio_track_failed'),
      description: t('error_description_no_audio_track'),
    });
    return;
  }
  localAudioTrack.stop();
  localAudioTrack = null;
  onLocalAudioTrackEnded();
  console.log('[destroyLocalAudioTrack] Successfully destroyed local audio track');
  notify({
    title: t('destroy_local_audio_track_success'),
  });
  $('#local-audio-device-select').attr('disabled', false);
};

const publishLocalAudioTrack = async () => {
  try {
    if (!trroClient?.client) {
      console.warn(`[publishLocalAudioTrack] TRROSDK instance not initialized.`);
      notify({
        title: t('publishLocalAudioTrack'),
        description: t('error_description_not_logged_in'),
      });
      return;
    }
    if (!localAudioTrack) {
      console.warn('[publishLocalAudioTrack] Already created');
      notify({
        title: t('publish_local_audio_track_failed'),
        description: t('error_description_no_audio_track'),
      });
      return;
    }
    if (isLocalAudioTrackPublished) {
      console.warn('[publishLocalAudioTrack] Already published');
      notify({
        title: t('publish_local_audio_track_failed'),
        description: t('error_description_already_published_audio_track'),
      });
      return;
    }
    const { flag, message } = await trroClient.publishAudioTrack(localAudioTrack);
    if (!flag) {
      if (message.includes('No permission')) {
        notify({
          title: t('publish_local_audio_track_failed'),
          description: t('error_description_no_master_permission'),
        });
      } else if (message.includes('Must subscribe')) {
        notify({
          title: t('publish_local_audio_track_failed'),
          description: t('error_description_no_subscibed_video_stream'),
        });
      } else {
        notify({
          title: t('publish_local_audio_track_failed'),
          description: `${message}`,
        });
      }
      return;
    }
    isLocalAudioTrackPublished = true;
    notify({ title: t('publish_local_audio_track_success') });
  } catch (error) {
    console.error(`[publishLocalAudioTrack] ${error.name}: ${error.message}`);
    isLocalAudioTrackPublished = false;
    notify({
      title: t('publish_local_audio_track_failed'),
      description: `${error.name} ${error.message}`,
    });
  }
};

const unpublishLocalAudioTrack = async () => {
  try {
    if (!trroClient?.client) {
      console.warn(`[unpublishLocalAudioTrack] TRROSDK instance not initialized.`);
      notify({
        title: t('unpublish_local_audio_track_failed'),
        description: t('error_description_not_logged_in'),
      });
      return;
    }
    if (!isLocalAudioTrackPublished) {
      console.warn('[unpublishLocalAudioTrack] Not published');
      notify({
        title: t('unpublish_local_audio_track_failed'),
        description: t('error_description_no_published_audio_stream'),
      });
      return;
    }
    const { flag, message } = await trroClient.unpublishAudioTrack();
    console.log('unpublishLocalAudioTrack', flag, message);
    if (!flag) {
      notify({
        title: t('unpublish_local_audio_track_failed'),
        description: `${message}`,
      });
      return;
    }
    isLocalAudioTrackPublished = false;
    notify({ title: t('unpublish_local_audio_track_success') });
  } catch (error) {
    console.error(`[unpublishLocalAudioTrack] ${error.name}: ${error.message}`);
    notify({
      title: t('unpublish_local_audio_track_failed'),
      description: `${error.name} ${error.message}`,
    });
  }
};

const muteLocalAudioTrack = async () => {
  try {
    if (!trroClient?.client) {
      console.warn(`[muteLocalAudioTrack] TRROSDK instance not initialized.`);
      notify({
        title: t('mute_local_audio_track_failed'),
        description: t('error_description_not_logged_in'),
      });
      return;
    }
    if (!isLocalAudioTrackPublished) {
      console.warn('[muteLocalAudioTrack] Not published');
      notify({
        title: t('mute_local_audio_track_failed'),
        description: t('error_description_no_published_audio_stream'),
      });
      return;
    }
    const { flag, message } = await trroClient.muteLocalAudioTrack();
    if (!flag) {
      notify({
        title: t('mute_local_audio_track_failed'),
        description: `${message}`,
      });
      return;
    }
    notify({ title: t('mute_local_audio_track_success') });
  } catch (error) {
    console.error(`[muteLocalAudioTrack] ${error.name}: ${error.message}`);
    notify({
      title: t('mute_local_audio_track_failed'),
      description: `${error.name} ${error.message}`,
    });
  }
};

const unmuteLocalAudioTrack = async () => {
  try {
    if (!trroClient?.client) {
      console.warn(`[unmuteLocalAudioTrack] TRROSDK instance not initialized.`);
      notify({
        title: t('unmute_local_audio_track_failed'),
        description: t('error_description_not_logged_in'),
      });
      return;
    }
    if (!isLocalAudioTrackPublished) {
      console.warn('[unmuteLocalAudioTrack] Not published');
      notify({
        title: t('mute_local_audio_track_failed'),
        description: t('error_description_no_published_audio_stream'),
      });
      return;
    }
    const { flag, message } = await trroClient.unmuteLocalAudioTrack();
    if (!flag) {
      notify({
        title: t('unmute_local_audio_track_failed'),
        description: `${message}`,
      });
      return;
    }
    notify({ title: t('unmute_local_audio_track_success') });
  } catch (error) {
    console.error(`[unmuteLocalAudioTrack] ${error.name}: ${error.message}`);
    notify({
      title: t('unmute_local_audio_track_failed'),
      description: `${error.name} ${error.message}`,
    });
  }
};

const getDiagnosisReport = async (fieldDeviceId = diagnosingFieldDeviceId) => {
  try {
    updateDiagnosisReport('LOADING');
    const result = await trroClient.requestDiagnosisReport(fieldDeviceId);
    if (result.flag) {
      notify({ title: t('get_diagnosis_report_success') });
      updateDiagnosisReport('SUCCESS', result.report);
    } else {
      notify({
        title: t('get_diagnosis_report_failed'),
        description: result.message,
      });
      if (result.message.toLowerCase().includes('requesting') || result.message.toLowerCase().includes('pending')) {
        updateDiagnosisReport('LOADING');
      } else {
        updateDiagnosisReport('FAILED');
      }
    }
  } catch (error) {
    console.error(`[getDiagnosisReport] ${error.name}: ${error.message}`);
    notify({
      title: t('get_diagnosis_report_failed'),
      description: error.message,
    });
    updateDiagnosisReport('FAILED');
  }
};

// UI Helpers

/**
 * @param {object} options
 * @param {string} options.title
 * @param {string} options.description
 */
const notify = ({ title, description }) => {
  const notificationElem = $('#notification');
  if (!notificationElem) {
    console.error('notificationElem is not defined');
    return;
  }
  if (notificationElem.hasClass('hidden')) {
    notificationElem.removeClass('hidden');
  }
  notificationElem.html(
    `
      <span class="text-[14px] text-gray-700 font-bold">${title}</span>
      <span class="text-[14px]">${!!description ? description : ''}</span>
    `,
  );
};

/**
 * @param {object} options
 * @param {'SUCCESS'|'LOADING'|'FAILED'|'CLOSE'|'KICKED'} options.status
 * @param {string} [options.message]
 */
const updateLoginStatus = ({ status, message }) => {
  const loginStatusElem = $('#login-status');
  if (!loginStatusElem) {
    console.error('loginStatusElem is not defined');
    return;
  }
  if (status === 'SUCCESS') {
    loginStatusElem.html(`
      <span class="text-green-500">${t('login_status_success')}</span>
    `);
  } else if (status === 'LOADING') {
    loginStatusElem.html(`
      <span class="text-blue-500">${t('login_status_loading')}</span>
    `);
  } else if (status === 'FAILED') {
    loginStatusElem.html(`
      <span class="text-red-500">${t('login_status_failed')}</span>
      ${message ? `<span>${message}</span>` : ''}
    `);
  } else if (status === 'CLOSE') {
    loginStatusElem.html(`
      <span class="text-red-500">${t('login_status_closed')}</span>
    `);
  } else if (status === 'KICKED') {
    loginStatusElem.html(`
      <span class="text-red-500">${t('login_status_kicked')}</span>
    `);
  }
};

/**
 * @param {Array} gatewayList
 */
const updateGatewayTable = (gatewayList) => {
  const gatewayTableElem = $('#gateway-table');
  if (!gatewayTableElem) {
    console.error('gatewayTableElem is not defined');
    return;
  }
  let tableBodyElem = gatewayTableElem.find('tbody');
  if (!tableBodyElem) {
    tableBodyElem = $('<tbody></tbody>');
    gatewayTableElem.append(tableBodyElem);
  }
  if (tableBodyElem.children().length > 0) {
    tableBodyElem.empty();
  }
  for (const { fieldDeviceId, deviceName, deviceType, version, status, streamCount } of gatewayList) {
    tableBodyElem.append(`
      <tr>
        <td class="px-[5px] py-[2px]">
          <input 
            id="gateway-table-check-${fieldDeviceId}" 
            type="checkbox" 
            onclick="handleGatewayCheckboxChange('${fieldDeviceId}', this.checked)" 
          />
        </td>
        <td class="px-[5px] py-[2px]">${fieldDeviceId}</td>
        <td class="px-[5px] py-[2px]">${deviceName}</td>
        <td class="px-[5px] py-[2px]">${deviceType}</td>
        <td class="px-[5px] py-[2px]">${version}</td>
        <td class="px-[5px] py-[2px]">${status}</td>
        <td class="px-[5px] py-[2px]">${streamCount}</td>
      </tr>
    `);
  }
  if (gatewayList.length) {
    $('#gateway-table-check-all').attr('disabled', false);
  } else {
    $('#gateway-table-check-all').attr('disabled', true);
  }
};

/**
 * @param {object} options
 * @param {{ fieldDeviceId: string, state: 'CONNECTED'|'DISCONNECTED' }[]} options.data
 */
const updateFieldDeviceConnectionState = ({ data }) => {
  const stateElem = $('#field-device-connection-state-text');
  if (!stateElem) {
    console.error('stateElem is not defined');
    return;
  }
  stateElem.empty();
  if (data.length === 0) {
    stateElem.text('-');
    return;
  }
  for (const { fieldDeviceId, state } of data) {
    stateElem.append(`
      <p class="flex gap-[5px]">
        <span class="text-[14px] text-gray-900 font-bold">${fieldDeviceId}</span>
        <span class="text-[14px] ${state === 'CONNECTED' ? 'text-green-500' : 'text-red-500'}">
          ${state === 'CONNECTED' ? t('connected') : t('not_connected')}
        </span>
      </p>
    `);
  }
};

/**
 * @param {object} options
 * @param {{ fieldDeviceId: string, permission: 'GUEST'|'MASTER' }[]} options.data
 */
const updateFieldDevicePermissionState = ({ data }) => {
  const stateElem = $('#current-permission-text');
  if (!stateElem) {
    console.error('stateElem is not defined');
    return;
  }
  stateElem.empty();
  if (data?.length === 0) {
    stateElem.text('-');
    return;
  }
  for (const { fieldDeviceId, permission } of data) {
    stateElem.append(`
      <p class="flex gap-[5px]">
        <span class="text-[14px] text-gray-900 font-bold">${fieldDeviceId}</span>
        <span class="text-[14px] ${permission === 'GUEST' ? 'text-gray-900' : 'text-blue-500'}">
          ${permission === 'GUEST' ? 'Guest' : 'Master'}
        </span>
      </p>
    `);
  }
};

/**
 * Auto-apply video buffer configuration to newly subscribed streams
 * @param {string} fieldDeviceId
 * @param {number} streamId
 */
const autoApplyBufferConfig = (fieldDeviceId, streamId) => {
  // If no target value is set, skip
  if (videoBufferConfig.target === null) {
    return;
  }

  setTimeout(() => {
    const { method, target } = videoBufferConfig;
    let result;
    if (method === 'jitterBufferTarget') {
      result = trroClient?.client?.setJitterBufferTarget?.(fieldDeviceId, {
        streamId,
        jitterBufferTarget: target,
      });
    } else {
      result = trroClient?.client?.setPlayoutDelayHint?.(fieldDeviceId, {
        streamId,
        playoutDelayHint: target,
      });
    }

    console.log(`[autoApplyBufferConfig] Applied ${method}=${target}ms to stream ${streamId}, result:`, result);
  }, 500); // Wait for stream establishment before applying
};

/**
 * @param {string} fieldDeviceId
 * @param {'string'|'ArrayBuffer'} messageType
 * @param {string|ArrayBuffer} [message]
 * @param {'sctp'|'mqtt'} [channelType='sctp']
 */
const updateFieldDeviceMessage = (fieldDeviceId, messageType, message, channelType = 'sctp') => {
  console.log('[updateFieldDeviceMessage]', fieldDeviceId, messageType, message, channelType);
  const currentTime = new Date();
  const safeId = sanitizeIdForDom(fieldDeviceId);
  const wrapperElem = $(`#field-messages-wrapper`);
  if (!wrapperElem) {
    console.error('[updateFieldDeviceMessage] wrapperElem is not defined');
    return;
  }
  if (wrapperElem.hasClass('hidden')) {
    wrapperElem.removeClass('hidden');
  }
  let deviceWrapperElem = wrapperElem.find(`#field-message-${safeId}-wrapper`);
  if (!deviceWrapperElem.length) {
    deviceWrapperElem = $(`
      <div 
        class="w-full box-border p-[15px] border rounded bg-white flex flex-col gap-[15px]" 
        id="field-message-${safeId}-wrapper"
      >
        <h4 class="font-bold">${fieldDeviceId}</h4>
        <div class="flex flex-col gap-[10px]" id="field-message-${safeId}"></div>
      </div>
    `);
    wrapperElem.append(deviceWrapperElem);
  }
  let messageElem = deviceWrapperElem.find(`#field-message-${safeId}`);
  if (!messageElem.length) {
    messageElem = $(`
      <div class="flex flex-col gap-[10px]" id="field-message-${safeId}"></div>
    `);
    deviceWrapperElem.append(messageElem);
  }
  messageElem.empty();
  // const channelLabel = channelType === 'mqtt' ? '<span class="text-blue-500">[MQTT]</span>' : '[SCTP]';
  if (messageType === 'string' && typeof message === 'string') {
    messageElem.append(`
      <span class="text-[14px] text-gray-900">${currentTime.toISOString()} string</span>
      <span class="text-[14px] text-gray-900">${message}</span>
    `);
  } else if (messageType === 'ArrayBuffer' && message instanceof ArrayBuffer) {
    const messageString = new TextDecoder().decode(message);
    messageElem.append(`
      <span class="text-[14px] text-gray-900">
        ${currentTime.toISOString()} [ArrayBuffer ${message.byteLength}]
      </span>
      <span class="text-[14px] text-gray-900">${messageString}</span>
    `);
  } else {
    console.warn(`[updateFieldDeviceMessage] invalid messageType or message`);
  }
};

/**
 * Update media wrapper for field device
 * @param {string} fieldDeviceId
 * @param {{ mediaType: 'VIDEO'|'AUDIO'; streamId: number; }[]} streams
 */
const updateMediaWrapper = (fieldDeviceId, streams) => {
  const wrapperElem = $(`#field-media-wrapper`);
  if (!wrapperElem) {
    console.error('[updateMediaWrapper] wrapperElem is not defined');
    return;
  }
  if (wrapperElem.hasClass('hidden')) {
    wrapperElem.removeClass('hidden');
  }

  const safeId = sanitizeIdForDom(fieldDeviceId);

  let deviceWrapperElem = wrapperElem.find(`#field-media-${safeId}`);
  if (!deviceWrapperElem.length) {
    deviceWrapperElem = $(`
      <div 
        class="w-full box-border p-[15px] border rounded bg-white flex flex-col gap-[15px]" 
        id="field-media-${safeId}"
      >
        <h4 class="font-bold">${fieldDeviceId}</h4>
      </div>
    `);
    wrapperElem.append(deviceWrapperElem);
  }
  let deviceVideoWrapperElem = deviceWrapperElem.find(`#field-videos-${safeId}`);
  let deviceAudioWrapperElem = deviceWrapperElem.find(`#field-audios-${safeId}`);
  if (!deviceVideoWrapperElem.length) {
    deviceVideoWrapperElem = $(`
      <div class="grid grid-cols-[repeat(auto-fill,_minmax(500px,_1fr))] gap-[10px]" id="field-videos-${safeId}">
      </div>
    `);
    if (deviceAudioWrapperElem.length) {
      deviceAudioWrapperElem.before(deviceVideoWrapperElem);
    } else {
      deviceWrapperElem.append(deviceVideoWrapperElem);
    }
  }
  if (!deviceAudioWrapperElem.length) {
    deviceAudioWrapperElem = $(`
      <div class="grid grid-cols-[repeat(auto-fill,_minmax(500px,_1fr))] gap-[10px]" id="field-audios-${safeId}">
      </div>
    `);
    deviceWrapperElem.append(deviceAudioWrapperElem);
  }
  const oldVideoStreamIds = [];
  for (const elem of deviceVideoWrapperElem.children()) {
    const match = elem.id.match(/^field-video-(.+)-(\d+)$/);
    if (match && match[1] === safeId) {
      oldVideoStreamIds.push(Number(match[2]));
    }
  }
  const newVideoStreamIds = streams.filter((stream) => stream.mediaType === 'VIDEO').map((stream) => stream.streamId);
  if (oldVideoStreamIds.join(',') !== newVideoStreamIds.join(',')) {
    const newElems = [];
    for (const streamId of newVideoStreamIds) {
      let elem = deviceVideoWrapperElem.find(`#field-video-${safeId}-${streamId}`);
      if (elem.length) {
        newElems.push(elem[0].parentNode.removeChild(elem[0]));
      } else {
        const newElem = $(`
          <div id="field-video-${safeId}-${streamId}">
            <p class="text-[14px]" id="field-video-${safeId}-${streamId}-info">
              ${t('remote_media_video_stream_title', { streamId })}
            </p>
          </div>
        `);
        newElems.push(newElem);
      }
    }
    if (deviceVideoWrapperElem.children().length) {
      deviceVideoWrapperElem.empty();
    }
    deviceVideoWrapperElem.append(...newElems);
  }
  const oldAudioElem = deviceAudioWrapperElem.find(`#field-audio-${safeId}`);
  const needAudio = streams.some((stream) => stream.mediaType === 'AUDIO');
  if (!oldAudioElem.length && needAudio) {
    const newElem = $(`
      <div id="field-audio-${safeId}">
      </div>
    `);
    deviceAudioWrapperElem.append(newElem);
  } else if (oldAudioElem.length && !needAudio) {
    deviceAudioWrapperElem.empty();
  }

  // Per-stream stats card container for this field device.
  // 位于 video 容器上方；折叠形态与 credentials-generator / session-permission-token / log-collector
  // 等其它卡片**完全一致**：外层 <div>，标题 <h5> 内放 ▶/▼ 小三角，内容 <div id="...-content"> 默认 display:none。
  let deviceStatsWrapperElem = deviceWrapperElem.find(`#field-stats-${safeId}`);
  if (!deviceStatsWrapperElem.length) {
    deviceStatsWrapperElem = $(`
      <div class="w-full box-border p-[15px] border rounded bg-gray-50 flex flex-col gap-[10px]" id="field-stats-${safeId}">
        <h5
          class="font-bold text-sm cursor-pointer hover:text-blue-600 flex items-center gap-[5px]"
          id="field-stats-header-${safeId}"
        >
          <span id="field-stats-arrow-${safeId}">▶</span>
          <span>${t('stream_stats_card_title')}</span>
        </h5>
        <div id="field-stats-content-${safeId}" style="display: none;" class="flex flex-col gap-[8px]">
          <!-- hidden: method selector and apply-all controls (kept for future re-enablement)
          <div class="flex items-center gap-[8px] flex-wrap text-xs">
            <label class="font-medium">${t('stream_stats_method_label')}</label>
            <select
              class="border rounded p-[3px] text-xs bg-white stream-stats-method-select"
              data-field-device-id="${fieldDeviceId}"
            >
              <option value="playoutDelayHint">playoutDelayHint (${t(
                'video_buffer_method_chrome_recommended',
              )})</option>
              <option value="jitterBufferTarget">jitterBufferTarget</option>
            </select>
            <span class="text-gray-500">${t('stream_stats_method_hint')}</span>
          </div>
          <div class="flex items-center gap-[8px] flex-wrap text-xs">
            <label class="font-medium">${t('stream_stats_apply_all_label')}</label>
            <input
              type="number"
              min="0"
              max="4000"
              placeholder="${t('stream_stats_input_placeholder')}"
              class="border rounded p-[3px] text-xs w-[120px] bg-white stream-stats-apply-all-input"
              data-field-device-id="${fieldDeviceId}"
            />
            <button
              class="bg-white p-[3px] border rounded text-blue-500 hover:text-blue-600 cursor-pointer text-xs stream-stats-apply-all-btn"
              data-field-device-id="${fieldDeviceId}"
            >${t('stream_stats_btn_apply_all')}</button>
            <span class="text-gray-500">${t('stream_stats_apply_all_hint')}</span>
          </div>
          -->
          <div id="field-stats-body-${safeId}" class="text-xs text-gray-600">${t('stream_stats_waiting')}</div>
        </div>
      </div>
    `);
    // 放在 video 容器之前（视频上方）
    if (deviceVideoWrapperElem.length) {
      deviceVideoWrapperElem.before(deviceStatsWrapperElem);
    } else {
      deviceWrapperElem.append(deviceStatsWrapperElem);
    }

    // 折叠切换：与 toggleCredentialsGenerator / toggleSessionPermissionToken 模式一致
    const headerElem = deviceStatsWrapperElem.find(`#field-stats-header-${safeId}`);
    const contentElem = deviceStatsWrapperElem.find(`#field-stats-content-${safeId}`);
    const arrowElem = deviceStatsWrapperElem.find(`#field-stats-arrow-${safeId}`);
    headerElem.on('click', () => {
      if (contentElem.is(':visible')) {
        contentElem.hide();
        arrowElem.text('▶');
      } else {
        contentElem.show();
        arrowElem.text('▼');
      }
    });

    // 同步默认 method 到下拉框，并监听切换（共享到 videoBufferConfig）
    deviceStatsWrapperElem.find('.stream-stats-method-select').val(videoBufferConfig.method);
    deviceStatsWrapperElem.find('.stream-stats-method-select').on('change', function () {
      videoBufferConfig.method = $(this).val();
    });

    // "应用到所有流"：对当前 fieldDevice 下所有已知视频流批量调用当前 method 的 API
    deviceStatsWrapperElem.find('.stream-stats-apply-all-btn').on('click', function () {
      const fid = $(this).attr('data-field-device-id');
      const inputElem = deviceStatsWrapperElem.find('.stream-stats-apply-all-input');
      const raw = inputElem.val();
      if (raw === '' || raw === null || raw === undefined) {
        notify({
          title: t('stream_stats_set_failed'),
          description: t('stream_stats_input_empty'),
        });
        return;
      }
      const target = parseInt(raw, 10);
      if (Number.isNaN(target)) {
        notify({
          title: t('stream_stats_set_failed'),
          description: t('stream_stats_input_empty'),
        });
        return;
      }

      // 从 streamStatsMap 里找到该 fieldDevice 下所有视频流的 streamId
      const groupKey = Object.keys(streamStatsMap).find((uid) => uid === fid || uid.endsWith(`/${fid}`));
      const group = groupKey ? streamStatsMap[groupKey] : null;
      const streamIds = group
        ? Object.values(group)
            .map((s) => s.streamId)
            .filter((id) => Number.isInteger(id))
        : [];

      if (!streamIds.length) {
        notify({
          title: t('stream_stats_set_failed'),
          description: t('stream_stats_apply_all_no_streams'),
        });
        return;
      }

      const { method } = videoBufferConfig;
      let successCount = 0;
      let lastMessage = '';
      streamIds.forEach((streamId) => {
        let res;
        if (method === 'jitterBufferTarget') {
          res = trroClient?.client?.setJitterBufferTarget?.(fid, {
            streamId,
            jitterBufferTarget: target,
          });
        } else {
          res = trroClient?.client?.setPlayoutDelayHint?.(fid, {
            streamId,
            playoutDelayHint: target,
          });
        }
        if (res?.code === 0) {
          successCount += 1;
        } else {
          lastMessage = res?.message || lastMessage;
        }
        // 同步每行输入框的草稿值，便于与表格展示一致
        const trackId = `track_video${streamId}`;
        streamStatsJbtDraft[trackId] = target;
      });

      // 每行草稿值同步了，触发一次重绘让 UI 立即反映输入框
      renderStreamStatsCard(fid);

      const allOk = successCount === streamIds.length;
      notify({
        title: allOk ? t('stream_stats_set_success') : t('stream_stats_set_failed'),
        description: `${fid} ${method} ${successCount}/${streamIds.length}${lastMessage ? `: ${lastMessage}` : ''}`,
      });
    });
  }
  renderStreamStatsCard(fieldDeviceId);

  // 视频渲染配置卡片（rotate / zoom / mirror / nightVision / renderBackend）
  // 折叠形态与 stats / credentials-generator 等卡片完全一致，默认折叠。
  let renderCfgWrapperElem = deviceWrapperElem.find(`#field-render-cfg-${safeId}`);
  if (!renderCfgWrapperElem.length) {
    renderCfgWrapperElem = $(`
      <div class="w-full box-border p-[15px] border rounded bg-gray-50 flex flex-col gap-[10px]" id="field-render-cfg-${safeId}">
        <h5
          class="font-bold text-sm cursor-pointer hover:text-blue-600 flex items-center gap-[5px]"
          id="field-render-cfg-header-${safeId}"
        >
          <span id="field-render-cfg-arrow-${safeId}">▶</span>
          <span>${t('render_cfg_card_title')}</span>
        </h5>
        <div id="field-render-cfg-content-${safeId}" style="display: none;" class="flex flex-col gap-[8px] text-xs">
          <!-- <div class="text-gray-500">${t('render_cfg_hint')}</div> -->
          <div class="flex items-center gap-[8px] flex-wrap">
            <label class="font-medium">${t('render_cfg_stream_id')}</label>
            <select class="border rounded p-[3px] text-xs bg-white render-cfg-stream-id"></select>
            <label class="font-medium flex items-center gap-[4px]">
              <input type="checkbox" class="render-cfg-night-vision" />${t('render_cfg_night_vision')}
            </label>
            <button
              class="bg-white p-[4px_10px] border rounded text-blue-500 hover:text-blue-600 cursor-pointer text-xs render-cfg-apply-btn"
              data-field-device-id="${fieldDeviceId}"
            >${t('render_cfg_btn_apply')}</button>
            <button
              class="bg-white p-[4px_10px] border rounded text-gray-500 hover:text-gray-700 cursor-pointer text-xs render-cfg-reset-btn"
              data-field-device-id="${fieldDeviceId}"
            >${t('render_cfg_btn_reset')}</button>
          </div>
        </div>
      </div>
    `);
    // 放在 stats 卡片之后、video 容器之前
    if (deviceStatsWrapperElem.length) {
      deviceStatsWrapperElem.after(renderCfgWrapperElem);
    } else if (deviceVideoWrapperElem.length) {
      deviceVideoWrapperElem.before(renderCfgWrapperElem);
    } else {
      deviceWrapperElem.append(renderCfgWrapperElem);
    }

    // 折叠切换
    const headerElem = renderCfgWrapperElem.find(`#field-render-cfg-header-${safeId}`);
    const contentElem = renderCfgWrapperElem.find(`#field-render-cfg-content-${safeId}`);
    const arrowElem = renderCfgWrapperElem.find(`#field-render-cfg-arrow-${safeId}`);
    headerElem.on('click', () => {
      if (contentElem.is(':visible')) {
        contentElem.hide();
        arrowElem.text('▶');
      } else {
        contentElem.show();
        arrowElem.text('▼');
      }
    });

    // Populate streamId dropdown from subscribed streams; refresh on card expand
    const populateRenderCfgStreamIds = () => {
      const sel = renderCfgWrapperElem.find('.render-cfg-stream-id');
      const currentVal = sel.val();
      const groupKey2 = Object.keys(streamStatsMap).find(
        (uid) => uid === fieldDeviceId || uid.endsWith(`/${fieldDeviceId}`),
      );
      const group2 = groupKey2 ? streamStatsMap[groupKey2] : null;
      const ids = group2
        ? [
            ...new Set(
              Object.values(group2)
                .map((s) => s.streamId)
                .filter((id) => Number.isInteger(id)),
            ),
          ].sort((a, b) => a - b)
        : [];
      sel.empty();
      ids.forEach((id) => {
        sel.append(`<option value="${id}">${id}</option>`);
      });
      // Restore previously selected value if still available
      if (currentVal && ids.includes(parseInt(currentVal, 10))) {
        sel.val(currentVal);
      }
    };
    populateRenderCfgStreamIds();
    // Refresh dropdown when card expands (populate regardless of visibility — safe since DOM is hidden)
    headerElem.on('click', () => {
      populateRenderCfgStreamIds();
    });

    // Apply render config (nightVision only; rotate/zoom/mirror/backend are hidden)
    renderCfgWrapperElem.find('.render-cfg-apply-btn').on('click', function () {
      const fid = $(this).attr('data-field-device-id');
      const sid = parseInt(renderCfgWrapperElem.find('.render-cfg-stream-id').val(), 10);
      if (Number.isNaN(sid)) {
        notify({ title: t('render_cfg_set_failed'), description: t('render_cfg_stream_id_empty') });
        return;
      }
      const nightVision = renderCfgWrapperElem.find('.render-cfg-night-vision').is(':checked');

      const res = trroClient?.client?.setVideoRenderConfig?.({
        fieldDeviceId: fid,
        renderConfigs: [
          {
            streamId: sid,
            mount: `#field-video-${sanitizeIdForDom(fid)}-${sid}`,
            rotate: 0,
            mirror: 'off',
            renderBackend: 'auto',
            nightVision,
          },
        ],
      });
      Promise.resolve(res)
        .then((r) => {
          notify({
            title: r?.code === 0 ? t('render_cfg_set_success') : t('render_cfg_set_failed'),
            description: `${fid} streamId ${sid} nightVision=${nightVision}: ${r?.message ?? ''}`,
          });
        })
        .catch((err) => {
          notify({ title: t('render_cfg_set_failed'), description: `${err?.name}: ${err?.message}` });
        });
    });

    // Reset render config (clear nightVision; rotate/zoom/mirror/backend defaults are included)
    renderCfgWrapperElem.find('.render-cfg-reset-btn').on('click', function () {
      const fid = $(this).attr('data-field-device-id');
      const sid = parseInt(renderCfgWrapperElem.find('.render-cfg-stream-id').val(), 10);
      if (Number.isNaN(sid)) {
        notify({ title: t('render_cfg_set_failed'), description: t('render_cfg_stream_id_empty') });
        return;
      }
      // Reset nightVision only (other controls are hidden)
      renderCfgWrapperElem.find('.render-cfg-night-vision').prop('checked', false);

      const res = trroClient?.client?.setVideoRenderConfig?.({
        fieldDeviceId: fid,
        renderConfigs: [
          {
            streamId: sid,
            mount: `#field-video-${sanitizeIdForDom(fid)}-${sid}`,
            rotate: 0,
            mirror: 'off',
            renderBackend: 'auto',
          },
        ],
      });
      Promise.resolve(res)
        .then((r) => {
          notify({
            title: r?.code === 0 ? t('render_cfg_set_success') : t('render_cfg_set_failed'),
            description: `${fid} streamId ${sid} reset: ${r?.message ?? ''}`,
          });
        })
        .catch((err) => {
          notify({ title: t('render_cfg_set_failed'), description: `${err?.name}: ${err?.message}` });
        });
    });
  }
};

/**
 * Render the per-stream stats card for a single field device.
 *
 * Columns: streamId / Remote RTT / Remote Lost / Field RTT / Field Lost / FPS / Current Jitter / Target Jitter / Set Target
 * Remote metrics come from webrtcStats; field metrics come from gatewayStats; FPS from webrtcStats.
 * @param {string} fieldDeviceId
 */
const renderStreamStatsCard = (fieldDeviceId) => {
  const safeId = sanitizeIdForDom(fieldDeviceId);
  const bodyElem = $(`#field-stats-body-${safeId}`);
  if (!bodyElem.length) return;

  // srcUserId may equal `${projectId}/${fieldDeviceId}` in public cloud mode
  const groupKey = Object.keys(streamStatsMap).find(
    (uid) => uid === fieldDeviceId || uid.endsWith(`/${fieldDeviceId}`),
  );
  const group = groupKey ? streamStatsMap[groupKey] : null;
  const rows = group
    ? Object.keys(group)
        .map((trackId) => ({ trackId, ...group[trackId] }))
        .sort((a, b) => (a.streamId ?? 0) - (b.streamId ?? 0))
    : [];

  if (!rows.length) {
    bodyElem.html(`<span>${t('stream_stats_waiting')}</span>`);
    return;
  }

  // gatewayStats lookup key (may use projectId-prefixed or bare fieldDeviceId)
  const gwGroupKey = Object.keys(gatewayStatsMap).find(
    (uid) => uid === fieldDeviceId || uid.endsWith(`/${fieldDeviceId}`),
  );
  const gwGroup = gwGroupKey ? gatewayStatsMap[gwGroupKey] : null;

  const fmt = (v) => (typeof v === 'number' && !Number.isNaN(v) ? v.toFixed(1) : '-');
  // lost from webrtcStats is in percentage (e.g. 0.5 = 0.5%)
  const fmtLostPct = (v) => (typeof v === 'number' && !Number.isNaN(v) ? `${v.toFixed(2)}%` : '-');
  // field lost shown as rate; raw value may be packet count or percentage depending on gateway version
  const fmtFieldLost = (v) => (typeof v === 'number' && !Number.isNaN(v) ? `${v.toFixed(2)}%` : '-');

  const gridCols = 'grid-cols-[60px_90px_90px_90px_90px_60px_110px_110px_1fr]';
  const tipHtml = `<span class="jbt-tooltip-wrap" style="position:relative;display:inline-flex;align-items:center;cursor:help;">
    <span class="jbt-tooltip-trigger" style="display:inline-flex;align-items:center;justify-content:center;border-radius:50%;border:1px solid #9ca3af;color:#6b7280;width:14px;height:14px;font-size:10px;line-height:1;">!</span>
    <span class="jbt-tooltip-popup" style="display:none;position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);background:#1f2937;color:#fff;font-size:11px;line-height:1.3;padding:4px 8px;border-radius:4px;white-space:nowrap;z-index:50;pointer-events:none;">${t(
      'stream_stats_jitter_buffer_target_tip',
    )}</span>
  </span>`;

  const headerRow = `
    <div class="grid ${gridCols} gap-[6px] font-semibold border-b pb-[4px]">
      <div>${t('stream_stats_col_stream_id')}</div>
      <div>${t('stream_stats_col_remote_rtt')}</div>
      <div>${t('stream_stats_col_remote_lost')}</div>
      <div>${t('stream_stats_col_field_rtt')}</div>
      <div>${t('stream_stats_col_field_lost')}</div>
      <div>${t('stream_stats_col_fps')}</div>
      <div>${t('stream_stats_col_jitter_buffer')}</div>
      <div>${t('stream_stats_col_jitter_buffer_target')}</div>
      <div>${t('stream_stats_col_set_target')}${tipHtml}</div>
    </div>
  `;

  const dataRows = rows
    .map((r) => {
      const gw = gwGroup ? gwGroup[r.streamId] : null;
      // Remote metrics from webrtcStats
      const remoteRtt = fmt(r.rtt);
      const remoteLost = fmtLostPct(r.lost);
      // Field metrics from gatewayStats
      const fieldRtt = gw ? fmt(gw.rtt) : '-';
      const fieldLost = gw ? fmtFieldLost(gw.lost) : '-';
      // FPS from webrtcStats
      const fps = r.fps ?? 0;

      const draftVal = streamStatsJbtDraft[r.trackId];
      const inputVal = draftVal !== undefined && draftVal !== null ? draftVal : '';

      return `
        <div class="grid ${gridCols} gap-[6px] items-center py-[3px]">
          <div>${r.streamId}</div>
          <div>${remoteRtt}</div>
          <div>${remoteLost}</div>
          <div>${fieldRtt}</div>
          <div>${fieldLost}</div>
          <div>${fps}</div>
          <div>${fmt(r.jitterBuffer)}</div>
          <div>${fmt(r.jitterBufferTarget)}</div>
          <div class="flex items-center gap-[6px]">
            <input
              type="number"
              min="0"
              max="4000"
              value="${inputVal}"
              placeholder="${t('stream_stats_input_placeholder')}"
              class="border rounded p-[3px] text-xs w-[100px] bg-white stream-stats-jbt-input"
              data-track-id="${r.trackId}"
            />
            <button
              class="bg-white p-[3px] border rounded text-blue-500 hover:text-blue-600 cursor-pointer text-xs stream-stats-jbt-btn"
              data-field-device-id="${fieldDeviceId}"
              data-track-id="${r.trackId}"
              data-stream-id="${r.streamId}"
            >${t('stream_stats_btn_apply')}</button>
          </div>
        </div>
      `;
    })
    .join('');

  bodyElem.html(headerRow + dataRows);

  // Bind tooltip hover — instant show/hide without native title delay
  bodyElem
    .find('.jbt-tooltip-wrap')
    .on('mouseenter', function () {
      $(this).find('.jbt-tooltip-popup').css('display', 'block');
    })
    .on('mouseleave', function () {
      $(this).find('.jbt-tooltip-popup').css('display', 'none');
    });

  // Bind input draft —— 允许清空字符串，表示"未设置"
  bodyElem.find('.stream-stats-jbt-input').on('input', function () {
    const trackId = $(this).attr('data-track-id');
    const raw = $(this).val();
    if (raw === '' || raw === null || raw === undefined) {
      delete streamStatsJbtDraft[trackId];
      return;
    }
    const val = parseInt(raw, 10);
    if (!Number.isNaN(val)) streamStatsJbtDraft[trackId] = val;
  });
  // Bind apply click — use the shared card-level method (playoutDelayHint | jitterBufferTarget)
  bodyElem.find('.stream-stats-jbt-btn').on('click', function () {
    const fid = $(this).attr('data-field-device-id');
    const trackId = $(this).attr('data-track-id');
    const streamId = parseInt($(this).attr('data-stream-id'), 10);
    const target = streamStatsJbtDraft[trackId];
    if (target === undefined || target === null) {
      notify({
        title: t('stream_stats_set_failed'),
        description: t('stream_stats_input_empty'),
      });
      return;
    }
    const { method } = videoBufferConfig;
    let res;
    if (method === 'jitterBufferTarget') {
      res = trroClient?.client?.setJitterBufferTarget?.(fid, {
        streamId,
        jitterBufferTarget: target,
      });
    } else {
      res = trroClient?.client?.setPlayoutDelayHint?.(fid, {
        streamId,
        playoutDelayHint: target,
      });
    }
    notify({
      title: res?.code === 0 ? t('stream_stats_set_success') : t('stream_stats_set_failed'),
      description: `${fid} ${method} streamId ${streamId}: ${res?.message ?? ''}`,
    });
  });
};

/**
 * Render stats card for every connected field device.
 */
const renderAllStreamStatsCards = () => {
  const ids = trroClient?.connectedDeviceIds ?? [];
  ids.forEach((id) => renderStreamStatsCard(id));
};

/**
 * @param {'SUCCESS'|'FAILED'|'LOADING'} status
 * @param {any} [report]
 */
const updateDiagnosisReport = (status, report) => {
  const currentTime = new Date();
  if (status === 'LOADING') {
    $('#diagnosing-field-device-select').attr('disabled', true);
    $('#get-diagnosis-report').attr('disabled', true);
    $('#get-diagnosis-report').removeClass(['text-blue-500', 'hover:text-blue-600', 'cursor-pointer']);
    $('#get-diagnosis-report').addClass(['text-gray-500', 'cursor-not-allowed']);
    $('#get-diagnosis-report').text(t('button_text_get_diagnosis_report_loading'));
    $('#download-diagnosis-report').attr('disabled', true);
    $('#download-diagnosis-report').removeClass(['text-blue-500', 'hover:text-blue-600', 'cursor-pointer']);
    $('#download-diagnosis-report').addClass(['text-gray-500', 'cursor-not-allowed']);
    $('#diagnosis-report-wrapper').empty();
    $('#diagnosis-report-wrapper').addClass('hidden');
  } else if (status === 'FAILED') {
    $('#diagnosing-field-device-select').attr('disabled', false);
    $('#get-diagnosis-report').attr('disabled', false);
    $('#get-diagnosis-report').removeClass(['text-gray-500', 'cursor-not-allowed']);
    $('#get-diagnosis-report').addClass(['text-blue-500', 'hover:text-blue-600', 'cursor-pointer']);
    $('#get-diagnosis-report').text(t('button_text_get_diagnosis_report'));
    $('#download-diagnosis-report').attr('disabled', true);
    $('#download-diagnosis-report').removeClass(['text-blue-500', 'hover:text-blue-600', 'cursor-pointer']);
    $('#download-diagnosis-report').addClass(['text-gray-500', 'cursor-not-allowed']);
    $('#diagnosis-report-wrapper').empty();
    $('#diagnosis-report-wrapper').addClass('hidden');
  } else if (report) {
    $('#diagnosing-field-device-select').attr('disabled', false);
    $('#get-diagnosis-report').attr('disabled', false);
    $('#get-diagnosis-report').removeClass(['text-gray-500', 'cursor-not-allowed']);
    $('#get-diagnosis-report').addClass(['text-blue-500', 'hover:text-blue-600', 'cursor-pointer']);
    $('#get-diagnosis-report').text(t('button_text_get_diagnosis_report'));
    $('#download-diagnosis-report').attr('disabled', false);
    $('#download-diagnosis-report').removeClass(['text-gray-500', 'cursor-not-allowed']);
    $('#download-diagnosis-report').addClass(['text-blue-500', 'hover:text-blue-600', 'cursor-pointer']);
    $('#download-diagnosis-report').off('click');
    $('#download-diagnosis-report').on('click', () => {
      const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `DiagnosisReport-${
        report?.device_id?.replaceAll('/', '-') ?? 'Unknown'
      }-${currentTime.toISOString()}.json`;
      a.click();
      URL.revokeObjectURL(url);
      a.remove();
    });
    if ($('#diagnosis-report-wrapper').hasClass('hidden')) {
      $('#diagnosis-report-wrapper').removeClass('hidden');
    }
    $('#diagnosis-report-wrapper').empty();
    $('#diagnosis-report-wrapper').append(`
      <div class="w-full box-border p-[15px] border rounded bg-white flex flex-col gap-[15px]">
        <h4 class="font-bold">${t('diagnosis_report_card_title_overview')}</h4>
        <div class="w-fit m-[-15px] border-spacing-[15px] table">
          <div class="table-row">
            <div class="table-cell">${t('diagnosis_report_diag_error')}</div>
            <div class="table-cell">${report?.diag_error === 'ok' ? '-' : `${report?.diag_error ?? ''}`}</div>
          </div>
          <div class="table-row">
            <div class="table-cell">${t('diagnosis_report_field_pid')}</div>
            <div class="table-cell">${!!report?.field_pid ? report.field_pid : '-'}</div>
          </div>
          <div class="table-row">
            <div class="table-cell">${t('diagnosis_report_license_check')}</div>
            <div class="table-cell">
              ${report?.license_check === 'ok' ? '✅' : `❌${'  '}${report?.license_check ?? ''}`}
            </div>
          </div>
          <div class="table-row">
            <div class="table-cell">${t('diagnosis_report_field_mqtt_connect')}</div>
            <div class="table-cell">
              ${report?.field_mqtt_connect === 'ok' ? '✅' : `❌${'  '}${report?.field_mqtt_connect ?? ''}`}
            </div>
          </div>
          <div class="table-row">
            <div class="table-cell">${t('diagnosis_report_remote_mqtt_connect')}</div>
            <div class="table-cell">
              ${report?.remote_mqtt_connect === 'ok' ? '✅' : `❌${'  '}${report?.remote_mqtt_connect ?? ''}`}
            </div>
          </div>
        </div>
      </div>
    `);
    for (const item of report?.result ?? []) {
      $('#diagnosis-report-wrapper').append(`
        <div class="w-full box-border p-[15px] border rounded bg-white flex flex-col gap-[15px]">
          <h4 class="font-bold">${t('diagnosis_report_card_title_stream', { streamId: item?.stream_id ?? '-' })}</h4>
          <div class="w-full box-border p-[15px] border rounded bg-white flex flex-col gap-[15px]">
            <h4 class="font-bold">${t('field_device')}</h4>
            <div class="w-fit m-[-15px] border-spacing-[15px] table">
              <div class="table-row">
                <div class="table-cell">${t('diagnosis_report_service_mode')}</div>
                <div class="table-cell">${item?.field?.field_mode ?? '-'}</div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('diagnosis_report_signal_connect')}</div>
                <div class="table-cell">
                  ${item?.field?.signal_connect === 'ok' ? '✅' : `❌${'  '}${item?.field?.signal_connect ?? ''}`}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('diagnosis_report_media_connect')}</div>
                <div class="table-cell">
                  ${item?.field?.media_connect === 'ok' ? '✅' : `❌${'  '}${item?.field?.media_connect ?? ''}`}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('diagnosis_report_video_transfer')}</div>
                <div class="table-cell">
                  ${item?.field?.video_transfer === 'ok' ? '✅' : `❌${'  '}${item?.field?.video_transfer ?? ''}`}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('diagnosis_report_video_capture')}</div>
                <div class="table-cell">
                  ${item?.field?.video_capture === 'ok' ? '✅' : `❌${'  '}${item?.field?.video_capture ?? ''}`}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('diagnosis_report_video_encode')}</div>
                <div class="table-cell">
                  ${item?.field?.video_encode === 'ok' ? '✅' : `❌${'  '}${item?.field?.video_encode ?? ''}`}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">RTT</div>
                <div class="table-cell">
                  ${item?.field?.rtt?.map((rtt) => `${rtt}ms`)?.join(', ') ?? '-'}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('lost_rate')}</div>
                <div class="table-cell">
                  ${item?.field?.lost?.map((lost) => `${lost}%`)?.join(', ') ?? '-'}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('bitrate')}</div>
                <div class="table-cell">
                  ${item?.field?.bps?.map((bps) => `${bps}kbps`)?.join(', ') ?? '-'}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('frame_rate')}</div>
                <div class="table-cell">
                  ${item?.field?.fps?.join(', ') ?? '-'}
                </div>
              </div>
            </div>
          </div>
          <div class="w-full box-border p-[15px] border rounded bg-white flex flex-col gap-[15px]">
            <h4 class="font-bold">${t('remote_device')}</h4>
            <div class="w-fit m-[-15px] border-spacing-[15px] table">
              <div class="table-row">
                <div class="table-cell">${t('diagnosis_report_service_mode')}</div>
                <div class="table-cell">${item?.remote?.remote_mode ?? '-'}</div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('diagnosis_report_signal_connect')}</div>
                <div class="table-cell">
                  ${item?.remote?.signal_connect === 'ok' ? '✅' : `❌${'  '}${item?.remote?.signal_connect ?? ''}`}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('diagnosis_report_media_connect')}</div>
                <div class="table-cell">
                  ${item?.remote?.media_connect === 'ok' ? '✅' : `❌${'  '}${item?.remote?.media_connect ?? ''}`}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('diagnosis_report_video_transfer')}</div>
                <div class="table-cell">
                  ${item?.remote?.video_transfer === 'ok' ? '✅' : `❌${'  '}${item?.remote?.video_transfer ?? ''}`}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('diagnosis_report_video_decode')}</div>
                <div class="table-cell">
                  ${item?.remote?.video_decode === 'ok' ? '✅' : `❌${'  '}${item?.remote?.video_decode ?? ''}`}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('lag_k100')}</div>
                <div class="table-cell">
                  ${item?.remote?.lag_k100 ?? '-'}%
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('lag_k150')}</div>
                <div class="table-cell">
                  ${item?.remote?.lag_k150 ?? '-'}%
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">RTT</div>
                <div class="table-cell">
                  ${item?.remote?.rtt?.map((rtt) => `${rtt}ms`)?.join(', ') ?? '-'}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('lost_rate')}</div>
                <div class="table-cell">
                  ${item?.remote?.lost?.map((lost) => `${lost}%`)?.join(', ') ?? '-'}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('bitrate')}</div>
                <div class="table-cell">
                  ${item?.remote?.bps?.map((bps) => `${bps}kbps`)?.join(', ') ?? '-'}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('frame_rate')}</div>
                <div class="table-cell">
                  ${item?.remote?.fps?.join(', ') ?? '-'}
                </div>
              </div>
              <div class="table-row">
                <div class="table-cell">${t('latency')}</div>
                <div class="table-cell">
                  ${item?.remote?.latency?.map((latency) => `${latency}ms`)?.join(', ') ?? '-'}
                </div>
              </div>
            </div>
          </div>
        </div>
      `);
    }
  } else {
    $('#diagnosing-field-device-select').attr('disabled', false);
    $('#get-diagnosis-report').attr('disabled', false);
    $('#get-diagnosis-report').removeClass(['text-gray-500', 'cursor-not-allowed']);
    $('#get-diagnosis-report').addClass(['text-blue-500', 'hover:text-blue-600', 'cursor-pointer']);
    $('#get-diagnosis-report').text(t('button_text_get_diagnosis_report'));
    $('#download-diagnosis-report').attr('disabled', true);
    $('#download-diagnosis-report').removeClass(['text-blue-500', 'hover:text-blue-600', 'cursor-pointer']);
    $('#download-diagnosis-report').addClass(['text-gray-500', 'cursor-not-allowed']);
    $('#diagnosis-report-wrapper').empty();
    $('#diagnosis-report-wrapper').addClass('hidden');
    console.warn('[updateDiagnosisReport] report is empty');
  }
};
