import {ChainForkConfig} from "@lodestar/config";
import {IForkChoice, ProtoBlock, getSafeExecutionBlockHash} from "@lodestar/fork-choice";
import {
  BUILDER_INDEX_SELF_BUILD,
  ForkName,
  ForkPostBellatrix,
  ForkPostCapella,
  ForkPostDeneb,
  ForkPostFulu,
  ForkPostGloas,
  ForkPreGloas,
  ForkSeq,
  isForkPostAltair,
  isForkPostBellatrix,
  isForkPostGloas,
} from "@lodestar/params";
import {
  G2_POINT_AT_INFINITY,
  IBeaconStateView,
  type IBeaconStateViewBellatrix,
  computeTimeAtSlot,
  isStatePostBellatrix,
  isStatePostCapella,
  isStatePostGloas,
} from "@lodestar/state-transition";
import {
  BLSPubkey,
  BLSSignature,
  BeaconBlock,
  BeaconBlockBody,
  BlindedBeaconBlock,
  BlindedBeaconBlockBody,
  BlobsBundle,
  Bytes32,
  ExecutionPayload,
  ExecutionPayloadHeader,
  Root,
  RootHex,
  SSEPayloadAttributes,
  Slot,
  ValidatorIndex,
  Wei,
  altair,
  capella,
  deneb,
  electra,
  fulu,
  gloas,
  ssz,
} from "@lodestar/types";
import {Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
import {ZERO_HASH_HEX} from "../../constants/index.js";
import {numToQuantity} from "../../execution/engine/utils.js";
import {
  IExecutionBuilder,
  IExecutionEngine,
  PayloadAttributes,
  PayloadId,
  getExpectedGasLimit,
} from "../../execution/index.js";
import {fromGraffitiBytes} from "../../util/graffiti.js";
import {kzg} from "../../util/kzg.js";
import type {BeaconChain} from "../chain.js";
import {CommonBlockBody} from "../interface.js";
import {validateBlobsAndKzgCommitments, validateCellsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js";

// Time to provide the EL to generate a payload from new payload id
const PAYLOAD_GENERATION_TIME_MS = 500;

export enum PayloadPreparationType {
  Fresh = "Fresh",
  Cached = "Cached",
  Reorged = "Reorged",
  Blinded = "Blinded",
}

/**
 * Block production steps tracked in metrics
 */
export enum BlockProductionStep {
  proposerSlashing = "proposerSlashing",
  attesterSlashings = "attesterSlashings",
  voluntaryExits = "voluntaryExits",
  blsToExecutionChanges = "blsToExecutionChanges",
  attestations = "attestations",
  syncAggregate = "syncAggregate",
  executionPayload = "executionPayload",
}

export type BlockAttributes = {
  randaoReveal: BLSSignature;
  graffiti: Bytes32;
  slot: Slot;
  parentBlock: ProtoBlock;
  feeRecipient?: string;
};

export enum BlockType {
  Full = "Full",
  Blinded = "Blinded",
}
export type AssembledBodyType<T extends BlockType> = T extends BlockType.Full
  ? BeaconBlockBody
  : BlindedBeaconBlockBody;
export type AssembledBlockType<T extends BlockType> = T extends BlockType.Full ? BeaconBlock : BlindedBeaconBlock;

export type ProduceFullGloas = {
  type: BlockType.Full;
  fork: ForkPostGloas;
  executionPayload: ExecutionPayload<ForkPostGloas>;
  executionRequests: electra.ExecutionRequests;
  blobsBundle: BlobsBundle<ForkPostGloas>;
  cells: fulu.Cell[][];
  parentBlockRoot: Root;
};
export type ProduceFullFulu = {
  type: BlockType.Full;
  fork: ForkPostFulu;
  executionPayload: ExecutionPayload<ForkPostFulu>;
  blobsBundle: BlobsBundle<ForkPostFulu>;
  cells: fulu.Cell[][];
};
export type ProduceFullDeneb = {
  type: BlockType.Full;
  fork: ForkName.deneb | ForkName.electra;
  executionPayload: ExecutionPayload<ForkPostDeneb>;
  blobsBundle: BlobsBundle<ForkPostDeneb>;
};
export type ProduceFullBellatrix = {
  type: BlockType.Full;
  fork: ForkName.bellatrix | ForkName.capella;
  executionPayload: ExecutionPayload<ForkPostBellatrix>;
};
export type ProduceFullPhase0 = {
  type: BlockType.Full;
  fork: ForkName.phase0 | ForkName.altair;
};
export type ProduceBlinded = {
  type: BlockType.Blinded;
  fork: ForkName;
};

// The results of block production returned by `produceBlockBody`
// The types are defined separately so typecasting can be used

/** The result of local block production, everything that's not the block itself */
export type ProduceResult =
  | ProduceFullGloas
  | ProduceFullFulu
  | ProduceFullDeneb
  | ProduceFullBellatrix
  | ProduceFullPhase0
  | ProduceBlinded;

export async function produceBlockBody<T extends BlockType>(
  this: BeaconChain,
  blockType: T,
  currentState: IBeaconStateView,
  blockAttr: BlockAttributes & {
    proposerIndex: ValidatorIndex;
    proposerPubKey: BLSPubkey;
    commonBlockBodyPromise: Promise<CommonBlockBody>;
  }
): Promise<{
  body: AssembledBodyType<T>;
  produceResult: ProduceResult;
  executionPayloadValue: Wei;
  shouldOverrideBuilder?: boolean;
}> {
  const {
    slot: blockSlot,
    feeRecipient: requestedFeeRecipient,
    parentBlock,
    proposerIndex,
    proposerPubKey,
    commonBlockBodyPromise,
  } = blockAttr;
  let executionPayloadValue: Wei;
  let blockBody: AssembledBodyType<T>;
  const parentBlockRoot = fromHex(parentBlock.blockRoot);
  // even though shouldOverrideBuilder is relevant for the engine response, for simplicity of typing
  // we just return it undefined for the builder which anyway doesn't get consumed downstream
  let shouldOverrideBuilder: boolean | undefined;
  const fork = this.config.getForkName(blockSlot);
  const produceResult = {
    type: blockType,
    fork,
  } as ProduceResult;

  const logMeta: Record<string, string | number | bigint> = {
    fork,
    blockType,
    slot: blockSlot,
  };
  this.logger.verbose("Producing beacon block body", logMeta);

  if (isForkPostGloas(fork)) {
    if (!isStatePostGloas(currentState)) {
      throw new Error("Expected Gloas state for Gloas block production");
    }

    // TODO GLOAS: support non self-building here, the block type differentiation between
    // full and blinded no longer makes sense in gloas, it might be a good idea to move
    // this into a completely separate function and have pre/post gloas more separated
    const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice);
    const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
    const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex);

    const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer();

    this.logger.verbose("Preparing execution payload from engine", {
      slot: blockSlot,
      parentBlockRoot: toRootHex(parentBlockRoot),
      feeRecipient,
    });

    // Get execution payload from EL
    let parentBlockHash: Bytes32;
    let parentExecutionRequests: electra.ExecutionRequests;
    // Apply parent payload once here as it's reused by EL prep and voluntary exit filtering below
    let stateAfterParentPayload: IBeaconStateViewBellatrix = currentState;
    const isExtendingPayload = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot));
    if (isExtendingPayload) {
      parentBlockHash = currentState.latestExecutionPayloadBid.blockHash;
      parentExecutionRequests = await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot);
      stateAfterParentPayload = currentState.withParentPayloadApplied(parentExecutionRequests);
    } else {
      parentBlockHash = currentState.latestExecutionPayloadBid.parentBlockHash;
      parentExecutionRequests = ssz.electra.ExecutionRequests.defaultValue();
    }
    const prepareRes = await prepareExecutionPayload(
      this,
      this.logger,
      fork,
      parentBlockRoot,
      parentBlockHash,
      safeBlockHash,
      finalizedBlockHash ?? ZERO_HASH_HEX,
      stateAfterParentPayload,
      feeRecipient
    );

    const {prepType, payloadId} = prepareRes;
    Object.assign(logMeta, {executionPayloadPrepType: prepType});

    if (prepType !== PayloadPreparationType.Cached) {
      await sleep(PAYLOAD_GENERATION_TIME_MS);
    }

    this.logger.verbose("Fetching execution payload from engine", {slot: blockSlot, payloadId});
    const payloadRes = await this.executionEngine.getPayload(fork, payloadId);

    endExecutionPayload?.({step: BlockProductionStep.executionPayload});

    const {executionPayload, blobsBundle, executionRequests} = payloadRes;
    executionPayloadValue = payloadRes.executionPayloadValue;
    shouldOverrideBuilder = payloadRes.shouldOverrideBuilder;

    if (blobsBundle === undefined) {
      throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`);
    }
    if (executionRequests === undefined) {
      throw Error(`Missing executionRequests response from getPayload at fork=${fork}`);
    }

    const cells = blobsBundle.blobs.map((blob) => kzg.computeCells(blob));
    if (this.opts.sanityCheckExecutionEngineBlobs) {
      await validateCellsAndKzgCommitments(blobsBundle.commitments, blobsBundle.proofs, cells);
    }

    // Create self-build execution payload bid
    const bid: gloas.ExecutionPayloadBid = {
      parentBlockHash,
      parentBlockRoot,
      blockHash: executionPayload.blockHash,
      prevRandao: currentState.getRandaoMix(currentState.epoch),
      feeRecipient: executionPayload.feeRecipient,
      gasLimit: BigInt(executionPayload.gasLimit),
      builderIndex: BUILDER_INDEX_SELF_BUILD,
      slot: blockSlot,
      value: 0,
      executionPayment: 0,
      blobKzgCommitments: blobsBundle.commitments,
      executionRequestsRoot: ssz.electra.ExecutionRequests.hashTreeRoot(executionRequests),
    };
    const signedBid: gloas.SignedExecutionPayloadBid = {
      message: bid,
      signature: G2_POINT_AT_INFINITY,
    };

    const commonBlockBody = await commonBlockBodyPromise;
    const gloasBody = Object.assign({}, commonBlockBody) as gloas.BeaconBlockBody;
    gloasBody.signedExecutionPayloadBid = signedBid;
    gloasBody.payloadAttestations = this.payloadAttestationPool.getPayloadAttestationsForBlock(
      parentBlock.blockRoot,
      blockSlot - 1
    );
    gloasBody.parentExecutionRequests = parentExecutionRequests;
    // Drop voluntary exits that parent_execution_requests have invalidated (e.g. a withdrawal
    // request initiating an exit on the same validator). Op pool selected against the unapplied
    // state, so re-validate against the post-apply state to avoid producing an invalid block.
    if (isExtendingPayload && commonBlockBody.voluntaryExits.length > 0) {
      gloasBody.voluntaryExits = commonBlockBody.voluntaryExits.filter((signedVoluntaryExit) =>
        stateAfterParentPayload.isValidVoluntaryExit(signedVoluntaryExit, false)
      );
    }
    blockBody = gloasBody as AssembledBodyType<T>;

    // Store execution payload data required to construct execution payload envelope later
    const gloasResult = produceResult as ProduceFullGloas;
    gloasResult.executionPayload = executionPayload as ExecutionPayload<ForkPostGloas>;
    gloasResult.executionRequests = executionRequests;
    gloasResult.blobsBundle = blobsBundle;
    gloasResult.cells = cells;
    gloasResult.parentBlockRoot = fromHex(parentBlock.blockRoot);

    const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime);
    this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime);
    this.logger.verbose("Produced block with self-build bid", {
      slot: blockSlot,
      executionPayloadValue,
      prepType,
      payloadId,
      fetchedTime,
      executionBlockHash: toRootHex(executionPayload.blockHash),
      blobs: blobsBundle.commitments.length,
    });

    Object.assign(logMeta, {
      transactions: executionPayload.transactions.length,
      blobs: blobsBundle.commitments.length,
      shouldOverrideBuilder,
    });
  } else if (isForkPostBellatrix(fork)) {
    if (!isStatePostBellatrix(currentState)) {
      throw new Error("Expected Bellatrix state for execution block production");
    }

    const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice);
    const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
    const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex);
    const feeRecipientType = requestedFeeRecipient
      ? "requested"
      : this.beaconProposerCache.get(proposerIndex)
        ? "cached"
        : "default";

    Object.assign(logMeta, {feeRecipientType, feeRecipient});

    if (blockType === BlockType.Blinded) {
      if (!this.executionBuilder) throw Error("External builder not configured");
      const executionBuilder = this.executionBuilder;

      const builderPromise = (async () => {
        const endExecutionPayloadHeader = this.metrics?.builderBlockProductionTimeSteps.startTimer();
        // This path will not be used in the production, but is here just for merge mock
        // tests because merge-mock requires an fcU to be issued prior to fetch payload
        // header.
        if (executionBuilder.issueLocalFcUWithFeeRecipient !== undefined) {
          await prepareExecutionPayload(
            this,
            this.logger,
            fork,
            parentBlockRoot,
            currentState.latestExecutionPayloadHeader.blockHash,
            safeBlockHash,
            finalizedBlockHash ?? ZERO_HASH_HEX,
            currentState,
            executionBuilder.issueLocalFcUWithFeeRecipient
          );
        }

        // For MeV boost integration, this is where the execution header will be
        // fetched from the payload id and a blinded block will be produced instead of
        // fullblock for the validator to sign
        this.logger.verbose("Fetching execution payload header from builder", {
          slot: blockSlot,
          proposerPubKey: toHex(proposerPubKey),
        });
        const headerRes = await prepareExecutionPayloadHeader(this, fork, currentState, proposerPubKey);

        endExecutionPayloadHeader?.({
          step: BlockProductionStep.executionPayload,
        });

        return headerRes;
      })();

      const [builderRes, commonBlockBody] = await Promise.all([builderPromise, commonBlockBodyPromise]);
      blockBody = Object.assign({}, commonBlockBody) as AssembledBodyType<BlockType.Blinded>;

      (blockBody as BlindedBeaconBlockBody).executionPayloadHeader = builderRes.header;
      executionPayloadValue = builderRes.executionPayloadValue;

      const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime);
      const prepType = PayloadPreparationType.Blinded;
      this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime);
      this.logger.verbose("Fetched execution payload header from builder", {
        slot: blockSlot,
        executionPayloadValue,
        prepType,
        fetchedTime,
      });

      const targetGasLimit = executionBuilder.getValidatorRegistration(proposerPubKey)?.gasLimit;
      if (!targetGasLimit) {
        // This should only happen if cache was cleared due to restart of beacon node
        this.logger.warn("Failed to get validator registration, could not check header gas limit", {
          slot: blockSlot,
          proposerIndex,
          proposerPubKey: toPubkeyHex(proposerPubKey),
        });
      } else {
        const headerGasLimit = builderRes.header.gasLimit;
        const parentGasLimit = currentState.latestExecutionPayloadHeader.gasLimit;
        const expectedGasLimit = getExpectedGasLimit(parentGasLimit, targetGasLimit);

        const lowerBound = Math.min(parentGasLimit, expectedGasLimit);
        const upperBound = Math.max(parentGasLimit, expectedGasLimit);

        if (headerGasLimit < lowerBound || headerGasLimit > upperBound) {
          throw Error(
            `Header gas limit ${headerGasLimit} is outside of acceptable range [${lowerBound}, ${upperBound}]`
          );
        }

        if (headerGasLimit !== expectedGasLimit) {
          this.logger.warn("Header gas limit does not match expected value", {
            slot: blockSlot,
            headerGasLimit,
            expectedGasLimit,
            parentGasLimit,
            targetGasLimit,
          });
        }
      }

      if (ForkSeq[fork] >= ForkSeq.deneb) {
        const {blobKzgCommitments} = builderRes;
        if (blobKzgCommitments === undefined) {
          throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
        }

        (blockBody as deneb.BlindedBeaconBlockBody).blobKzgCommitments = blobKzgCommitments;
        Object.assign(logMeta, {blobs: blobKzgCommitments.length});
      }

      if (ForkSeq[fork] >= ForkSeq.electra) {
        const {executionRequests} = builderRes;
        if (executionRequests === undefined) {
          throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`);
        }
        (blockBody as electra.BlindedBeaconBlockBody).executionRequests = executionRequests;
      }
    }

    // blockType === BlockType.Full
    else {
      // enginePromise only supports pre-gloas
      const enginePromise = (async () => {
        const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer();

        this.logger.verbose("Preparing execution payload from engine", {
          slot: blockSlot,
          parentBlockRoot: toRootHex(parentBlockRoot),
          feeRecipient,
        });
        // https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/deneb/validator.md#constructing-the-beaconblockbody
        const prepareRes = await prepareExecutionPayload(
          this,
          this.logger,
          fork,
          parentBlockRoot,
          currentState.latestExecutionPayloadHeader.blockHash,
          safeBlockHash,
          finalizedBlockHash ?? ZERO_HASH_HEX,
          currentState,
          feeRecipient
        );

        const {prepType, payloadId} = prepareRes;
        Object.assign(logMeta, {executionPayloadPrepType: prepType});

        if (prepType !== PayloadPreparationType.Cached) {
          // Wait for 500ms to allow EL to add some txs to the payload
          // the pitfalls of this have been put forward here, but 500ms delay for block proposal
          // seems marginal even with unhealthy network
          //
          // See: https://discord.com/channels/595666850260713488/892088344438255616/1009882079632314469
          await sleep(PAYLOAD_GENERATION_TIME_MS);
        }

        this.logger.verbose("Fetching execution payload from engine", {slot: blockSlot, payloadId});
        const payloadRes = await this.executionEngine.getPayload(fork, payloadId);

        endExecutionPayload?.({
          step: BlockProductionStep.executionPayload,
        });

        return {...prepareRes, ...payloadRes};
      })().catch((e) => {
        this.metrics?.blockPayload.payloadFetchErrors.inc();
        throw e;
      });

      const [engineRes, commonBlockBody] = await Promise.all([enginePromise, commonBlockBodyPromise]);
      blockBody = Object.assign({}, commonBlockBody) as AssembledBodyType<BlockType.Blinded>;

      {
        const {prepType, payloadId, executionPayload, blobsBundle, executionRequests} = engineRes;
        shouldOverrideBuilder = engineRes.shouldOverrideBuilder;

        (blockBody as BeaconBlockBody<ForkPostBellatrix & ForkPreGloas>).executionPayload = executionPayload;
        (produceResult as ProduceFullBellatrix).executionPayload = executionPayload;
        executionPayloadValue = engineRes.executionPayloadValue;
        Object.assign(logMeta, {transactions: executionPayload.transactions.length, shouldOverrideBuilder});

        const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime);
        this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime);
        this.logger.verbose("Fetched execution payload from engine", {
          slot: blockSlot,
          executionPayloadValue,
          prepType,
          payloadId,
          fetchedTime,
          executionHeadBlockHash: toRootHex(engineRes.executionPayload.blockHash),
        });
        if (executionPayload.transactions.length === 0) {
          this.metrics?.blockPayload.emptyPayloads.inc({prepType});
        }

        if (ForkSeq[fork] >= ForkSeq.fulu) {
          if (blobsBundle === undefined) {
            throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`);
          }
          // NOTE: Even though the fulu.BlobsBundle type is superficially the same as deneb.BlobsBundle, it is NOT.
          // In fulu, proofs are _cell_ proofs, vs in deneb they are _blob_ proofs.

          const timer = this?.metrics?.peerDas.dataColumnSidecarComputationTime.startTimer();
          const cells = blobsBundle.blobs.map((blob) => kzg.computeCells(blob));
          timer?.();
          if (this.opts.sanityCheckExecutionEngineBlobs) {
            const validationTimer = this.metrics?.peerDas.kzgVerificationDataColumnBatchTime.startTimer();
            try {
              await validateCellsAndKzgCommitments(blobsBundle.commitments, blobsBundle.proofs, cells);
            } finally {
              validationTimer?.();
            }
          }

          (blockBody as deneb.BeaconBlockBody).blobKzgCommitments = blobsBundle.commitments;
          (produceResult as ProduceFullFulu).blobsBundle = blobsBundle;
          (produceResult as ProduceFullFulu).cells = cells;

          Object.assign(logMeta, {blobs: blobsBundle.commitments.length});
        } else if (ForkSeq[fork] >= ForkSeq.deneb) {
          if (blobsBundle === undefined) {
            throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`);
          }

          if (this.opts.sanityCheckExecutionEngineBlobs) {
            await validateBlobsAndKzgCommitments(blobsBundle.commitments, blobsBundle.proofs, blobsBundle.blobs);
          }

          (blockBody as deneb.BeaconBlockBody).blobKzgCommitments = blobsBundle.commitments;
          (produceResult as ProduceFullDeneb).blobsBundle = blobsBundle;

          Object.assign(logMeta, {blobs: blobsBundle.commitments.length});
        }

        if (ForkSeq[fork] >= ForkSeq.electra) {
          if (executionRequests === undefined) {
            throw Error(`Missing executionRequests response from getPayload at fork=${fork}`);
          }
          (blockBody as electra.BeaconBlockBody).executionRequests = executionRequests;
        }
      }
    }
  } else {
    const commonBlockBody = await commonBlockBodyPromise;
    blockBody = Object.assign({}, commonBlockBody) as AssembledBodyType<T>;
    executionPayloadValue = BigInt(0);
  }

  const {graffiti, attestations, deposits, voluntaryExits, attesterSlashings, proposerSlashings} = blockBody;

  Object.assign(logMeta, {
    graffiti: fromGraffitiBytes(graffiti),
    attestations: attestations.length,
    deposits: deposits.length,
    voluntaryExits: voluntaryExits.length,
    attesterSlashings: attesterSlashings.length,
    proposerSlashings: proposerSlashings.length,
  });

  if (isForkPostAltair(fork)) {
    const {syncAggregate} = blockBody as altair.BeaconBlockBody;
    Object.assign(logMeta, {
      syncAggregateParticipants: syncAggregate.syncCommitteeBits.getTrueBitIndexes().length,
    });
  }

  if (ForkSeq[fork] >= ForkSeq.gloas) {
    const {blsToExecutionChanges, payloadAttestations} = blockBody as BeaconBlockBody<ForkPostGloas>;
    Object.assign(logMeta, {
      blsToExecutionChanges: blsToExecutionChanges.length,
      payloadAttestations: payloadAttestations.length,
    });
  } else if (ForkSeq[fork] >= ForkSeq.capella) {
    const {blsToExecutionChanges, executionPayload} = blockBody as BeaconBlockBody<ForkPostCapella & ForkPreGloas>;
    Object.assign(logMeta, {
      blsToExecutionChanges: blsToExecutionChanges.length,
    });

    // withdrawals are only available in full body
    if (blockType === BlockType.Full) {
      Object.assign(logMeta, {
        withdrawals: executionPayload.withdrawals.length,
      });
    }
  }

  Object.assign(logMeta, {executionPayloadValue});
  this.logger.verbose("Produced beacon block body", logMeta);

  return {body: blockBody as AssembledBodyType<T>, produceResult, executionPayloadValue, shouldOverrideBuilder};
}

