import Soap from '../utils/soap'
import * as Util from '../utils/util'

const namespaceAttributes = ['xmlns:tds="http://www.onvif.org/ver10/device/wsdl"']

/**
 * All ONVIF API functions return a Promise.
 *
 * @param {Error} error The error object.
 * @param {string} error.message The error message.
 * @param {xml} error.soap Associated SOAP xml that is cause of the error.
 * @param {object} error.fault Fault information
 * @param {string} error.fault.reason Reason for the error.
 * @param {string} error.fault.code Code for the error.
 * @param {string} error.fault.detail Details of the error.
 * @param {data} data The returned data when there is no error.
 */

/**
 * @class
 * Provide core functionality for Onvif Device Management.
 * <p>
 * {@link https://www.onvif.org/onvif/specs/core/ONVIF-Core-Specification-v220.pdf}<br>
 * {@link https://www.onvif.org/ver10/device/wsdl/devicemgmt.wsdl}<br>
 * {@link https://www.onvif.org/ver10/events/wsdl/event.wsdl}<br>
 * </p>
 * <h3>Functions</h3>
 * {@link Core#getWsdlUrl},
 * {@link Core#getServices},
 * {@link Core#getServiceCapabilities},
 * {@link Core#getCapabilities},
 * {@link Core#getHostname},
 * {@link Core#setHostname},
 * {@link Core#setHostnameFromDHCP},
 * {@link Core#getDNS},
 * {@link Core#setDNS},
 * {@link Core#getNTP},
 * {@link Core#setNTP},
 * {@link Core#getDynamicDNS},
 * setDynamicDNS,
 * {@link Core#getNetworkInterfaces},
 * setNetworkInterfaces,
 * {@link Core#getNetworkProtocols},
 * {@link Core#getNetworkDefaultGateway},
 * setNetworkDefaultGateway,
 * {@link Core#getZeroConfiguration},
 * setZeroConfiguration,
 * {@link Core#getIPAddressFilter},
 * setIPAddressFilter,
 * addIPAddressFilter,
 * removeIPAddressFilter,
 * {@link Core#getDot11Capabilities},
 * {@link Core#getDot11Status},
 * {@link Core#scanAvailableDot11Networks},
 * {@link Core#getDeviceInformation},
 * {@link Core#getSystemUris},
 * {@link Core#getSystemBackup},
 * restoreSystem,
 * startSystemRestore,
 * {@link Core#getSystemDateAndTime},
 * setSystemDateAndTime,
 * setSystemFactoryDefault,
 * upgradeSystemFirmware,
 * startFirmwareUpgrade,
 * {@link Core#getSystemLog},
 * {@link Core#getSystemSupportInformation},
 * {@link Core#systemReboot},
 * {@link Core#getScopes},
 * setScopes,
 * addScopes,
 * removeScopes,
 * {@link Core#getGeoLocation},
 * setGeoLocation,
 * deleteGeoLocation,
 * {@link Core#getDiscoveryMode},
 * setDiscoveryMode,
 * {@link Core#getRemoteDiscoveryMode},
 * setRemoteDiscoveryMode,
 * {@link Core#getDPAddresses},
 * setDPAddresses,
 * {@link Core#getAccessPolicy},
 * setAccessPolicy
 * {@link Core#getUsers},
 * createUsers,
 * deleteUsers,
 * setUser,
 * createDot1XConfiguration,
 * setDot1XConfiguration,
 * {@link Core#getDot1XConfiguration},
 * {@link Core#getDot1XConfigurations},
 * deleteDot1XConfigurations,
 * createCertificate,
 * {@link Core#getCertificates},
 * {@link Core#getCACertificates},
 * {@link Core#getCertificatesStatus},
 * setCertificatesStatus,
 * getPkcs10Request,
 * {@link Core#getClientCertificateMode},
 * setClientCertificateMode,
 * loadCertificates,
 * loadCertificateWithPrivateKey,
 * getCertificateInformation,
 * loadCACertificates,
 * deleteCertificates,
 * {@link Core#getRemoteUser},
 * setRemoteUser,
 * {@link Core#getEndpointReference},
 * {@link Core#getRelayOutputs},
 * setRelayOutputSettings,
 * setRelayOutputState,
 * sendAuxiliaryCommand
 * <br><br>
 * <h3>Overview</h3>
 * The Device Service is divided into five different categories: capabilities, network, system, I/O
 * and security commands. This set of commands can be used to get information about the
 * device capabilities and configurations or to set device configurations. An ONVIF compliant
 * device shall support the device management service as specified in [ONVIF DM WSDL]. A
 * basic set of operations are required for the device management service, other operations are
 * recommended or optional to support. The detailed requirements are listed under the command
 * descriptions.
 */
export default class Core {
  soap = new Soap()
  timeDiff = 0

  serviceAddress?: { href: string; host: string }
  username?: string
  password?: string

