import { PoolResponse } from "@kyve/proto-beta/lcd/kyve/query/v1beta1/pools";
import KyveSDK, { KyveClient, KyveLCDClientType } from "@kyve/sdk-beta";
import { Command, OptionValues } from "commander";
import os from "os";
import { Logger } from "tslog";

import { version as coreVersion } from "../package.json";
import {
  parseCache,
  parseMnemonic,
  parseNetwork,
  parsePoolId,
} from "./commander";
import {
  canPropose,
  canVote,
  claimUploaderRole,
  compressionFactory,
  continueRound,
  createBundleProposal,
  getBalances,
  runCache,
  runNode,
  saveBundleDecompress,
  saveBundleDownload,
  saveGetTransformDataItem,
  saveLoadValidationBundle,
  setupCacheProvider,
  setupLogger,
  setupMetrics,
  setupSDK,
  setupValidator,
  skipUploaderRole,
  storageProviderFactory,
  submitBundleProposal,
  syncPoolConfig,
  syncPoolState,
  validateBundleProposal,
  validateIsNodeValidator,
  validateIsPoolActive,
  validateRuntime,
  validateVersion,
  voteBundleProposal,
  waitForAuthorization,
  waitForCacheContinuation,
  waitForNextBundleProposal,
  waitForUploadInterval,
} from "./methods";
import { ICacheProvider, IMetrics, IRuntime } from "./types";
import { standardizeJSON } from "./utils";

/**
 * Main class of KYVE protocol nodes representing a node.
 *
 * @class Node
 * @constructor
 */
export class Node {
  // reactor attributes
  protected runtime!: IRuntime;
  protected cacheProvider!: ICacheProvider;

  // sdk attributes
  public sdk!: KyveSDK;
  public client!: KyveClient;
  public lcd!: KyveLCDClientType;

  // node attributes
  public coreVersion!: string;
  public pool!: PoolResponse;
  public poolConfig!: any;
  public name!: string;

  // logger attributes
  public logger!: Logger;

  // metrics attributes
  public m!: IMetrics;

  // node option attributes
  protected poolId!: number;
  protected staker!: string;
  protected valaccount!: string;
  protected storagePriv!: string;
  protected network!: string;
  protected rpc!: string;
  protected rest!: string;
  protected cache!: string;
  protected debug!: boolean;
  protected metrics!: boolean;
  protected metricsPort!: number;
  protected home!: string;

  // setups
  protected setupLogger = setupLogger;
  protected setupCacheProvider = setupCacheProvider;
  protected setupMetrics = setupMetrics;
  protected setupSDK = setupSDK;
  protected setupValidator = setupValidator;

  // checks
  protected validateRuntime = validateRuntime;
  protected validateVersion = validateVersion;
  protected validateIsNodeValidator = validateIsNodeValidator;
  protected validateIsPoolActive = validateIsPoolActive;

  // timeouts
  protected waitForAuthorization = waitForAuthorization;
  protected waitForUploadInterval = waitForUploadInterval;
  protected waitForNextBundleProposal = waitForNextBundleProposal;
  protected waitForCacheContinuation = waitForCacheContinuation;

  // helpers
  protected continueRound = continueRound;
  protected saveGetTransformDataItem = saveGetTransformDataItem;

  // factories
  protected storageProviderFactory = storageProviderFactory;
  protected compressionFactory = compressionFactory;

  // txs
  protected claimUploaderRole = claimUploaderRole;
  protected skipUploaderRole = skipUploaderRole;
  protected voteBundleProposal = voteBundleProposal;
  protected submitBundleProposal = submitBundleProposal;

  // queries
  protected syncPoolState = syncPoolState;
  protected syncPoolConfig = syncPoolConfig;
  protected getBalances = getBalances;
  protected canVote = canVote;
  protected canPropose = canPropose;

  // validate
  protected saveBundleDownload = saveBundleDownload;
  protected saveBundleDecompress = saveBundleDecompress;
  protected saveLoadValidationBundle = saveLoadValidationBundle;
  protected validateBundleProposal = validateBundleProposal;

  // upload
  protected createBundleProposal = createBundleProposal;

