import { setTimeZonePure } from '@core/appState/appSetting';
import {
  setDeviceConnected,
  setFirmwareDownloadProgress,
  setFirmwareUpdateAvailable,
  setFirmwareUpdateInProgress,
  setFirmwareUpdateSize,
  setLastSoftwareCheck,
  setSoftwareUpdateAvailable,
  setIsDisplayOn,
  setTouchScreen,
} from '@core/appState/deviceState';
import { DeviceManufacturer, DiskKeys, OnOff, STOP_COMMAND, DeviceOrientation } from '@core/constants';
import { DeviceAPI, storeExport as store } from '@core/index';
import {
  bytesToHuman,
  createImageFromCanvas,
  mapRelativeValue,
  semverIsGreater,
  spoofPausedVideoPlayback,
} from '@core/Util';
import TimeZones from './BrightSignTimezones';
import { isEmpty } from '@core/Util';
import { checkActiveNetworkInterface } from '@core/Flows/NetworkChangeFlow';

class BrightSign extends DeviceAPI {
  private nodeServerURL = 'http://localhost:9090';
  private heartbeatPort = 5003;
  private _defaultTimeServer = 'https://time.brightsignnetwork.com';
  private serialNumber = '';
  backendReady = false;
  knownHosts = [
    'brightsignnetwork.com',
    'ws.bsn.cloud',
    'bsnm.s3.amazonaws.com',
    'sqs.us-east-1.amazonaws.com',
    'bsnm.s3.amazonaws.com',
  ];
  firmwareUpdateURLValidationPattern = /^(.+)\.bsfw$/;
  requiresVideoZIndex = true;
  softwareUpdateValidationPattern = /^(.+)autorun\.zip$/;
  supportsCECControl = true;
  supportsCustomResolution = true;
  supportsDisplayRotation = true;
  supportsLocalCache = true;
  supportsMonitoring = true;
  supportsProxy = true;
  supportsTCPConfig = true;
  supportsTimeServer = true;
  supportsVirtualKeyboard = true;
  supportsWifiConfig = true;
  supportsVideoWall = false;
  supportsStorageEncryption = true;

  constructor(Logger: any) {
    super(Logger);

    const handshakeSocket = new WebSocket('ws://localhost:9089');
    handshakeSocket.onopen = () => {};
    handshakeSocket.onclose = () => {};
    handshakeSocket.onerror = () => {
      this.backendReady = false;
      setTimeout(() => {
        this.Logger.error('[STARTUP] Backend not ready. Retrying...');
        this.restartApp();
      }, 750);
    };
    handshakeSocket.onmessage = ({ data }) => {
      if (data === 'ready') {
        this.Logger.info('[STARTUP] Backend ready.');
        this.backendReady = true;
        this.sendHeartbeatToNodeServer();
        handshakeSocket.close();
      }
    };
  }

  init = async () => {
    try {
      this.serialNumber = await this.getSerialNumber();
      this.osVersion = await this.getFirmwareVersion();
      this.supportsStorageEncryption = await this.isEncryptionSupported();
      await this.loadSettingsIntoLocalStorage();

      if ((await this.getWebSecurity()) !== '1') {
        await this.disableWebSecurity();
        this.reboot();
      }
    } catch (error) {
      this.Logger.error(error);
      // this may fail while running the migration assistant in the node-server.
      // if so, just restart the application until it succeeds.
      this.restartApp();
    }
  };

  /** Runs as a background task in ShimApp.tsx */

  postInit = async () => {
    const [hasTouchScreen, supportsVideoWall] = await Promise.all([
      this.hasTouchScreen(),
      this.checkHdmiOutputs(),
      this.getSystemValueForTimeZone(),
    ]);

    this.supportsVideoWall = supportsVideoWall;
    store.dispatch(setTouchScreen(hasTouchScreen));
  };

