/**
Copyright: 2024 Contrast Security, Inc
  Contact: support@contrastsecurity.com
  License: Commercial

  NOTICE: This Software and the patented inventions embodied within may only be
  used as part of Contrast Security’s commercial offerings. Even though it is
  made available through public repositories, use of this Software is subject to
  the applicable End User Licensing Agreement found at
  https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
  between Contrast Security and the End User. The Software may not be reverse
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
  way not consistent with the End User License Agreement.
*/
'use strict';

const BaseConnectionState = require('./base-connection-state');
const {
  CONNECTION_STATES: { FAILURE, SUCCESS, UNKNOWN },
  MAX_START_WAIT_MS,
  RESEND_WAIT_MS
} = require('./constants');

/**
 * Localizes state-specific behavior related to sending messages to the
 * contrast-service while the Speedracer instance is in the UNKNOWN connection
 * state. 'Unknown' refers to the certainty of whether the service is running.
 * While in this state, we will attempt to send a message to the service as
 * configured. If a failure results from this attempt, and the failure suggests
 * that the service is not running per the configuration, we'll attempt to start
 * and own the service ourselves, running it in a child process. From within
 * this state, valid state transitions are either FAILURE or SUCCESS. If we
 * cannot start the service this will transition to FAILURE. If we start and
 * send a successful message this will transition to SUCCESS. In that sense this
 * is not a stable state since we will always transition out of it into one of
 * the others; the others are stable states.
 */
class UnknownConnectionState extends BaseConnectionState {
  constructor(speedracer) {
    super(speedracer);
    this.state = UNKNOWN;
  }

  /**
   * Sends a message to the service.
   * @param {SpeedracerBaseMessage} message
   * @returns {Promise}
   */
  async send(message) {
    try {
      const clientResponse = await super.send(message);
      this.speedracer.transitionConnectionState(SUCCESS);
      return clientResponse;
    } catch (err) {
      // See if we need to start or wait for service
      try {
        await this.handleSendFailure(err);
        await this.retryInitialization();
        const result = await this.speedracer.send(message);
        return result;
      } catch (err) {
        this.speedracer.transitionConnectionState(FAILURE);
        throw err;
      }
    }
  }

  /**
   * Called when sending a message to the service results in an error. There are
   * a few things we can do depending on our internal state that keeps track of
   * our connection history. We may need to start/wait for the service before
   * attempting to resend.
   * @param {SpeedracerBaseMessage} message The message sent
   * @param {Error} err The Error that occurred while sending message
   * @returns {Promise}
   */
  handleSendFailure(err) {
    this.speedracer._messageErrorCount++;

    if (this.shouldTryToStart(err)) {
      return this.speedracer.start();
    } else if (this.shouldWaitForReadyService(err)) {
      return new Promise((resolve) => {
        const waitTime = RESEND_WAIT_MS * this.speedracer._messageErrorCount;
        this.speedracer.logger.debug(
          `waiting ${waitTime} ms for service to be ready`
        );
        setTimeout(resolve, waitTime);
      });
    }
    if (this.speedracer.startFailureError) {
      err = this.speedracer.startFailureError;
    }

    return Promise.reject(err);
  }

  /**
   * As per SRA1-3-XXX we need to resend initialization when we initialize
   * a new speedracer / speedracer connection after we've already sent the
   * messages.
   * @returns {Promise}
   */
  async retryInitialization() {
    const startup = this.speedracer.messageCache.get('startup');
    const appcreate = this.speedracer.messageCache.get('appcreate');

    if (!startup || !appcreate) return Promise.resolve();

    this.speedracer.logger.debug(
      'Sending initialization messages to service'
    );

    await this.speedracer.send(startup);
    await this.speedracer.send(appcreate);
  }

  /**
   * Returns true if the send message failure suggests that the service is not
   * running and we've never tried to start it ourselves.
   * @param {Error} err The Error raised when the message was sent
   * @returns {Boolean}
   */
  shouldTryToStart(err) {
    const {
      speedracer: { client }
    } = this;
    return (
      client.isConnectFailureError(err) &&
      !this.speedracer.serviceProcess &&
      !this.speedracer.startFailureError
    );
  }

  /**
   * Returns true if the message failure suggests that the service is not
   * running but we have attempted to start it ourselves. We wait a maximum of
   * 3 seconds before giving up waiting.
   * @param {Error} err The Error raised when the message was sent
   * @returns {Boolean}
   */
  shouldWaitForReadyService(err) {
    const { speedracer } = this;

    return (
      speedracer.client.isConnectFailureError(err) &&
      !!speedracer.serviceProcess &&
      speedracer.connectionState.state === UNKNOWN &&
      speedracer._messageErrorCount * RESEND_WAIT_MS < MAX_START_WAIT_MS
    );
  }
}

module.exports = UnknownConnectionState;