  version?: string

  /**
   * Call this function directly after instantiating a Core object.
   * @param serviceAddress An url object from url package - require('url').
   * @param username Optional only if the device does NOT have a user.
   * @param password Optional only if the device does NOT have a password.
   */
  init(serviceAddress: { href: string; host: string }, username?: string, password?: string) {
    this.serviceAddress = serviceAddress
    this.username = username
    this.password = password
  }

  /**
   * Private function for creating a SOAP request.
   * @param body The body of the xml.
   */
  private createRequest(body: string) {
    const soapEnvelope = this.soap.createRequest({
      body,
      xmlns: namespaceAttributes,
      diff: this.timeDiff,
      username: this.username,
      password: this.password
    })
    return soapEnvelope
  }

  private async buildRequest(methodName: string, xml?: string | null) {
    if (typeof methodName === 'undefined' || methodName === null) {
      throw new Error('The "methodName" argument for buildRequest is required.')
    }

    let errMsg = ''

    if ((errMsg = Util.isInvalidValue(methodName, 'string'))) {
      throw new Error('The "methodName" argument for buildRequest is invalid:' + errMsg)
    }

    let soapBody = ''

    if (!xml) {
      soapBody += `<tds:${methodName}/>`
    } else {
      soapBody += `<tds:${methodName}>`
      soapBody += xml
      soapBody += `</tds:${methodName}>`
    }

    const soapEnvelope = this.createRequest(soapBody)
    return await this.soap.makeRequest('core', this.serviceAddress, methodName, soapEnvelope)
  }

  /**
   * Returns the onvif device's time difference<br>
   * {@link getSystemDateAndTime} must be called first to get an accurate time.
   */
  getTimeDiff() {
    return this.timeDiff
  }

  // ---------------------------------------------
  // Core API
  // ---------------------------------------------

  /**
   * It is possible for an endpoint to request a URL that can be used to retrieve the complete
   * schema and WSDL definitions of a device. The command gives in return a URL entry point
   * where all the necessary product specific WSDL and schema definitions can be retrieved. The
   * device shall provide a URL for WSDL and schema download through the GetWsdlUrl
   * command.
   */
  getWsdlUrl() {
    return this.buildRequest('GetWsdlUrl', null)
  }

  /**
   * <strong>+++ I get an 'Action Failed' with Axis cameras. Hikvision works fine.</strong><br>
   * Returns a collection of the devices services and possibly their available capabilities. The
   * returned capability response message is untyped to allow future addition of services, service
   * revisions and service capabilities. All returned service capabilities shall be structured by
   * different namespaces which are supported by a device.<br>
   * A device shall implement this method if any of the ONVIF compliant services implements the
   * GetServiceCapabilities. For making sure about the structure of GetServices response with
   * capabilities, please refer to Annex C. Example for GetServices Response with capabilities.<br>
   * The version in GetServicesResponse shall contain the specification version number of the
   * corresponding service that is implemented by a device.
   * @param includeCapability The message contains a request for all services in the device and
   * possibly the capabilities for each service. If the Boolean
   * IncludeCapability is set, then the response shall include the services
   * capabilities.
   */
  async getServices(includeCapability?: boolean) {
    let errMsg = ''

    if (typeof includeCapability !== 'undefined' && includeCapability !== null) {
      if ((errMsg = Util.isInvalidValue(includeCapability, 'boolean'))) {
        throw new Error('The "includeCapability" argument for getServices is invalid: ' + errMsg)
      }
    }

    let soapBody = ''

    if (typeof includeCapability !== 'undefined' && includeCapability !== null) {
      // soapBody += '<tds:IncludeCapability>' + (includeCapability ? 1 : 0) + '</tds:IncludeCapability>'
      soapBody += '<tds:IncludeCapability>' + includeCapability + '</tds:IncludeCapability>'
    }

    return await this.buildRequest('GetServices', soapBody)
  }

  /**
   * <strong>+++ I get an 'Action Failed' with Axis cameras. Hikvision works fine.</strong><br>
   * This command returns the capabilities of the device service. The service shall implement this
   * method if the device supports the GetServices method.
   */
  getServiceCapabilities() {
    return this.buildRequest('GetServiceCapabilities')
  }

  /**
   * This method provides a backward compatible interface for the base capabilities. Refer to
   * GetServices for a full set of capabilities.<br>
   * Annex A describes how to interpret the indicated capability. Apart from the addresses, the
   * capabilities only reflect optional functions in this specification.
   */
  getCapabilities() {
    const soapBody = '<tds:Category>All</tds:Category>'
    return this.buildRequest('GetCapabilities', soapBody)
  }

  /**
   * This operation is used by an endpoint to get the hostname from a device. The device shall
   * return its hostname configurations through the GetHostname command.
   */
  getHostname() {
    return this.buildRequest('GetHostname')
  }

