import {setMaxListeners} from "node:events";
import {PrivateKey} from "@libp2p/interface";
import {Registry} from "prom-client";
import {hasher} from "@chainsafe/persistent-merkle-tree";
import {BeaconApiMethods} from "@lodestar/api/beacon/server";
import {BeaconConfig} from "@lodestar/config";
import type {LoggerNode} from "@lodestar/logger/node";
import {ZERO_HASH_HEX} from "@lodestar/params";
import {IBeaconStateView, PubkeyCache, isStatePostBellatrix, isStatePostGloas} from "@lodestar/state-transition";
import {phase0} from "@lodestar/types";
import {sleep, toRootHex} from "@lodestar/utils";
import {ProcessShutdownCallback} from "@lodestar/validator";
import {BeaconRestApiServer, getApi} from "../api/index.js";
import {BeaconChain, IBeaconChain, initBeaconMetrics} from "../chain/index.js";
import {ValidatorMonitor, createValidatorMonitor} from "../chain/validatorMonitor.js";
import {IBeaconDb} from "../db/index.js";
import {initializeExecutionBuilder, initializeExecutionEngine} from "../execution/index.js";
import {HttpMetricsServer, Metrics, createMetrics, getHttpMetricsServer} from "../metrics/index.js";
import {MonitoringService} from "../monitoring/index.js";
import {Network, getReqRespHandlers} from "../network/index.js";
import {BackfillSync} from "../sync/backfill/index.js";
import {BeaconSync, IBeaconSync} from "../sync/index.js";
import {Clock} from "../util/clock.js";
import {runNodeNotifier} from "./notifier.js";
import {IBeaconNodeOptions} from "./options.js";

export * from "./options.js";

export type BeaconNodeModules = {
  opts: IBeaconNodeOptions;
  config: BeaconConfig;
  db: IBeaconDb;
  metrics: Metrics | null;
  validatorMonitor: ValidatorMonitor | null;
  network: Network;
  chain: IBeaconChain;
  api: BeaconApiMethods;
  sync: IBeaconSync;
  backfillSync: BackfillSync | null;
  metricsServer: HttpMetricsServer | null;
  monitoring: MonitoringService | null;
  restApi?: BeaconRestApiServer;
  controller?: AbortController;
};

export type BeaconNodeInitModules = {
  opts: IBeaconNodeOptions;
  config: BeaconConfig;
  pubkeyCache: PubkeyCache;
  db: IBeaconDb;
  logger: LoggerNode;
  processShutdownCallback: ProcessShutdownCallback;
  privateKey: PrivateKey;
  dataDir: string;
  peerStoreDir?: string;
  anchorState: IBeaconStateView;
  isAnchorStateFinalized: boolean;
  wsCheckpoint?: phase0.Checkpoint;
  metricsRegistries?: Registry[];
};

export enum BeaconNodeStatus {
  started = "started",
  closing = "closing",
  closed = "closed",
}

enum LoggerModule {
  api = "api",
  backfill = "backfill",
  chain = "chain",
  execution = "execution",
  metrics = "metrics",
  monitoring = "monitoring",
  network = "network",
  /** validator monitor */
  vmon = "vmon",
  rest = "rest",
  sync = "sync",
}

/**
 * Short delay before closing db to give async operations sufficient time to complete
 * and prevent "Database is not open" errors when shutting down beacon node.
 */
const DELAY_BEFORE_CLOSING_DB_MS = 500;

/**
 * The main Beacon Node class.  Contains various components for getting and processing data from the
 * Ethereum Consensus ecosystem as well as systems for getting beacon node metadata.
 */
export class BeaconNode {
  opts: IBeaconNodeOptions;
  config: BeaconConfig;
  db: IBeaconDb;
  metrics: Metrics | null;
  metricsServer: HttpMetricsServer | null;
  monitoring: MonitoringService | null;
  validatorMonitor: ValidatorMonitor | null;
  network: Network;
  chain: IBeaconChain;
  api: BeaconApiMethods;
  restApi?: BeaconRestApiServer;
  sync: IBeaconSync;
  backfillSync: BackfillSync | null;

