/* 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;
          }
        },
        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 (global) */
const videoBufferConfig = {
  method: 'playoutDelayHint', // Default method
  target: null, // null means use default value
};

/** Debounce timer for buffer configuration */
let bufferApplyTimer = null;

// 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,
      });
      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}`,
    });
    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>
    `);
  }
};

/**
 * Initialize video buffer control card (create once, apply globally)
 */
const initVideoBufferControlCard = () => {
  const wrapperElem = $(`#field-media-wrapper`);
  if (!wrapperElem.length) {
    console.error('[initVideoBufferControlCard] wrapperElem is not defined');
    return;
  }

  // Check if already created
  if ($('#video-buffer-control-card').length) {
    return;
  }

  const bufferControlCard = $(`
    <div 
      class="w-full box-border p-[15px] border rounded bg-blue-50 flex flex-col gap-[10px]" 
      id="video-buffer-control-card"
    >
      <h4 class="font-bold text-blue-900">${t('video_buffer_control_title')}</h4>
      <div class="flex items-center gap-[10px] flex-wrap">
        <label class="text-sm font-medium min-w-[120px]">${t('video_buffer_method_label')}</label>
        <select id="video-buffer-method" class="border rounded p-[5px] text-sm bg-white">
          <option value="jitterBufferTarget">jitterBufferTarget</option>
          <option value="playoutDelayHint" selected>playoutDelayHint (${t(
            'video_buffer_method_chrome_recommended',
          )})</option>
        </select>
      </div>
      <div class="flex items-center gap-[10px] flex-wrap">
        <label class="text-sm font-medium min-w-[120px]">${t('video_buffer_target_label')}</label>
        <input 
          type="number" 
          id="video-buffer-target" 
          class="border rounded p-[5px] text-sm w-[120px] bg-white" 
          placeholder="${t('video_buffer_target_placeholder')}"
          min="0"
          step="10"
        />
        <span class="text-xs text-gray-600" id="video-buffer-status">${t('video_buffer_status_waiting')}</span>
      </div>
      <div class="text-xs text-gray-600">
        ${t('video_buffer_hint')}
      </div>
    </div>
  `);

  wrapperElem.prepend(bufferControlCard);
  setupVideoBufferListeners();
};

/**
 * Set up video buffer control event listeners (real-time update)
 */
const setupVideoBufferListeners = () => {
  const methodSelect = $('#video-buffer-method');
  const targetInput = $('#video-buffer-target');

  const applyRealtime = () => {
    // Clear previous timer (debounce)
    if (bufferApplyTimer) {
      clearTimeout(bufferApplyTimer);
    }

    // Delay 300ms to apply configuration (prevent frequent input)
    bufferApplyTimer = setTimeout(() => {
      applyVideoBufferConfig();
    }, 300);
  };

  // Listen for dropdown selection change
  methodSelect.on('change', applyRealtime);

  // Listen for input field change
  targetInput.on('input', applyRealtime);
};

/**
 * Update video buffer status display
 * @param {string} status
 * @param {string} color
 */
const updateVideoBufferStatus = (status, color = 'gray') => {
  const statusElem = $('#video-buffer-status');
  if (statusElem.length) {
    statusElem.text(status).removeClass('text-gray-600 text-green-600 text-red-600 text-blue-600');
    statusElem.addClass(`text-${color}-600`);
  }
};

/**
 * Apply video buffer configuration to all video streams of all field devices
 */
const applyVideoBufferConfig = () => {
  const method = $('#video-buffer-method').val();
  const targetValue = $('#video-buffer-target').val();

  // Update global configuration
  videoBufferConfig.method = method;

  if (!targetValue || targetValue === '') {
    // Empty means restore to default value
    videoBufferConfig.target = null;
    updateVideoBufferStatus(t('video_buffer_status_default'), 'gray');

    // Apply null to all connected streams
    applyBufferToAllStreams(method, null);
    return;
  }

  const target = parseInt(targetValue, 10);
  if (isNaN(target) || target < 0) {
    updateVideoBufferStatus(t('video_buffer_status_invalid'), 'red');
    return;
  }

  videoBufferConfig.target = target;

  // Apply to all connected streams
  applyBufferToAllStreams(method, target);
};

/**
 * Apply buffer configuration to all video streams of all field devices
 * @param {string} method
 * @param {number | null} target
 */
const applyBufferToAllStreams = (method, target) => {
  if (!trroClient?.connectedDeviceIds?.length) {
    updateVideoBufferStatus(t('video_buffer_status_no_devices'), 'gray');
    return;
  }

  let totalStreams = 0;
  let successCount = 0;
  let failCount = 0;

  // Iterate through all connected devices
  trroClient.connectedDeviceIds.forEach((fieldDeviceId) => {
    // Get all subscribed streams for this device
    const deviceStreams = trroClient.subscribedStreamIds[fieldDeviceId];
    if (!deviceStreams?.length) {
      return;
    }

    // Filter out video streams
    const videoStreams = deviceStreams.filter((s) => s.mediaType === 'VIDEO');
    totalStreams += videoStreams.length;

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

      if (result?.code === 0) {
        successCount += 1;
      } else {
        failCount += 1;
      }
    });
  });

  if (totalStreams === 0) {
    updateVideoBufferStatus(t('video_buffer_status_no_streams'), 'gray');
  } else if (target === null) {
    if (successCount === totalStreams) {
      updateVideoBufferStatus(t('video_buffer_status_restored', { count: totalStreams }), 'green');
    } else {
      updateVideoBufferStatus(
        t('video_buffer_status_partial_restored', { success: successCount, total: totalStreams }),
        'blue',
      );
    }
  } else if (successCount === totalStreams) {
    updateVideoBufferStatus(t('video_buffer_status_applied', { count: totalStreams }), 'green');
  } else if (successCount > 0) {
    updateVideoBufferStatus(
      t('video_buffer_status_partial_success', { success: successCount, total: totalStreams }),
      'blue',
    );
  } else {
    updateVideoBufferStatus(t('video_buffer_status_failed'), 'red');
  }
};

/**
 * 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');
  }

  // Initialize global video buffer control card (only create once)
  initVideoBufferControlCard();

  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();
  }
};

/**
 * @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');
  }
};