  /**
   * This operation sets the hostname on a device. It shall be possible to set the device hostname
   * configurations through the SetHostname command. Attention: a call to SetDNS may result in
   * overriding a previously set hostname.<br>
   * A device shall accept strings formated according to RFC 1123 section 2.1 or alternatively to
   * RFC 952, other string shall be considered as invalid strings.<br>
   * A device shall try to retrieve the name via DHCP when the HostnameFromDHCP capability is
   * set and an empty name string is provided.
   * @param name The host name. If Name is an empty string hostname
   * should be retrieved from DHCP, otherwise the specified Name
   * shall be used.
   */
  setHostname(name: string) {
    return this.buildRequest('SetHostname', `<tds:Name>${name || ''}</tds:Name>`)
  }

  /**
   * This operation controls whether the hostname shall be retrieved from DHCP.
   * A device shall support this command if support is signalled via the HostnameFromDHCP
   * capability. Depending on the device implementation the change may only become effective
   * after a device reboot. A device shall accept the command independent whether it is currently
   * using DHCP to retrieve its IPv4 address or not. Note that the device is not required to retrieve
   * its hostname via DHCP while the device is not using DHCP for retrieving its IP address. In the
   * latter case the device may fall back to the statically set hostname.
   * @param fromDHCP True if the hostname shall be obtained via DHCP.
   */
  async setHostnameFromDHCP(fromDHCP?: boolean) {
    let errMsg = ''

    if ((errMsg = Util.isInvalidValue(fromDHCP, 'boolean'))) {
      throw new Error('The "fromDHCP" argument for setHostnameFromDHCP is invalid: ' + errMsg)
    }

    return await this.buildRequest('SetHostnameFromDHCP', `<tds:FromDHCP>${fromDHCP}</tds:FromDHCP>`)
  }

  /**
   * This operation gets the DNS settings from a device. The device shall return its DNS
   * configurations through the GetDNS command.
   */
  async getDNS() {
    const result = await this.buildRequest('GetDNS')

    if ('data' in result) {
      try {
        const di = result.data.DNSInformation

        if (!di.SearchDomain) {
          di.SearchDomain = []
        } else if (!Array.isArray(di.SearchDomain)) {
          di.SearchDomain = [di.SearchDomain]
        }

        if (!di.DNSManual) {
          di.DNSManual = []
        } else if (!Array.isArray(di.DNSManual)) {
          di.DNSManual = [di.DNSManual]
        }

        result.data = di
      } catch (e) {}
    }

    return result as {
      data?: {
        GetDNSResponse: any
        SearchDomain: any[]
        DNSManual: { type: 'IPv4' | 'IPv6'; IP4Address: string; IP6Address: number }[]
      }
    }
  }

  /**
   * This operation sets the DNS settings on a device. It shall be possible to set the device DNS
   * configurations through the SetDNS command.<br>
   * It is valid to set the FromDHCP flag while the device is not using DHCP to retrieve its IPv4
   * address.
   * @param fromDHCP True if the DNS servers are obtained via DHCP.
   * @param searchDomain The domain(s) to search if the hostname is not
   * fully qualified.
   * @param DNSManual A list of manually given DNS servers
   */
  async setDNS(
    fromDHCP: boolean,
    searchDomain?: any[],
    DNSManual?: { type: 'IPv4' | 'IPv6'; IPv4Address?: string; IPv6Address?: string }[]
  ) {
    let errMsg = ''

    if ((errMsg = Util.isInvalidValue(fromDHCP, 'boolean'))) {
      throw new Error('The "fromDHCP" argument for setDNS is invalid: ' + errMsg)
    }

    if (typeof searchDomain !== 'undefined' && searchDomain !== null) {
      if ((errMsg = Util.isInvalidValue(searchDomain, 'array', true))) {
        throw new Error('The "searchDomain" argument for setDNS is invalid: ' + errMsg)
      }

      for (let i = 0; i < searchDomain.length; i++) {
        if ((errMsg = Util.isInvalidValue(searchDomain[i], 'string'))) {
          throw new Error(`A "searchDomain" property was invalid(${searchDomain[i]}): ` + errMsg)
        }
      }
    }

    if (typeof DNSManual !== 'undefined' && DNSManual !== null) {
      if ((errMsg = Util.isInvalidValue(DNSManual, 'array', true))) {
        throw new Error('The "DNSManual" argument for setDNS is invalid: ' + errMsg)
      }

      for (let i = 0; i < DNSManual.length; i++) {
        const d = DNSManual[i]

        if ((errMsg = Util.isInvalidValue(d, 'object'))) {
          throw new Error(`A "DNSManual" property for setDNS is invalid(${JSON.stringify(d)}): ` + errMsg)
        }

        const type = d.type

        if ((errMsg = Util.isInvalidValue(type, 'string'))) {
          throw new Error('The "type" property for setDNS is invalid: ' + errMsg)
        }

        if (!type.match(/^(IPv4|IPv6)$/)) {
          throw new Error('The "type" value for setDNS is invalid: The value must be either "IPv4" or "IPv6".')
        }

        if (type === 'IPv4' && (errMsg = Util.isInvalidValue(d.IPv4Address, 'string'))) {
          throw new Error('The "IPv4Address" property for setDNS is invalid: ' + errMsg)
        }

        if (type === 'IPv6' && (errMsg = Util.isInvalidValue(d.IPv6Address, 'string'))) {
          throw new Error('The "IPv6Address" property for setDNS is invalid: ' + errMsg)
        }
      }
    }

    let soapBody = ''
    // soapBody += '<tds:FromDHCP>' + (fromDHCP ? 1 : 0) + '</tds:FromDHCP>'
    soapBody += '<tds:FromDHCP>' + fromDHCP + '</tds:FromDHCP>'

    if (typeof searchDomain !== 'undefined' && searchDomain !== null) {
      searchDomain.forEach(s => {
        soapBody += '<tds:SearchDomain>' + s + '</tds:SearchDomain>'
      })
    }

    if (typeof DNSManual !== 'undefined' && DNSManual !== null) {
      if (DNSManual.length === 0) {
        soapBody += '<tds:DNSManual></tds:DNSManual>'
      } else {
        DNSManual.forEach(d => {
          soapBody += '<tds:DNSManual>'
          soapBody += '<tt:Type>' + d.type + '</tt:Type>'

          if (d.type === 'IPv4') {
            soapBody += '<tt:IPv4Address>' + d.IPv4Address + '</tt:IPv4Address>'
          } else {
            soapBody += '<tt:IPv6Address>' + d.IPv6Address + '</tt:IPv6Address>'
          }

          soapBody += '</tds:DNSManual>'
        })
      }
    }

    return await this.buildRequest('SetDNS', soapBody)
  }