/**
 * Produce ExecutionPayload for post-merge.
 */
export async function prepareExecutionPayload(
  chain: {
    executionEngine: IExecutionEngine;
    config: ChainForkConfig;
  },
  logger: Logger,
  fork: ForkPostBellatrix,
  parentBlockRoot: Root,
  parentBlockHash: Bytes32,
  safeBlockHash: RootHex,
  finalizedBlockHash: RootHex,
  /**
   * Post-gloas, when extending a full parent, callers must apply
   * parent execution payload first (see `withParentPayloadApplied`).
   */
  state: IBeaconStateViewBellatrix,
  suggestedFeeRecipient: string
): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> {
  const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime);
  const prevRandao = state.getRandaoMix(state.epoch);

  const payloadIdCached = chain.executionEngine.payloadIdCache.get({
    headBlockHash: toRootHex(parentBlockHash),
    finalizedBlockHash,
    timestamp: numToQuantity(timestamp),
    prevRandao: toHex(prevRandao),
    suggestedFeeRecipient,
  });

  // prepareExecutionPayload will throw error via notifyForkchoiceUpdate if
  // the EL returns Syncing on this request to prepare a payload
  // TODO: Handle only this case, DO NOT put a generic try / catch that discards all errors
  let payloadId: PayloadId | null;
  let prepType: PayloadPreparationType;

  if (payloadIdCached) {
    payloadId = payloadIdCached;
    prepType = PayloadPreparationType.Cached;
  } else {
    // If there was a payload assigned to this timestamp, it would imply that there some sort
    // of payload reorg, i.e. head, fee recipient or any other fcu param changed
    if (chain.executionEngine.payloadIdCache.hasPayload({timestamp: numToQuantity(timestamp)})) {
      prepType = PayloadPreparationType.Reorged;
    } else {
      prepType = PayloadPreparationType.Fresh;
    }

    const attributes: PayloadAttributes = preparePayloadAttributes(fork, chain, {
      prepareState: state,
      prepareSlot: state.slot,
      parentBlockRoot,
      parentBlockHash,
      feeRecipient: suggestedFeeRecipient,
    });

    payloadId = await chain.executionEngine.notifyForkchoiceUpdate(
      fork,
      toRootHex(parentBlockHash),
      safeBlockHash,
      finalizedBlockHash,
      attributes
    );
    logger.verbose("Prepared payload id from execution engine", {payloadId});
  }

  // Should never happen, notifyForkchoiceUpdate() with payload attributes always
  // returns payloadId
  if (payloadId === null) {
    throw Error("notifyForkchoiceUpdate returned payloadId null");
  }

  // We are only returning payloadId here because prepareExecutionPayload is also called from
  // prepareNextSlot, which is an advance call to execution engine to start building payload
  // Actual payload isn't produced till getPayload is called.
  return {payloadId, prepType};
}

