/* eslint-disable no-restricted-globals */

import { logger } from '@rails/actioncable';

// This is based on https://github.com/rails/rails/blob/5a477890c809d4a17dc0dede43c6b8cef81d8175/actioncable/app/javascript/action_cable/connection_monitor.js
// so that we can take advantage of the improved reconnection logic. We can remove this once we upgrade @rails/actioncable to a version that includes this.

// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting
// revival reconnections if things go astray. Internal class, not intended for direct user manipulation.

const now = () => new Date().getTime();

const secondsSince = (time) => (now() - time) / 1000;
class ConnectionMonitor {
  constructor(connection) {
    this.visibilityDidChange = this.visibilityDidChange.bind(this);
    this.connection = connection;
    this.reconnectAttempts = 0;
  }

  start() {
    if (!this.isRunning()) {
      this.startedAt = now();
      delete this.stoppedAt;
      this.startPolling();
      addEventListener('visibilitychange', this.visibilityDidChange);
      logger.log(
        `ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`,
      );
    }
  }

  stop() {
    if (this.isRunning()) {
      this.stoppedAt = now();
      this.stopPolling();
      removeEventListener('visibilitychange', this.visibilityDidChange);
      logger.log('ConnectionMonitor stopped');
    }
  }

  isRunning() {
    return this.startedAt && !this.stoppedAt;
  }

  recordPing() {
    this.pingedAt = now();
  }

  recordConnect() {
    this.reconnectAttempts = 0;
    this.recordPing();
    delete this.disconnectedAt;
    logger.log('ConnectionMonitor recorded connect');
  }

  recordDisconnect() {
    this.disconnectedAt = now();
    logger.log('ConnectionMonitor recorded disconnect');
  }

  // Private

  startPolling() {
    this.stopPolling();
    this.poll();
  }

  stopPolling() {
    clearTimeout(this.pollTimeout);
  }

  poll() {
    this.pollTimeout = setTimeout(() => {
      this.reconnectIfStale();
      this.poll();
    }, this.getPollInterval());
  }

  getPollInterval() {
    const { staleThreshold, reconnectionBackoffRate } = this.constructor;
    const backoff = (1 + reconnectionBackoffRate) ** Math.min(this.reconnectAttempts, 10);
    const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate;
    const jitter = jitterMax * Math.random();
    return staleThreshold * 1000 * backoff * (1 + jitter);
  }

  reconnectIfStale() {
    if (this.connectionIsStale()) {
      logger.log(
        `ConnectionMonitor detected stale connection. reconnectAttempts = ${
          this.reconnectAttempts
        }, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${
          this.constructor.staleThreshold
        } s`,
      );
      this.reconnectAttempts += 1;
      if (this.disconnectedRecently()) {
        logger.log(
          `ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(
            this.disconnectedAt,
          )} s`,
        );
      } else {
        logger.log('ConnectionMonitor reopening');
        this.connection.reopen();
      }
    }
  }

  get refreshedAt() {
    return this.pingedAt ? this.pingedAt : this.startedAt;
  }

  connectionIsStale() {
    return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
  }

  disconnectedRecently() {
    return (
      this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold
    );
  }

  visibilityDidChange() {
    if (document.visibilityState === 'visible') {
      setTimeout(() => {
        if (this.connectionIsStale() || !this.connection.isOpen()) {
          logger.log(
            `ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`,
          );
          this.connection.reopen();
        }
      }, 200);
    }
  }
}

ConnectionMonitor.staleThreshold = 6; // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
ConnectionMonitor.reconnectionBackoffRate = 0.15;

export default ConnectionMonitor;