  /**
   * This operation gets the NTP settings from a device. If the device supports NTP, it shall be
   * possible to get the NTP server settings through the GetNTP command.
   */
  getNTP() {
    return this.buildRequest('GetNTP')
  }

  /**
   * This operation sets the NTP settings on a device. If support for NTP is signalled via the NTP
   * capability, it shall be possible to set the NTP server settings through the SetNTP command.<br>
   * A device shall accept string formated according to RFC 1123 section 2.1, other string shall be
   * considered as invalid strings. It is valid to set the FromDHCP flag while the device is not using
   * DHCP to retrieve its IPv4 address.<br>
   * Changes to the NTP server list shall not affect the clock mode DateTimeType. Use
   * SetSystemDateAndTime to activate NTP operation.
   * @param fromDHCP True if the NTP servers are obtained via DHCP.
   * @param NTPManual A list of manually given NTP servers when they
   * not are obtained via DHCP.
   */
  async setNTP(fromDHCP: boolean, NTPManual?: any[]) {
    let errMsg = ''

    if ((errMsg = Util.isInvalidValue(fromDHCP, 'boolean'))) {
      throw new Error('The "fromDHCP" argument for setNTP is invalid: ' + errMsg)
    }

    if (typeof NTPManual !== 'undefined' && NTPManual !== null) {
      if ((errMsg = Util.isInvalidValue(NTPManual, 'array', true))) {
        throw new Error('The "NTPManual" argument for setNTP is invalid: ' + errMsg)
      }

      for (let i = 0; i < NTPManual.length; i++) {
        const d = NTPManual[i]
        if ((errMsg = Util.isInvalidValue(d, 'object'))) {
          throw new Error(`A "NTPManual" property for setNTP is invalid(${JSON.stringify(d)}): ` + errMsg)
        }

        const type = d.type

        if ((errMsg = Util.isInvalidValue(type, 'string'))) {
          throw new Error('The "type" property for setNTP is invalid: ' + errMsg)
        }

        if (!type.match(/^(IPv4|IPv6)$/)) {
          throw new Error('The "type" value for setNTP is invalid: The value must be either "IPv4" or "IPv6".')
        }

        if (type === 'IPv4' && (errMsg = Util.isInvalidValue(d.IPv4Address, 'string'))) {
          throw new Error('The "IPv4Address" property for setNTP is invalid: ' + errMsg)
        }

        if (type === 'IPv6' && (errMsg = Util.isInvalidValue(d.IPv6Address, 'string'))) {
          throw new Error('The "IPv6Address" property for setNTP is invalid: ' + errMsg)
        }
      }
    }

    let soapBody = ''
    // soapBody += '<tds:FromDHCP>' + (fromDHCP ? 1 : 0) + '</tds:FromDHCP>'
    soapBody += '<tds:FromDHCP>' + fromDHCP + '</tds:FromDHCP>'

    if (typeof NTPManual !== 'undefined' && NTPManual !== null) {
      if (NTPManual.length === 0) {
        soapBody += '<tds:NTPManual></tds:NTPManual>'
      } else {
        NTPManual.forEach(d => {
          soapBody += '<tds:NTPManual>'
          soapBody += '<tt:Type>' + d.type + '</tt:Type>'

          if (d.type === 'IPv4') {
            soapBody += '<tt:IPv4Address>' + d.IPv4Address + '</tt:IPv4Address>'
          } else {
            soapBody += '<tt:IPv6Address>' + d.IPv6Address + '</tt:IPv6Address>'
          }
          soapBody += '</tds:NTPManual>'
        })
      }
    }

    return await this.buildRequest('SetNTP', soapBody)
  }