async function prepareExecutionPayloadHeader(
  chain: {
    executionBuilder?: IExecutionBuilder;
    config: ChainForkConfig;
  },
  fork: ForkPostBellatrix,
  state: IBeaconStateViewBellatrix,
  proposerPubKey: BLSPubkey
): Promise<{
  header: ExecutionPayloadHeader;
  executionPayloadValue: Wei;
  blobKzgCommitments?: deneb.BlobKzgCommitments;
  executionRequests?: electra.ExecutionRequests;
}> {
  if (!chain.executionBuilder) {
    throw Error("executionBuilder required");
  }

  const parentHash = state.latestExecutionPayloadHeader.blockHash;
  return chain.executionBuilder.getHeader(fork, state.slot, parentHash, proposerPubKey);
}

export function getPayloadAttributesForSSE(
  fork: ForkPostBellatrix,
  chain: {
    config: ChainForkConfig;
    forkChoice: IForkChoice;
  },
  {
    prepareState,
    prepareSlot,
    parentBlockRoot,
    parentBlockHash,
    feeRecipient,
  }: {
    /**
     * Post-gloas, when extending a full parent, callers must apply
     * parent execution payload first (see `withParentPayloadApplied`).
     */
    prepareState: IBeaconStateViewBellatrix;
    prepareSlot: Slot;
    parentBlockRoot: Root;
    parentBlockHash: Bytes32;
    feeRecipient: string;
  }
): SSEPayloadAttributes {
  const payloadAttributes = preparePayloadAttributes(fork, chain, {
    prepareState,
    prepareSlot,
    parentBlockRoot,
    parentBlockHash,
    feeRecipient,
  });

  let parentBlockNumber: number;
  if (isForkPostGloas(fork)) {
    const parentBlock = chain.forkChoice.getBlockHexAndBlockHash(
      toRootHex(parentBlockRoot),
      toRootHex(parentBlockHash)
    );
    if (parentBlock?.executionPayloadBlockHash == null) {
      throw Error(`Parent block not found in fork choice root=${toRootHex(parentBlockRoot)}`);
    }
    parentBlockNumber = parentBlock.executionPayloadNumber;
  } else {
    parentBlockNumber = prepareState.payloadBlockNumber;
  }

  const ssePayloadAttributes: SSEPayloadAttributes = {
    proposerIndex: prepareState.getBeaconProposer(prepareSlot),
    proposalSlot: prepareSlot,
    parentBlockNumber,
    parentBlockRoot,
    parentBlockHash,
    payloadAttributes,
  };
  return ssePayloadAttributes;
}