  // main
  protected runNode = runNode;
  protected runCache = runCache;

  /**
   * Constructor for the core class. It is required to provide the
   * runtime class here in order to run the
   *
   * @method constructor
   * @param {IRuntime} runtime which implements the interface IRuntime
   */
  constructor(runtime: IRuntime) {
    // set provided runtime
    this.runtime = runtime;

    // set @kyve/core version
    this.coreVersion = coreVersion;
  }

  /**
   * Bootstrap method for protocol node. It initializes all commands including
   * the main program which can be called with "start"
   *
   * @method bootstrap
   * @return {void}
   */
  public bootstrap(): void {
    // define main program
    const program = new Command();

    // define version command
    program
      .command("version")
      .description("Print runtime and core version")
      .action(() => {
        console.log(`${this.runtime.name} version: ${this.runtime.version}`);
        console.log(`@kyve/core version: ${this.coreVersion}`);
        console.log(`Node version: ${process.version}`);
        console.log();
        console.log(`Platform: ${os.platform()}`);
        console.log(`Arch: ${os.arch()}`);
      });

    // define start command
    program
      .command("start")
      .description("Run the protocol node")
      .requiredOption(
        "--pool <string>",
        "The ID of the pool this valaccount should participate as a validator",
        parsePoolId
      )
      .requiredOption(
        "--valaccount <string>",
        "The mnemonic of the valaccount",
        parseMnemonic
      )
      .requiredOption(
        "--storage-priv <string>",
        "The private key of the storage provider"
      )
      .requiredOption(
        "--network <local|alpha|beta|korellia>",
        "The network of the KYVE chain",
        parseNetwork
      )
      .option(
        "--rpc",
        "Custom rpc endpoint the node uses for submitting transactions to chain"
      )
      .option(
        "--rest",
        "Custom rest api endpoint the node uses for querying from chain"
      )
      .option(
        "--cache <memory|jsonfile|leveldb>",
        "The cache this node should use",
        parseCache,
        "leveldb"
      )
      .option("--debug", "Run the validator node in debug mode")
      .option(
        "--verbose",
        "[DEPRECATED] Run the validator node in verbose logging mode"
      )
      .option(
        "--metrics",
        "Start a prometheus metrics server on http://localhost:8080/metrics"
      )
      .option(
        "--metrics-port <number>",
        "Specify the port of the metrics server. Only considered if '--metrics' is set [default = 8080]",
        "8080"
      )
      .option(
        "--home <string>",
        "Specify the home directory of the node where logs and the cache should save their data. [default current directory]",
        "./"
      )
      .action((options) => {
        this.start(options);
      });

    // bootstrap program
    program.parse();
  }

  /**
   * Main method of @kyve/core. By running this method the node will start and run.
   * For this method to run the Runtime, Storage Provider and the Cache have to be added first.
   *
   * This method will run indefinetely and only exits on specific exit conditions like running
   * an incorrect runtime or version.
   *
   * @method start
   * @param {OptionValues} options contains all node options defined in bootstrap
   * @return {Promise<void>}
   */
  private async start(options: OptionValues): Promise<void> {
    // assign program options
    // to node instance
    this.poolId = options.pool;
    this.valaccount = options.valaccount;
    this.storagePriv = options.storagePriv;
    this.network = options.network;
    this.rpc = options.rpc;
    this.rest = options.rest;
    this.cache = options.cache;
    this.debug = options.debug;
    this.metrics = options.metrics;
    this.metricsPort = options.metricsPort;
    this.home = options.home;

    // perform setups
    this.setupLogger();
    this.setupMetrics();

    // perform async setups
    await this.setupSDK();
    await this.setupValidator();
    await this.setupCacheProvider();

    // start the node process. Node and cache should run at the same time.
    // Thats why, although they are async they are called synchronously
    try {
      await this.syncPoolState();

      this.runNode();
      this.runCache();
    } catch (err) {
      this.logger.fatal(`Unexpected runtime error. Exiting ...`);
      this.logger.fatal(standardizeJSON(err));

      process.exit(1);
    }
  }
}

// export commander
export * from "./commander";

// export types
export * from "./types";

// export utils
export * from "./utils";