  /**
   * This operation gets the dynamic DNS settings from a device. If the device supports dynamic
   * DNS as specified in [RFC 2136] and [RFC 4702], it shall be possible to get the type, name
   * and TTL through the GetDynamicDNS command
   */
  getDynamicDNS() {
    return this.buildRequest('GetDynamicDNS')
  }

  /**
   *
   * @param type The type of update. There are three possible types: the
   * device desires no update (NoUpdate), the device wants the
   * DHCP server to update (ServerUpdates) and the device does
   * the update itself (ClientUpdates).
   * @param name The DNS name in case of the device does the update.
   * @param ttl Time to live
   */
  async setDynamicDNS(type: 'NoUpdate' | 'ServerUpdates' | 'ClientUpdates', name?: string, ttl?: number) {
    let errMsg = ''

    if ((errMsg = Util.isInvalidValue(type, 'string'))) {
      throw new Error('The "type" argument for setDynamicDNS is invalid: ' + errMsg)
    }

    if (!type.match(/^(NoUpdate|ServerUpdates|ClientUpdates)$/)) {
      throw new Error('The "type" value for setDynamicDNS is invalid: The value must be either "IPv4" or "IPv6".')
    }

    if (typeof name !== 'undefined' && name !== null && (errMsg = Util.isInvalidValue(name, 'string', true))) {
      throw new Error('The "name" argument for setDynamicDNS is invalid: ' + errMsg)
    }

    if (typeof ttl !== 'undefined' && ttl !== null && (errMsg = Util.isInvalidValue(ttl, 'integer'))) {
      throw new Error('The "ttl" argument for setDynamicDNS is invalid: ' + errMsg)
    }

    let soapBody = ''
    soapBody += '<tds:SetDynamicDNS>'
    soapBody += '<tt:Type>' + type + '</tt:Type>'

    if (typeof name !== 'undefined' && name !== null) {
      soapBody += '<tt:Name>' + name + '</tt:Name>'
    }

    if (typeof ttl !== 'undefined' && ttl !== null) {
      soapBody += '<tt:TTL>' + ttl + '</tt:TTL>'
    }

    soapBody += '</tds:SetDynamicDNS>'

    const soapEnvelope = this.createRequest(soapBody)
    return await this.soap.makeRequest('core', this.serviceAddress, 'SetDynamicDNS', soapEnvelope)
  }

  /**
   * This operation gets the network interface configuration from a device. The device shall support return of network interface configuration settings as defined by the NetworkInterface type through the GetNetworkInterfaces command.
   */
  getNetworkInterfaces() {
    return this.buildRequest('GetNetworkInterfaces')
  }

  /**
   * This operation gets defined network protocols from a device. The device shall support the GetNetworkProtocols command returning configured network protocols.
   */
  getNetworkProtocols() {
    return this.buildRequest('GetNetworkProtocols')
  }

  /**
   * This operation gets the default gateway settings from a device. The device shall support the GetNetworkDefaultGateway command returning configured default gateway address(es).
   */
  getNetworkDefaultGateway() {
    return this.buildRequest('GetNetworkDefaultGateway')
  }

  /**
   * This operation gets the zero-configuration from a device. If the device supports dynamic IP configuration according to [RFC3927], it shall support the return of IPv4 zero configuration address and status through the GetZeroConfiguration command.<br>
   * Devices supporting zero configuration on more than one interface shall use the extension to list the additional interface settings.
   */
  getZeroConfiguration() {
    return this.buildRequest('GetZeroConfiguration')
  }

  /**
   * This operation gets the IP address filter settings from a device. If the device supports device access control based on IP filtering rules (denied or accepted ranges of IP addresses), the device shall support the GetIPAddressFilter command.
   */
  getIPAddressFilter() {
    return this.buildRequest('GetIPAddressFilter')
  }

  /**
   * This operation returns the IEEE802.11 capabilities. The device shall support this operation.<br>
   * <strong>Not all do.</strong>
   */
  getDot11Capabilities() {
    return this.buildRequest('GetDot11Capabilities')
  }

