/* eslint no-bitwise: ["error", { "allow": ["~", "&", ">>"] }] */
/* global navigator */

import CborReader from 'cbor-js';

import { WebCryptoModule, AesCbcCryptor, LegacyCryptor } from '../crypto/modules/WebCryptoModule/webCryptoModule';
import type { WebCryptoModule as CryptoModuleType } from '../crypto/modules/WebCryptoModule/webCryptoModule';

import { SubscriptionWorkerMiddleware } from '../transport/subscription-worker/subscription-worker-middleware';
import { ExtendedConfiguration, PlatformConfiguration } from '../core/interfaces/configuration';
import { stringifyBufferKeys } from '../core/components/stringify_buffer_keys';
import { PubNubConfiguration, setDefaults } from './components/configuration';
import { CryptorConfiguration } from '../core/interfaces/crypto-module';
import { PubNubFile, PubNubFileParameters } from '../file/modules/web';
import { makeConfiguration } from '../core/components/configuration';
import { TokenManager } from '../core/components/token_manager';
import { Cryptography } from '../core/interfaces/cryptography';
import { PubNubMiddleware } from '../transport/middleware';
import { WebTransport } from '../transport/web-transport';
import { decode } from '../core/components/base64_codec';
import { Transport } from '../core/interfaces/transport';
import Crypto from '../core/components/cryptography';
import WebCryptography from '../crypto/modules/web';
import { PubNubCore } from '../core/pubnub-common';
import Cbor from '../cbor/common';

/**
 * PubNub client for browser platform.
 */
export default class PubNub extends PubNubCore<ArrayBuffer | string, PubNubFileParameters, PubNubFile> {
  /**
   * Data encryption / decryption module constructor.
   */
  static CryptoModule: typeof CryptoModuleType | undefined =
    process.env.CRYPTO_MODULE !== 'disabled' ? WebCryptoModule : undefined;

  /**
   * Create and configure the PubNub client core.
   *
   * @param configuration - User-provided PubNub client configuration.
   *
   * @returns Configured and ready to use PubNub client.
   */
  constructor(configuration: PubNubConfiguration) {
    const sharedWorkerRequested = configuration.subscriptionWorkerUrl !== undefined;
    const configurationCopy = setDefaults(configuration);
    const platformConfiguration: PubNubConfiguration & ExtendedConfiguration & PlatformConfiguration = {
      ...configurationCopy,
      sdkFamily: 'Web',
    };

    if (process.env.FILE_SHARING_MODULE !== 'disabled') platformConfiguration.PubNubFile = PubNubFile;

    // Prepare full client configuration.
    const clientConfiguration = makeConfiguration(
      platformConfiguration,
      (cryptoConfiguration: CryptorConfiguration) => {
        if (!cryptoConfiguration.cipherKey) return undefined;

        if (process.env.CRYPTO_MODULE !== 'disabled') {
          const cryptoModule = new WebCryptoModule({
            default: new LegacyCryptor({
              ...cryptoConfiguration,
              ...(!cryptoConfiguration.logger ? { logger: clientConfiguration.logger() } : {}),
            }),
            cryptors: [new AesCbcCryptor({ cipherKey: cryptoConfiguration.cipherKey })],
          });

          return cryptoModule;
        } else return undefined;
      },
    );

    if (process.env.CRYPTO_MODULE !== 'disabled') {
      // Ensure that the logger has been passed to the user-provided crypto module.
      if (clientConfiguration.getCryptoModule())
        (clientConfiguration.getCryptoModule() as WebCryptoModule).logger = clientConfiguration.logger();
    }

    // Prepare Token manager.
    let tokenManager: TokenManager | undefined;
    if (process.env.CRYPTO_MODULE !== 'disabled') {
      tokenManager = new TokenManager(
        new Cbor((arrayBuffer: ArrayBuffer) => stringifyBufferKeys(CborReader.decode(arrayBuffer)), decode),
      );
    }

    // Legacy crypto (legacy data encryption / decryption and request signature support).
    let crypto: Crypto | undefined;
    if (process.env.CRYPTO_MODULE !== 'disabled') {
      if (clientConfiguration.getCipherKey() || clientConfiguration.secretKey) {
        crypto = new Crypto({
          secretKey: clientConfiguration.secretKey,
          cipherKey: clientConfiguration.getCipherKey(),
          useRandomIVs: clientConfiguration.getUseRandomIVs(),
          customEncrypt: clientConfiguration.getCustomEncrypt(),
          customDecrypt: clientConfiguration.getCustomDecrypt(),
          logger: clientConfiguration.logger(),
        });
      }
    }

    let cryptography: Cryptography<ArrayBuffer | string> | undefined;
    if (process.env.CRYPTO_MODULE !== 'disabled') cryptography = new WebCryptography();

    // Setup transport provider.
    let transport: Transport = new WebTransport(clientConfiguration.logger(), platformConfiguration.transport);

    if (process.env.SHARED_WORKER !== 'disabled') {
      if (configurationCopy.subscriptionWorkerUrl) {
        // Inject subscription worker into the transport provider stack.
        const middleware = new SubscriptionWorkerMiddleware({
          clientIdentifier: clientConfiguration._instanceId,
          subscriptionKey: clientConfiguration.subscribeKey,
          userId: clientConfiguration.getUserId(),
          workerUrl: configurationCopy.subscriptionWorkerUrl,
          sdkVersion: clientConfiguration.getVersion(),
          heartbeatInterval: clientConfiguration.getHeartbeatInterval(),
          workerOfflineClientsCheckInterval: platformConfiguration.subscriptionWorkerOfflineClientsCheckInterval!,
          workerUnsubscribeOfflineClients: platformConfiguration.subscriptionWorkerUnsubscribeOfflineClients!,
          workerLogVerbosity: platformConfiguration.subscriptionWorkerLogVerbosity!,
          tokenManager,
          transport,
          logger: clientConfiguration.logger(),
        });
        transport = middleware;

        window.onpagehide = (event: PageTransitionEvent) => {
          if (!event.persisted) middleware.terminate();
        };
      } else if (sharedWorkerRequested) {
        clientConfiguration
          .logger()
          .warn('PubNub', 'SharedWorker not supported in this browser. Fallback to the original transport.');
      }
    }

    const transportMiddleware = new PubNubMiddleware({
      clientConfiguration,
      tokenManager,
      transport,
    });

    super({
      configuration: clientConfiguration,
      transport: transportMiddleware,
      cryptography,
      tokenManager,
      crypto,
    });

    if (configuration.listenToBrowserNetworkEvents ?? true) {
      window.addEventListener('offline', () => {
        this.networkDownDetected();
      });

      window.addEventListener('online', () => {
        this.networkUpDetected();
      });
    }
  }

  private networkDownDetected() {
    this.logger.debug('PubNub', 'Network down detected');

    this.emitStatus({ category: PubNub.CATEGORIES.PNNetworkDownCategory });

    if (this._configuration.restore) this.disconnect(true);
    else this.destroy(true);
  }

  private networkUpDetected() {
    this.logger.debug('PubNub', 'Network up detected');

    this.emitStatus({ category: PubNub.CATEGORIES.PNNetworkUpCategory });
    this.reconnect();
  }
}