function preparePayloadAttributes(
  fork: ForkPostBellatrix,
  chain: {
    config: ChainForkConfig;
  },
  {
    prepareState,
    prepareSlot,
    parentBlockRoot,
    parentBlockHash,
    feeRecipient,
  }: {
    /**
     * Post-gloas, when extending a full parent, callers must apply
     * parent execution payload first (see `withParentPayloadApplied`).
     */
    prepareState: IBeaconStateViewBellatrix;
    prepareSlot: Slot;
    parentBlockRoot: Root;
    parentBlockHash: Bytes32;
    feeRecipient: string;
  }
): SSEPayloadAttributes["payloadAttributes"] {
  const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime);
  const prevRandao = prepareState.getRandaoMix(prepareState.epoch);
  const payloadAttributes = {
    timestamp,
    prevRandao,
    suggestedFeeRecipient: feeRecipient,
  };

  if (ForkSeq[fork] >= ForkSeq.capella) {
    if (!isStatePostCapella(prepareState)) {
      throw new Error("Expected Capella state for withdrawals");
    }

    if (isStatePostGloas(prepareState)) {
      const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash);
      if (isExtendingPayload) {
        // applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch
        // here means the caller did not apply parent payload to prepareState
        if (!byteArrayEquals(prepareState.latestBlockHash, prepareState.latestExecutionPayloadBid.blockHash)) {
          throw new Error("Expected state with parent execution payload applied for withdrawals");
        }
        (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
          prepareState.getExpectedWithdrawals().expectedWithdrawals;
      } else {
        // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch
        // already deducted from CL balances but never credited on the EL (the envelope
        // was not delivered). The next payload must carry those same withdrawals to
        // restore CL/EL consistency, otherwise validators permanently lose that balance.
        (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
          prepareState.payloadExpectedWithdrawals;
      }
    } else {
      // withdrawals logic is now fork aware as it changes on electra fork post capella
      (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
        prepareState.getExpectedWithdrawals().expectedWithdrawals;
    }
  }

  if (ForkSeq[fork] >= ForkSeq.deneb) {
    (payloadAttributes as deneb.SSEPayloadAttributes["payloadAttributes"]).parentBeaconBlockRoot = parentBlockRoot;
  }

  if (ForkSeq[fork] >= ForkSeq.gloas) {
    (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).slotNumber = prepareSlot;
  }

  return payloadAttributes;
}

export async function produceCommonBlockBody<T extends BlockType>(
  this: BeaconChain,
  blockType: T,
  currentState: IBeaconStateView,
  {randaoReveal, graffiti, slot, parentBlock}: BlockAttributes
): Promise<CommonBlockBody> {
  const stepsMetrics =
    blockType === BlockType.Full
      ? this.metrics?.executionBlockProductionTimeSteps
      : this.metrics?.builderBlockProductionTimeSteps;

  const fork = this.config.getForkName(slot);

  // TODO:
  // Iterate through the naive aggregation pool and ensure all the attestations from there
  // are included in the operation pool.
  // for (const attestation of db.attestationPool.getAll()) {
  //   try {
  //     opPool.insertAttestation(attestation);
  //   } catch (e) {
  //     // Don't stop block production if there's an error, just create a log.
  //     logger.error("Attestation did not transfer to op pool", {}, e);
  //   }
  // }
  const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] =
    this.opPool.getSlashingsAndExits(currentState, blockType, this.metrics);

  const endAttestations = stepsMetrics?.startTimer();
  const attestations = this.aggregatedAttestationPool.getAttestationsForBlock(
    fork,
    this.forkChoice,
    this.shufflingCache,
    currentState
  );
  endAttestations?.({
    step: BlockProductionStep.attestations,
  });

  const blockBody: Omit<CommonBlockBody, "blsToExecutionChanges" | "syncAggregate"> = {
    randaoReveal,
    graffiti,
    // Eth1 data voting is no longer required since electra
    eth1Data: currentState.eth1Data,
    proposerSlashings,
    attesterSlashings,
    attestations,
    // Since electra, deposits are processed by the execution layer,
    // we no longer support handling deposits from earlier forks.
    deposits: [],
    voluntaryExits,
  };

  if (ForkSeq[fork] >= ForkSeq.capella) {
    (blockBody as CommonBlockBody).blsToExecutionChanges = blsToExecutionChanges;
  }

  const endSyncAggregate = stepsMetrics?.startTimer();
  if (ForkSeq[fork] >= ForkSeq.altair) {
    const parentBlockRoot = fromHex(parentBlock.blockRoot);
    const previousSlot = slot - 1;
    const syncAggregate = this.syncContributionAndProofPool.getAggregate(previousSlot, parentBlockRoot);
    this.metrics?.production.producedSyncAggregateParticipants.observe(
      syncAggregate.syncCommitteeBits.getTrueBitIndexes().length
    );
    (blockBody as CommonBlockBody).syncAggregate = syncAggregate;
  }
  endSyncAggregate?.({
    step: BlockProductionStep.syncAggregate,
  });

  return blockBody as CommonBlockBody;
}