  /**
   * This operation returns the status of a wireless network interface. The device shall support this command.<br>
   * <strong>Not all do.</strong>
   * @param {string} interfaceToken Network reference token.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  async getDot11Status(interfaceToken: string) {
    let errMsg = ''

    if ((errMsg = Util.isInvalidValue(interfaceToken, 'string'))) {
      throw new Error('The "interfaceToken" argument for getDot11Status is invalid: ' + errMsg)
    }

    let soapBody = ''
    soapBody += '<tt:InterfaceToken>' + interfaceToken + '</tt:InterfaceToken>'

    return await this.buildRequest('GetDot11Status', soapBody)
  }

  /**
   * This operation returns a lists of the wireless networks in range of the device. A device should
   * support this operation. The following status can be returned for each network:
   * <ul>
   * <li>SSID (shall)</li>
   * <li>BSSID (should)</li>
   * <li>Authentication and key management suite(s) (should)</li>
   * <li>Pair cipher(s) (should)</li>
   * <li>Group cipher(s) (should)</li>
   * <li>Signal strength (should)</li>
   * </ul>
   * @param {string} interfaceToken Network reference token.
   */
  async scanAvailableDot11Networks(interfaceToken: string) {
    let errMsg = ''

    if ((errMsg = Util.isInvalidValue(interfaceToken, 'string'))) {
      throw new Error('The "interfaceToken" argument for scanAvailableDot11Networks is invalid: ' + errMsg)
    }

    const soapBody = '<tt:ReferenceToken>' + interfaceToken + '</tt:ReferenceToken>'

    return await this.buildRequest('ScanAvailableDot11Networks', soapBody)
  }

  /**
   * This operation gets device information, such as manufacturer, model and firmware version
   * from a device. The device shall support the return of device information through the
   * GetDeviceInformation command.
   */
  getDeviceInformation() {
    return this.buildRequest('GetDeviceInformation')
  }

  /**
   * This operation is used to retrieve URIs from which system information may be downloaded
   * using HTTP. URIs may be returned for the following system information:<br>
   * <strong>System Logs.</strong> Multiple system logs may be returned, of different types. The exact format of
   * the system logs is outside the scope of this specification.<br>
   * <strong>Support Information.</strong> This consists of arbitrary device diagnostics information from a device.
   * The exact format of the diagnostic information is outside the scope of this specification.<br>
   * <strong>System Backup.</strong> The received file is a backup file that can be used to restore the current
   * device configuration at a later date. The exact format of the backup configuration file is
   * outside the scope of this specification.<br>
   * If the device allows retrieval of system logs, support information or system backup data, it
   * should make them available via HTTP GET. If it does, it shall support the GetSystemUris
   * command.
   */
  getSystemUris() {
    return this.buildRequest('GetSystemUris')
  }

  /**
   * <i>This interface has been deprecated.</i><br>
   * A device shall implement this command if the capability
   * SystemBackup is signaled. For a replacement method see section 8.3.2 and 8.3.5.<br>
   * This operation retrieves system backup configuration file(s) from a device. The backup is
   * returned with reference to a name and mime-type together with binary data. The format of the
   * backup configuration data is vendor specific. It is expected that after completion of the restore
   * operation the device is working on the same configuration as that of the time the configuration
   * was backed up. Note that the configuration of static IP addresses may differ.<br>
   * Device vendors may put restrictions on the functionality to be restored. The detailed behavior
   * is outside the scope of this specification.<br>
   * The backup configuration file(s) are transmitted through MTOM [MTOM].
   */
  getSystemBackup() {
    return this.buildRequest('GetSystemBackup')
  }

  /**
   * This operation gets the device system date and time.
   * The device shall support the return of the daylight
   * saving setting and of the manual system date and time
   * (if applicable) or indication of NTP time (if applicable)
   * through the GetSystemDateAndTime command.<br>
   * A device shall provide the UTCDateTime information although
   * the item is marked as optional to ensure backward compatibility.<br>
   * This is required to be called for devices that
   * support the GetSystemDateAndTime SOAP method so
   * a time diff can be used in subsequent calls to
   * the device.
   */
  async getSystemDateAndTime() {
    const results = await this.buildRequest('GetSystemDateAndTime')

    if ('data' in results) {
      const parsed = this.parseGetSystemDateAndTime(results.data)

      if (parsed && parsed.date) {
        const deviceTime = parsed.date.getTime()
        const localTime = new Date().getTime()
        this.timeDiff = deviceTime - localTime
      }
    }

    return results as {
      data?: {
        GetSystemDateAndTimeResponse?: {
          SystemDateAndTime?: {
            DateTimeType?: string
            DaylightSavings?: string
            UTCDateTime?: {
              Time?: {
                Hour?: string
                Minute?: string
                Second?: string
              }
              Date?: {
                Day?: string
                Month?: string
                Year?: string
              }
            }
            TimeZone?: {
              TZ?: string
            }
          }
        }
      }
    }
  }