  private loadSettingsIntoLocalStorage = async () => {
    this.Logger.debug('Loading all settings into localStorage');
    let settings: any = await this.getAllSettings();
    if (typeof settings === 'string') {
      settings = JSON.parse(settings);
    }

    for (const key in settings as any) {
      if (settings.hasOwnProperty(key)) {
        const value = settings[key];
        if (
          (key === DiskKeys.AWSSettings || key === DiskKeys.ProvisionedDevicePayload) &&
          isEmpty(value) &&
          settings.activated
        ) {
          this.Logger.error(`[SHIM-5437] AWS SETTINGS/PROVISIONED DEVICE OBJECT IS EMPTY.`);
        }
        localStorage.setItem(key, value);
      }
    }
  };

  private sendHeartbeatToNodeServer = () => {
    const hearbeatSocket = new BSDatagramSocket();
    setInterval(() => {
      hearbeatSocket.SendTo('127.0.0.1', this.heartbeatPort, 'heartbeat');
    }, 30 * 1000);
  };

  private sendNodeServerRequest = async (endpoint: string, options?: RequestInit) => {
    if (!this.backendReady) {
      throw new Error("Backend isn't ready yet.");
    }

    const defaultHeaders = {
      'X-FWI-Device-Auth': this.serialNumber,
    };

    if (options?.headers) {
      options.headers = { ...options.headers, ...defaultHeaders };
    } else {
      options = {
        ...options,
        headers: defaultHeaders,
      };
    }

    const request = await fetch(`${this.nodeServerURL}${endpoint}`, options);

    return request;
  };

  private checkHdmiOutputs = async () => {
    try {
      const outputs = await this.getDisplayConfig();
      return !!(outputs && outputs.length > 1);
    } catch {
      return false;
    }
  };

  private isEncryptionSupported = async () => {
    const res = await this.sendNodeServerRequest('/storage/encryption/support');
    return (await res.json()) as boolean;
  };

  captureScreenshot = async (): Promise<Blob> => {
    return new Promise(async (resolve, reject) => {
      try {
        const displayStatus = this.getDisplayStatus();
        const { orientation } = store.getState().deviceSettings;
        let width = `${window.innerWidth}`;
        let height = `${window.innerHeight}`;
        const rot = orientation === DeviceOrientation.DEG_180 ? '180' : '0';

        if (displayStatus === OnOff.On) {
          if (orientation === DeviceOrientation.DEG_90 || orientation === DeviceOrientation.DEG_270) {
            width = `${window.innerHeight}`;
            height = `${window.innerWidth}`;
          }

          const params = new URLSearchParams({
            width,
            height,
            rot,
          });

          const response = await this.sendNodeServerRequest(`/screenshot?${params.toString()}`);
          const blob = await response.blob();
          resolve(blob);
        } else {
          const blackScreen = await createImageFromCanvas();
          resolve(blackScreen);
        }
      } catch (error) {
        reject(error);
      }
    });
  };