  status: BeaconNodeStatus;
  private controller?: AbortController;

  constructor({
    opts,
    config,
    db,
    metrics,
    metricsServer,
    monitoring,
    validatorMonitor,
    network,
    chain,
    api,
    restApi,
    sync,
    backfillSync,
    controller,
  }: BeaconNodeModules) {
    this.opts = opts;
    this.config = config;
    this.metrics = metrics;
    this.metricsServer = metricsServer;
    this.monitoring = monitoring;
    this.validatorMonitor = validatorMonitor;
    this.db = db;
    this.chain = chain;
    this.api = api;
    this.restApi = restApi;
    this.network = network;
    this.sync = sync;
    this.backfillSync = backfillSync;
    this.controller = controller;

    this.status = BeaconNodeStatus.started;
  }

  /**
   * Initialize a beacon node.  Initializes and `start`s the varied sub-component services of the
   * beacon node
   */
  static async init<T extends BeaconNode = BeaconNode>({
    opts,
    config,
    pubkeyCache,
    db,
    logger,
    processShutdownCallback,
    privateKey,
    dataDir,
    peerStoreDir,
    anchorState,
    isAnchorStateFinalized,
    wsCheckpoint,
    metricsRegistries = [],
  }: BeaconNodeInitModules): Promise<T> {
    if (hasher.name !== "hashtree") {
      logger.warn(`hashtree is not supported, using hasher ${hasher.name}`);
    }

    const controller = new AbortController();
    // We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10
    // Since it is perfectly fine to have listeners > 10
    setMaxListeners(Infinity, controller.signal);
    const signal = controller.signal;

    let metrics = null;
    if (
      opts.metrics.enabled ||
      // monitoring relies on metrics data
      opts.monitoring.endpoint
    ) {
      metrics = createMetrics(opts.metrics, anchorState.genesisTime, metricsRegistries);
      initBeaconMetrics(metrics, anchorState);
      // Since the db is instantiated before this, metrics must be injected manually afterwards
      db.setMetrics(metrics.db);
      signal.addEventListener("abort", metrics.close, {once: true});
    }

    const validatorMonitor =
      opts.metrics.enabled || opts.validatorMonitor.validatorMonitorLogs
        ? createValidatorMonitor(
            metrics?.register ?? null,
            config,
            anchorState.genesisTime,
            logger.child({module: LoggerModule.vmon}),
            opts.validatorMonitor
          )
        : null;

    const clock = new Clock({config, genesisTime: anchorState.genesisTime, signal});

    // Prune hot db repos
    // TODO: Should this call be awaited?
    await db.pruneHotDb();

    // Delete deprecated eth1 data to free up disk space for users
    logger.debug("Deleting deprecated eth1 data from database");
    const startTime = Date.now();
    db.deleteDeprecatedEth1Data()
      .then(() => {
        logger.debug("Deleted deprecated eth1 data", {durationMs: Date.now() - startTime});
      })
      .catch((e) => {
        logger.error("Failed to delete deprecated eth1 data", {}, e);
      });

    const monitoring = opts.monitoring.endpoint
      ? new MonitoringService(
          "beacon",
          {...opts.monitoring, endpoint: opts.monitoring.endpoint},
          {register: (metrics as Metrics).register, logger: logger.child({module: LoggerModule.monitoring})}
        )
      : null;

    let executionEngineOpts = opts.executionEngine;
    if (opts.executionEngine.mode === "mock") {
      const latestEth1BlockHash =
        isStatePostBellatrix(anchorState) && anchorState.isExecutionStateType
          ? isStatePostGloas(anchorState)
            ? toRootHex(anchorState.latestBlockHash)
            : toRootHex(anchorState.latestExecutionPayloadHeader.blockHash)
          : undefined;
      executionEngineOpts = {
        ...opts.executionEngine,
        genesisBlockHash: ZERO_HASH_HEX,
        eth1BlockHash: opts.executionEngine.eth1BlockHash ?? latestEth1BlockHash,
        genesisTime: anchorState.genesisTime,
        config,
      };
    }

    const chain = new BeaconChain(opts.chain, {
      privateKey,
      config,
      clock,
      pubkeyCache,
      dataDir,
      db,
      dbName: opts.db.name,
      logger: logger.child({module: LoggerModule.chain}),
      processShutdownCallback,
      metrics,
      validatorMonitor,
      anchorState,
      isAnchorStateFinalized,
      executionEngine: initializeExecutionEngine(executionEngineOpts, {
        metrics,
        signal,
        logger: logger.child({module: LoggerModule.execution}),
      }),
      executionBuilder: opts.executionBuilder.enabled
        ? initializeExecutionBuilder(opts.executionBuilder, config, metrics, logger)
        : undefined,
    });

    // Load persisted data from disk to in-memory caches
    await chain.init();

    // Network needs to be initialized before the sync
    // See https://github.com/ChainSafe/lodestar/issues/4543
    const network = await Network.init({
      opts: opts.network,
      config,
      logger: logger.child({module: LoggerModule.network}),
      metrics,
      chain,
      db,
      privateKey,
      peerStoreDir,
      getReqRespHandler: getReqRespHandlers({db, chain}),
    });

    const sync = new BeaconSync(opts.sync, {
      config,
      db,
      chain,
      metrics,
      network,
      wsCheckpoint,
      logger: logger.child({module: LoggerModule.sync}),
    });

    const backfillSync =
      opts.sync.backfillBatchSize > 0
        ? await BackfillSync.init(opts.sync, {
            config,
            db,
            chain,
            metrics,
            network,
            wsCheckpoint,
            anchorState,
            logger: logger.child({module: LoggerModule.backfill}),
            signal,
          })
        : null;

    const api = getApi(opts.api, {
      config,
      logger: logger.child({module: LoggerModule.api}),
      db,
      sync,
      network,
      chain,
      metrics,
    });

    // only start server if metrics are explicitly enabled
    const metricsServer = opts.metrics.enabled
      ? await getHttpMetricsServer(opts.metrics, {
          register: (metrics as Metrics).register,
          getOtherMetrics: async () => Promise.all([network.scrapeMetrics(), chain.archiveStore.scrapeMetrics()]),
          logger: logger.child({module: LoggerModule.metrics}),
        })
      : null;

    const restApi = new BeaconRestApiServer(opts.api.rest, {
      config,
      logger: logger.child({module: LoggerModule.rest}),
      api,
      metrics: metrics ? metrics.apiRest : null,
    });
    if (opts.api.rest.enabled) {
      await restApi.registerRoutes(opts.api.version);
      await restApi.listen();
    }

    void runNodeNotifier({network, chain, sync, config, logger, signal});

    return new BeaconNode({
      opts,
      config,
      db,
      metrics,
      metricsServer,
      monitoring,
      validatorMonitor,
      network,
      chain,
      api,
      restApi,
      sync,
      backfillSync,
      controller,
    }) as T;
  }

  /**
   * Stop beacon node and its sub-components.
   */
  async close(): Promise<void> {
    if (this.status === BeaconNodeStatus.started) {
      this.status = BeaconNodeStatus.closing;
      this.sync.close();
      this.backfillSync?.close();
      if (this.restApi) await this.restApi.close();
      await this.network.close();
      if (this.metricsServer) await this.metricsServer.close();
      if (this.monitoring) await this.monitoring.close();
      await this.chain.persistToDisk();
      await this.chain.close();
      // Abort signal last: close() calls above clear intervals/timeouts so no new
      // operations get scheduled. If we aborted first, a still-pending interval could
      // fire and schedule a new operation after abort, leaving it stuck and delaying shutdown.
      if (this.controller) this.controller.abort();
      await sleep(DELAY_BEFORE_CLOSING_DB_MS);
      await this.db.close();
      this.status = BeaconNodeStatus.closed;
    }
  }
}