  /**
   * Private function
   * @param sdt GetSystemDateAndTimeResponse converted to JSON.
   */
  private parseGetSystemDateAndTime(sdt: any) {
    const s0 = sdt
    if (!s0) {
      return null
    }
    const s1 = s0.GetSystemDateAndTimeResponse
    if (!s1) {
      return null
    }
    const s2 = s1.SystemDateAndTime
    if (!s2) {
      return null
    }

    const type = s2.DateTimeType || ''
    let dst = null
    if (s2.DaylightSavings) {
      dst = s2.DaylightSavings === 'true'
    }
    const tz = s2.TimeZone && s2.TimeZone.TZ ? s2.TimeZone.TZ : ''
    let date = null
    if (s2.UTCDateTime) {
      const udt = s2.UTCDateTime
      const t = udt.Time
      const d = udt.Date
      if (t && d && t.Hour && t.Minute && t.Second && d.Year && d.Month && d.Day) {
        date = new Date()
        date.setUTCFullYear(parseInt(d.Year, 10))
        date.setUTCMonth(parseInt(d.Month, 10) - 1)
        date.setUTCDate(parseInt(d.Day, 10))
        date.setUTCHours(parseInt(t.Hour, 10))
        date.setUTCMinutes(parseInt(t.Minute, 10))
        date.setUTCSeconds(parseInt(t.Second, 10))
      }
    }
    const res = {
      type: type,
      dst: dst,
      tz: tz,
      date: date
    }
    return res
  }

  /**
   * This operation gets a system log from the device. The exact format of the system logs is outside the scope of this standard.
   * @param logType Specifies the type of system log to get.
   * <ul>
   * <li>System: Indicates that a system log is requested.</li>
   * <li>Access: Indicates that a access log is requested.</li>
   * </ul>
   */
  async getSystemLog(logType: 'System' | 'Access') {
    let errMsg = ''

    if ((errMsg = Util.isInvalidValue(logType, 'string'))) {
      throw new Error('The "logType" argument for getSystemLog is invalid: ' + errMsg)
    }

    if (!logType.match(/^(System|Access)$/)) {
      throw new Error('The "logType" value for getSystemLog is invalid: The value must be either "System" or "Access".')
    }

    const soapBody = '<tt:LogType>' + logType + '</tt:LogType>'
    return await this.buildRequest('GetSystemLog', soapBody)
  }

  /**
   * This operation gets arbitary device diagnostics information from the device.
   */
  getSystemSupportInformation() {
    return this.buildRequest('GetSystemSupportInformation')
  }

  /**
   * This operation reboots the device.
   * @returns Contains the reboot message from the device (ie: Rebooting in 90 seconds).
   */
  systemReboot() {
    return this.buildRequest('SystemReboot')
  }

  /**
   * This operation requests the scope parameters of a device. The scope parameters are used in
   * the device discovery to match a probe message, see Section 7. The Scope parameters are of
   * two different types:
   * <ul>
   *  <li>Fixed</li>
   *  <li>Configurable</li>
   * </ul>
   * Fixed scope parameters are permanent device characteristics and cannot be removed
   * through the device management interface. The scope type is indicated in the scope list
   * returned in the get scope parameters response. A device shall support retrieval of discovery
   * scope parameters through the GetScopes command. As some scope parameters are
   * mandatory, the device shall return a non-empty scope list in the response.
   */
  getScopes() {
    return this.buildRequest('GetScopes')
  }

  /**
   * This operation gets the geo location information of a device. A device that signals support for
   * GeoLocation via the capability GeoLocationEntities shall support the retrieval of geo location
   * information via this command.
   */
  getGeoLocation() {
    return this.buildRequest('GetGeoLocation')
  }

  /**
   * This operation gets the discovery mode of a device
   */
  getDiscoveryMode() {
    return this.buildRequest('GetDiscoveryMode')
  }

  /**
   * This operation gets the remote discovery mode of a device. See Section 7.4 for the definition of remote discovery extensions. A device that supports remote discovery shall support retrieval of the remote discovery mode setting through the GetRemoteDiscoveryMode command.
   */
  getRemoteDiscoveryMode() {
    return this.buildRequest('GetRemoteDiscoveryMode')
  }

  /**
   * This operation gets the remote DP address or addresses from a device. If the device supports remote discovery, as specified in Section 7.4, the device shall support retrieval of the remote DP address(es) through the GetDPAddresses command.
   */
  getDPAddresses() {
    return this.buildRequest('GetDPAddresses')
  }