  setSetting = async (key: string, value: any): Promise<void> => {
    return new Promise(async (resolve, reject) => {
      // set it in localStorage
      localStorage.setItem(key, value);

      //set it in persistant BS memory.
      const response = await this.sendNodeServerRequest(`/setting/${key}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ value }),
      });

      const { success, msg } = await response.json();
      if (!success) {
        reject(msg);
      } else {
        resolve(msg);
      }
    });
  };

  getAllSettings = async () => {
    // NOTE: Brightsign returns keys in all lowercase, so in order to map these to the shape of the app state, we will need to add a helper function to transform keys.
    return new Promise(async (resolve, reject) => {
      try {
        const response = await this.sendNodeServerRequest(`/settings`);
        const json = await response.json();
        resolve(json.payload);
      } catch (error) {
        reject(error);
      }
    });
  };

  getScreenOrientation = () => {
    return this.getSetting(DiskKeys.Orientation) as unknown as DeviceOrientation;
  };

  setScreenOrientation = async (nextOrientation: DeviceOrientation) => {
    const { orientation } = store.getState().deviceSettings;
    if (orientation !== this.getScreenOrientation()) {
      window.DeviceAPI?.setSetting(DiskKeys.Orientation, nextOrientation);

      // nextOrientation is applied during htmlWidget setup in autorun.brs:42-57
      // delay reboot to allow other shadow settings to be saved.
      setTimeout(() => {
        this.reboot();
      }, 3000);
    }
  };

  checkForSoftwareUpdate = async (): Promise<boolean> => {
    const { lastSoftwareCheck } = store.getState().deviceState;

    this.Logger.info('[SOFTWARE] Checking for software update...');
    try {
      const updateURL = store.getState().appSettings.softwareUpdateURL;
      if (!updateURL) {
        this.Logger.error('[SOFTWARE] No software update URL found.');
        return false;
      }

      const response = await fetch(updateURL, {
        method: 'HEAD',
        headers: {
          'If-Modified-Since': lastSoftwareCheck,
        },
      });

      if (response.status === 200) {
        store.dispatch(setSoftwareUpdateAvailable(true));
        return true;
      } else if (response.status === 304) {
        store.dispatch(setSoftwareUpdateAvailable(false));
        return false;
      }
    } catch (error) {
      this.Logger.error(`[SOFTWARE] Failed to download software update: ${error}`);
      store.dispatch(setSoftwareUpdateAvailable(false));
      return false;
    }
    store.dispatch(setSoftwareUpdateAvailable(false));
    return false;
  };

  checkForFirmwareUpdate = async (): Promise<boolean> => {
    this.Logger.info('[FIRMWARE] Checking for firmware update...');
    const { firmwareUpdateURL } = store.getState().appSettings;
    const urlContainsFilename = firmwareUpdateURL?.match(/((\d{1,4}(\.)?){1,5})\.bsfw$/);
    if (urlContainsFilename) {
      const currentVersion = await window.DeviceAPI.getFirmwareVersion();
      const newVersion = urlContainsFilename[1];
      this.Logger.debug('[FIRMWARE] Comparing firmware versions: ', currentVersion, newVersion);
      const updateAvailable = semverIsGreater(currentVersion, newVersion);

      store.dispatch(setFirmwareUpdateAvailable(updateAvailable));
      return updateAvailable;
    }
    throw new Error('Unable to compare firmware versions from url');
  };

  deleteAllSettings = async (): Promise<void> => {
    return new Promise(async (resolve, reject) => {
      localStorage.clear();
      const response = await this.sendNodeServerRequest(`/settings`, {
        method: 'DELETE',
      });
      const { success } = await response.json();
      if (success) {
        resolve();
      } else {
        reject('failed to delete all settings');
      }
    });
  };

  getModel = async (): Promise<string> => {
    return new Promise(async (resolve, reject) => {
      const response = await this.sendNodeServerRequest(`/about`);
      const { success, msg, payload } = await response.json();
      if (success) {
        resolve(payload.model as string);
      } else {
        reject(msg);
      }
    });
  };

  getSerialNumber = async (): Promise<string> => {
    return new Promise(async (resolve, reject) => {
      // NOTE: don't use custom headers for this since the value we need is used as the custom header.
      const response = await fetch(`${this.nodeServerURL}/serial`);
      const { success, msg, payload } = await response.json();
      if (success) {
        resolve(payload.serialNumber as string);
      } else {
        reject(msg);
      }
    });
  };

  getFirmwareVersion = async (): Promise<string> => {
    return new Promise(async (resolve, reject) => {
      const response = await this.sendNodeServerRequest(`/about`);
      const { success, msg, payload } = await response.json();
      if (success) {
        resolve(payload.firmwareVersion as string);
      } else {
        reject(msg);
      }
    });
  };

  getManufacturer = () => {
    return DeviceManufacturer.BrightSign;
  };

  getMACAddress = async (): Promise<string> => {
    return new Promise(async (resolve, reject) => {
      const response = await this.sendNodeServerRequest(`/network`);
      const { success, msg, payload } = await response.json();
      if (success) {
        resolve(payload.macAddress as string);
      } else {
        reject(msg);
      }
    });
  };

  getIPAddress = async (): Promise<string> => {
    return new Promise(async (resolve, reject) => {
      const response = await this.sendNodeServerRequest(`/network`);
      const { success, msg, payload } = await response.json();
      if (success) {
        resolve(payload.IPAddress as string);
      } else {
        reject(msg);
      }
    });
  };

  updateSoftware = async () => {
    try {
      const { softwareUpdateURL } = store.getState().appSettings;
      const response = await this.sendNodeServerRequest(`/software?url=${softwareUpdateURL}`, {
        method: 'POST',
      });
      const { success, msg } = await response.json();
      if (success) {
        this.Logger.info(`[SOFTWARE] ${msg}`);
        store.dispatch(setLastSoftwareCheck(new Date().toUTCString()));
        this.reboot();
      } else {
        throw new Error(msg);
      }
    } catch (error) {
      this.Logger.error(`[SOFTWARE] Unable to update software. ${error}`);
    }
  };

  /**
   * establishes a web socket connection to the node server to track download progress,
   * downloads the new firmware file to the root of the sd card and triggers a reboot
   * BS will apply the new firmware file when found.
   */
  updateFirmware = async () => {
    // updating brightsign firmware.
    const { firmwareUpdateURL } = store.getState().appSettings;
    await spoofPausedVideoPlayback();
    store.dispatch(setFirmwareUpdateInProgress(true));

    try {
      const progressSocket = new WebSocket('ws://localhost:9001');
      progressSocket.onopen = this._handleSocketOpen;
      progressSocket.onclose = this._handleSocketClosed;
      progressSocket.onerror = this._handleSocketError;
      progressSocket.onmessage = ({ data }) => {
        this._handleSocketMessage(progressSocket, data);
      };

      // send GET request to the node server to make sure that the socket connection is ready.
      const socketReadyResponse = await this.sendNodeServerRequest(`/socket`);
      const { socketReady } = await socketReadyResponse.json();

      if (socketReady) {
        const response = await this.sendNodeServerRequest(`/firmware?url=${firmwareUpdateURL}`, {
          method: 'POST',
        });
        const { success, msg, payload } = await response.json();
        if (success) {
          this.Logger.info('Total Firmware Download size: ', bytesToHuman(payload));
          store.dispatch(setFirmwareUpdateSize(payload));
        }
      } else {
        // keep trying.
        console.log('socket not ready');
        this.updateFirmware();
        store.dispatch(setFirmwareUpdateInProgress(false));
      }
    } catch (error) {
      this.Logger.error(`Failed to download firmware update: ${error}`);
      store.dispatch(setFirmwareUpdateInProgress(false));
    }
  };

  reboot = () => {
    if (!store.getState().deviceState.firmwareUpdateInProgress) {
      this.postMessage(STOP_COMMAND);
      setTimeout(() => {
        this.sendNodeServerRequest(`/reboot`, { method: 'POST' })
          .then((response) => response.json())
          .then(({ success, msg }) => {
            if (!success) {
              this.Logger.error('Failed to reboot the device. ' + msg);
            }
          })
          .catch((err) => {
            this.Logger.error(err);
          });
      }, 3000);
    } else {
      this.Logger.warn('[FIRMWARE] Failed to reboot the device. Firmware update in progress.');
    }
  };

  private _handleSocketOpen = () => {
    this.Logger.debug('Firmware Update progress socket connected...');
  };

  private _handleSocketClosed = () => {
    this.Logger.debug('Firmware Update progress socket closed...');
    store.dispatch(setFirmwareUpdateInProgress(false));
  };

  private _handleSocketMessage = (socket: WebSocket, data: string) => {
    if (data === 'firmware download complete') {
      socket.close();
      setTimeout(() => {
        this.reboot();
      }, 1000);
    } else if (data === 'firmware download failed') {
      socket.close();
      throw new Error('Firmware Download Failed');
    } else {
      // this is percentage: string which represents the total progress of the download.
      store.dispatch(setFirmwareDownloadProgress(data));
    }
  };

  private _handleSocketError = (error: Event) => {
    this.Logger.error(`Firmware Update progress socket error: ${error}`);
    store.dispatch(setFirmwareUpdateInProgress(false));
  };

  private getSystemValueForTimeZone = async () => {
    try {
      const response = await this.sendNodeServerRequest('/timezone');
      const { timeZone } = store.getState().appSettings;
      const { success, msg, payload } = await response.json();

      if (success && timeZone && payload !== timeZone) {
        this.Logger.warn(`System time (${payload}) mismatch with app state (${timeZone}). Setting to ${payload} now.`);
        store.dispatch(setTimeZonePure(payload));
      } else if (!timeZone && payload) {
        this.setSetting(DiskKeys.TimeZone, payload);
      } else if (payload === timeZone) {
        this.Logger.debug(`System Timezone and App Timezone match: ${timeZone}`);
        return;
      } else {
        this.Logger.error(msg);
      }
    } catch (error) {
      this.Logger.error(error);
    }
  };

  getTimeZoneMap = () => TimeZones;

  getTimeServer = async () => {
    try {
      const res = await this.sendNodeServerRequest('/time-server');
      const { payload } = await res.json();
      return payload ? payload : this._defaultTimeServer;
    } catch {
      return this._defaultTimeServer;
    }
  };

  setTimeZone = async (zone: string) => {
    try {
      const response = await this.sendNodeServerRequest(`/timezone?timezone=${zone}`, {
        method: 'POST',
      });
      const { success, msg, payload } = await response.json();

      if (success) {
        this.Logger.debug('Timezone successfully set. Rebooting to apply changes.');
        this.reboot();
      } else {
        this.Logger.error(`Failed to set Timezone on device. ${msg}`);
      }
    } catch (error) {
      this.Logger.error(`Failed to set Timezone on device.`, error);
    }
  };

  turnDisplayOnOff = async (toggle: OnOff) => {
    store.dispatch(setIsDisplayOn(toggle === OnOff.On));
    const cecEnabled = this.getParsedSetting(DiskKeys.CECEnabled) ?? false;
    this.Logger.debug(`use CEC: ${cecEnabled}`);

    try {
      const response = await this.sendNodeServerRequest(`/display-power?toggle=${toggle}&cec=${cecEnabled}`, {
        method: 'POST',
      });
      const { success, msg, payload } = await response.json();
      this.Logger.info(`[DISPLAY] ${msg}`);
    } catch (error) {
      this.Logger.error('failed to toggle display power');
    }
  };

  checkInternet = async () => {
    const IPAddress = await this.getIPAddress();
    const { deviceConnected } = this.getState().deviceState;

    if (IPAddress === 'n/a' && deviceConnected) {
      store.dispatch(setDeviceConnected(false));
    } else {
      if (!deviceConnected) {
        store.dispatch(setDeviceConnected(true));
      }
    }
  };

  getDisplayStatus = (): OnOff => {
    const displayStatus = store.getState().deviceState.isDisplayOn ? OnOff.On : OnOff.Off;
    return displayStatus;
  };

  applyProxy = async () => {
    try {
      const response = await this.sendNodeServerRequest(`/proxy`, {
        method: 'POST',
      });
      const { success, msg, payload } = await response.json();

      if (success) {
        this.Logger.info('[NETWORK] Successfully saved proxy settings. Rebooting to apply.');
        this.reboot();
      } else {
        this.Logger.error(`[NETWORK] Failed to save proxy settings. ${msg}`);
      }
    } catch (error) {
      this.Logger.error(error);
    }
  };

  setTimeServer = async (timeServer: string) => {
    try {
      const response = await this.sendNodeServerRequest(`/time-server?timeServer=${timeServer}`, {
        method: 'POST',
      });
      const { success, msg } = await response.json();
      success ? this.Logger.info(msg) : this.Logger.error(msg);
      this.setSetting(DiskKeys.TimeServer, timeServer);
    } catch (error) {
      this.Logger.error(error);
    }
  };

  saveCloudLogsToDisk = async (logs: Log[]) => {
    console.log('[LOGS] saving logs to disk while offline');

    const blob = new Blob([JSON.stringify(logs)], {
      type: 'application/json',
    });

    const response = await this.sendNodeServerRequest(`/offline-logs`, {
      method: 'POST',
      body: blob,
    });

    const { success, msg, payload } = await response.json();
    if (success) {
      this.Logger.info(`[LOGS] ${msg.toString()}`);
      this.logs = [];
    } else {
      this.Logger.error(`[LOGS] ${msg.toString()}`);
    }
  };

  savePlaybackLogsToDisk = async (logs: PlayEvent[]) => {
    console.log('[LOGS] saving play logs to disk');
    const blob = new Blob([JSON.stringify(logs)], {
      type: 'application/json',
    });

    const response = await this.sendNodeServerRequest(`/offline-logs?playback=true`, {
      method: 'POST',
      body: blob,
    });

    const { success, msg } = await response.json();
    if (success) {
      this.Logger.info(`[LOGS] ${msg.toString()}`);
      // this.playbackLogs = [];
    } else {
      this.Logger.error(`[LOGS] ${msg.toString()}`);
    }
  };

  getAllLogsFromDisk = async (): Promise<Log[]> => {
    this.Logger.info('[LOGS] Uploading all logs from time offline to cloud.');
    try {
      const response = await this.sendNodeServerRequest(`/offline-logs`);
      const { success, msg, payload } = await response.json();
      if (success) {
        return payload as Log[];
      } else {
        this.Logger.error(msg);
      }
    } catch (error) {
      this.Logger.error(error);
      return [];
    }
    return [];
  };

  clearPlayerCache = async () => {
    this.Logger.debug('[CACHE] Attempting to delete cached files from disk...');
    try {
      const response = await this.sendNodeServerRequest('/files', {
        method: 'DELETE',
      });
      if (response.status === 200) {
        this.Logger.info('[CACHE] Successully cleared player cache');
        return true;
      }
      return false;
    } catch (error) {
      this.Logger.error(`[CACHE] Failed to clear player cache. ${error}`);
      return false;
    }
  };

  getActiveNetworkInterface = async (): Promise<string> => {
    let activeAdapter = 'Default Adapter';
    try {
      const interfaces = await this.sendNodeServerRequest('/network/interfaces');
      const { success, payload } = await interfaces.json();
      if (success && payload) {
        console.log(payload);
        if (payload.ethernet.hasLink && payload.ethernet.ipAddressList.length > 0) {
          activeAdapter = 'eth0';
        } else if (payload.wifi.hasLink && payload.wifi.ipAddressList.length > 0) {
          activeAdapter = 'wlan0';
        }
      }
    } catch {
      // Fail silently
    } finally {
      return activeAdapter;
    }
  };

  getNetworkInterfaces = async (): Promise<InterfacePresence> => {
    try {
      const interfaces = await this.sendNodeServerRequest('/network/interfaces');
      const { success, payload } = await interfaces.json();
      if (success) {
        return payload;
      }
    } catch (error) {
      this.Logger.error(error);
    }

    // Return default values and assume they're both there.
    return {
      wifi: {
        present: true,
      },
      ethernet: {
        present: true,
      },
    };
  };

  hasTouchScreen = async () => {
    try {
      const isMultiTouchPresent = await this.sendNodeServerRequest('/peripherals');
      const { success, msg, payload } = await isMultiTouchPresent.json();
      if (success) {
        return !!payload.isMultiTouchPresent || (!payload.keyboardIsAttached && payload.isMousePresent);
      } else {
        this.Logger.error(msg);
      }
    } catch (error) {
      this.Logger.error(error);
      return false;
    }
    return false;
  };

  scanNetworks = async (controller?: AbortController): Promise<{ ssid: string; signal: number }[]> => {
    this.Logger.info('[NETWORK] Scanning for networks...');
    const res = await this.sendNodeServerRequest(`/network/scan`, { signal: controller?.signal });
    const networks = await res.json();
    if (networks.success) {
      const payload: { essId: string; signal: number }[] = networks.payload;
      this.Logger.debug(`[NETWORK] found ${payload.length} networks.`);
      const maxSignalStrength = Math.max(...payload.map((n) => n.signal));
      const minSignalStrength = Math.min(...payload.map((n) => n.signal));

      const leanNetworks = payload.map((network) => ({
        ssid: network.essId,
        signal: mapRelativeValue(minSignalStrength, maxSignalStrength, network.signal, 1, 3),
      }));

      return leanNetworks;
    }
    this.Logger.error(`[NETWORK] Failed to scan for networks. ${networks.msg}`);
    return [];
  };

  connectToWifiNetwork = async (essId: string, passphrase: string): Promise<boolean> => {
    this.setSetting('wirelessKey', Buffer.from(`${essId};${passphrase}`).toString('base64'));
    const res = await this.sendNodeServerRequest(`/network/wifi`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        enable: true,
        essId,
        passphrase,
      }),
    });

    const { success, msg } = await res.json();
    this.Logger.info(`[NETWORK] ${msg}`);
    checkActiveNetworkInterface();
    return success;
  };

  disableWifi = async (): Promise<boolean> => {
    const res = await this.sendNodeServerRequest('/network/wifi', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        enable: false,
      }),
    });
    const data = await res.json();
    checkActiveNetworkInterface();
    return data.success;
  };

  getWifiStatus = async (): Promise<{
    ssid: string;
    signalStrength: number;
    connected: boolean;
  }> => {
    const res = await this.sendNodeServerRequest('/network/wifi');
    const data = await res.json();
    if (data.success) {
      return {
        connected: data.payload.connected,
        ssid: data.payload.essid,
        signalStrength: data.payload.signalStrength,
      };
    }

    return {
      connected: false,
      ssid: '',
      signalStrength: 0,
    };
  };

  getWebSecurity = async () => {
    // returns '1' if web security is disabled, '0' if it is enabled
    const res = await this.sendNodeServerRequest('/security');
    const { payload } = await res.json();
    this.Logger.debug(`[SECURITY] current disable-web-security setting: ${payload}`);
    return `${payload}` as '0' | '1';
  };

  disableWebSecurity = async () => {
    const res = await this.sendNodeServerRequest('/security?enable=false', {
      method: 'POST',
    });
    const { success, msg } = await res.json();
    this.Logger.info(`[SECURITY] ${msg}`);
    return success;
  };

  enableWebSecurity = async () => {
    const res = await this.sendNodeServerRequest('/security?enable=true', {
      method: 'POST',
    });
    const { success, msg } = await res.json();
    this.Logger.info(`[SECURITY] ${msg}`);
    return success;
  };

  getResolutions = async () => {
    this.Logger.info('[DISPLAY] Fetching available display resolutions');
    const response = await this.sendNodeServerRequest('/resolutions');
    const { payload } = await response.json();
    this.Logger.debug(`[DISPLAY] Active resolution: ${payload.active}`);
    this.Logger.debug(`[DISPLAY] Found ${payload.modes.length} resolutions`);
    this.Logger.debug(`[DISPLAY] Best resolution: ${payload.best}`);
    return payload;
  };

  setResolution = async (width: number, height: number, frequency: number) => {
    const res = await this.sendNodeServerRequest(`/resolution?width=${width}&height=${height}&frequency=${frequency}`, {
      method: 'POST',
    });
    const { payload } = await res.json();
    this.Logger.info(
      `[DISPLAY] Setting output resolution to ${width}x${height}x${frequency}p. Reboot required: ${payload}`
    );
    return payload;
  };

  getNetworkConfig = async () => {
    const res = await this.sendNodeServerRequest('/network/config');
    const { payload } = (await res.json()) as {
      success: boolean;
      msg: string;
      payload: NetworkInterfaceConfig;
    };

    const filteredDNS = payload.dnsServerList?.filter((dns) => dns.match(/(\d{1,3}\.){3}\d{1,3}/));
    return {
      ...payload,
      dnsServerList: filteredDNS,
    };
  };

  setNetworkConfig = async (config: Partial<NetworkInterfaceConfig>) => {
    this.Logger.info('[NETWORK] Setting custom network configuration...');
    try {
      const res = await this.sendNodeServerRequest('/network/config', {
        headers: { 'Content-Type': 'application/json' },
        method: 'POST',
        body: JSON.stringify({ config }),
      });

      const { success, msg, payload } = await res.json();

      console.log('[NETWORK] applied following config: ', payload);

      if (success) {
        this.Logger.info(`[NETWORK] Successfully set network configuration.`);
      } else {
        this.Logger.error(`[NETWORK] Failed to set network configuration. ${msg}`);
      }
    } catch (error) {
      this.Logger.error(`[NETWORK] Failed to set network configuration. ${error}`);
    }
  };

  resetNetworkConfig = async () => {
    this.Logger.info('[NETWORK] Resetting network configuration...');
    this.sendNodeServerRequest('/network/config', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ config: {} }),
    });
  };

  async getDisplayConfig(): Promise<VideoModeScreenConfig[] | undefined> {
    const res = await this.sendNodeServerRequest('/display/outputs');
    const { success, msg, payload } = await res.json();
    if (success) {
      return payload as VideoModeScreenConfig[];
    } else {
      this.Logger.debug(msg);
      return;
    }
  }

  async setDisplayConfig(config: VideoModeScreenConfig[]): Promise<boolean> {
    this.Logger.info('[DISPLAY] Setting display configuration...');
    const res = await this.sendNodeServerRequest('/display/outputs', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ config }),
    });
    const { success, msg, payload } = await res.json();
    if (success) {
      this.Logger.info(`[DISPLAY] ${msg}`);
      return payload;
    } else {
      this.Logger.error(msg);
      return false;
    }
  }

  async getStorageEncryptionStatus(): Promise<{
    encrypted: boolean;
    prerequisitesMet: boolean;
  }> {
    this.Logger.info('[STORAGE] Checking storage encryption status...');
    try {
      const res = await this.sendNodeServerRequest('/storage/encryption');
      const { success, msg, payload } = await res.json();
      if (success) {
        return {
          encrypted: payload.fs.includes('+ecryptfs'),
          prerequisitesMet: payload.prerequisitesMet,
        };
      }
    } catch {
      this.Logger.error('[STORAGE] Failed to check storage encryption status.');
    }

    return {
      encrypted: false,
      prerequisitesMet: false,
    };
  }

  async encryptStorage(encrypt: boolean) {
    this.Logger.info(`[STORAGE] ${encrypt ? 'Encrypting' : 'Decrypting'} storage...`);
    const res = await this.sendNodeServerRequest(`/storage/encrypt?encrypt=${encrypt}`, {
      method: 'POST',
    });
    const { success, msg } = await res.json();
    this.reboot();
    return { msg, success };
  }
}

export { BrightSign };

declare let BSDatagramSocket: DatagramSocket;

interface DatagramSocket {
  new (): DatagramSocket;
  BindLocalPort: (port: number) => boolean;
  GetLocalPort: () => number;
  JoinMulticastGroup: (group: string) => boolean;
  SendTo: (dest: string, port: number, data: ArrayBufferView | string) => boolean;
  Close: () => void;
  ondatagram: (event: DatagramSocketEvent) => void;
}

interface DatagramSocketEvent {
  readonly remoteHost: string;
  readonly remotePort: number;
  getBytes: () => ArrayBuffer;
}