  /**
   * Access to different services and sub-sets of services should be subject
   * to access control. The WS-Security framework gives the prerequisite for
   * end-point authentication. Authorization decisions can then be taken
   * using an access security policy. This standard does not mandate any
   * particular policy description format or security policy but this is
   * up to the device manufacturer or system provider to choose policy and
   * policy description format of choice. However, an access policy (in
   * arbitrary format) can be requested using this command. If the device
   * supports access policy settings based on WS-Security authentication,
   * then the device shall support this command.
   */
  getAccessPolicy() {
    return this.buildRequest('GetAccessPolicy')
  }

  /**
   * This operation lists the registered users and corresponding credentials on a device. The device shall support retrieval of registered device users and their credentials for the user token through the GetUsers command.
   */
  getUsers() {
    return this.buildRequest('GetUsers')
  }

  /**
   * This operation gets one IEEE 802.1X configuration parameter set from the device by
   * specifying the configuration token (Dot1XConfigurationToken).<br>
   * The device shall support this command if support for IEEE 802.1X is signaled via the Security
   * Dot1X capability.<br>
   * Regardless of whether the 802.1X method in the retrieved configuration has a password or
   * not, the device shall not include the Password element in the response.
   * @param dot1XConfigurationToken Dot1XConfigurationToken [ReferenceToken]
   */
  async getDot1XConfiguration(dot1XConfigurationToken: string) {
    let errMsg = ''

    if ((errMsg = Util.isInvalidValue(dot1XConfigurationToken, 'string'))) {
      throw new Error('The "dot1XConfigurationToken" argument for getDot1XConfiguration is invalid: ' + errMsg)
    }

    const soapBody = '<tds:Dot1XConfigurationToken>' + dot1XConfigurationToken + '</tds:Dot1XConfigurationToken>'
    return await this.buildRequest('GetDot1XConfiguration', soapBody)
  }

  /**
   * This operation gets all the existing IEEE 802.1X configuration parameter sets from the device.<br>
   * The device shall respond with all the IEEE 802.1X configurations so that the client can get to
   * know how many IEEE 802.1X configurations are existing and how they are configured.<br>
   * The device shall support this command if support for IEEE 802.1X is signaled via the Security
   * Dot1X capability.<br>
   * Regardless of whether the 802.1X method in the retrieved configuration has a password or
   * not, the device shall not include the Password element in the response.
   */
  getDot1XConfigurations() {
    return this.buildRequest('GetDot1XConfigurations')
  }

  /**
   * This operation gets all device server certificates (including self-signed) for the purpose of TLS
   * authentication and all device client certificates for the purpose of IEEE 802.1X authentication.<br>
   * This command lists only the TLS server certificates and IEEE 802.1X client certificates for the
   * device (neither trusted CA certificates nor trusted root certificates). The certificates are
   * returned as binary data. A device that supports TLS shall support this command and the
   * certificates shall be encoded using ASN.1 [X.681], [X.682], [X.683] DER [X.690] encoding
   * rules.
   */
  getCertificates() {
    return this.buildRequest('GetCertificates')
  }

  /**
   * CA certificates will be loaded into a device and be used for the sake of following two cases.<br>
   * The one is for the purpose of TLS client authentication in TLS server function. The other one
   * is for the purpose of Authentication Server authentication in IEEE 802.1X function. This
   * operation gets all CA certificates loaded into a device. A device that supports either TLS client
   * authentication or IEEE 802.1X shall support this command and the returned certificates shall
   * be encoded using ASN.1 [X.681], [X.682], [X.683] DER [X.690] encoding rules.
   */
  getCACertificates() {
    return this.buildRequest('GetCACertificates')
  }

  /**
   * This operation is specific to TLS functionality. This operation gets the status
   * (enabled/disabled) of the device TLS server certificates. A device that supports TLS shall
   * support this command.
   */
  getCertificatesStatus() {
    return this.buildRequest('GetCertificatesStatus')
  }

  /**
   * This operation is specific to TLS functionality. This operation gets the status
   * (enabled/disabled) of the device TLS client authentication. A device that supports TLS shall
   * support this command.
   */
  getClientCertificateMode() {
    return this.buildRequest('GetClientCertificateMode')
  }

  /**
   * This operation returns the configured remote user (if any). A device that signals support for
   * remote user handling via the Security Capability RemoteUserHandling shall support this
   * operation. The user is only valid for the WS-UserToken profile or as a HTTP / RTSP user.<br>
   * The algorithm to use for deriving the password is described in section 5.12.3.1.
   */
  getRemoteUser() {
    return this.buildRequest('GetRemoteUser')
  }

  /**
   * A client can ask for the device service endpoint reference address property that can be used
   * to derive the password equivalent for remote user operation. The device should support the
   * GetEndpointReference command returning the address property of the device service
   * endpoint reference.
   */
  getEndpointReference() {
    return this.buildRequest('GetEndpointReference')
  }

  /**
   * This operation gets a list of all available relay outputs and their settings.
   */
  getRelayOutputs() {
    return this.buildRequest('GetRelayOutputs')
  }
}
