import {ChainForkConfig} from "@lodestar/config";
import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
import {RootHex, Slot} from "@lodestar/types";
import {toRootHex} from "@lodestar/utils";
import {IClock} from "../../util/clock.js";
import {BlockError, BlockErrorCode} from "../errors/index.js";
import {IChainOptions} from "../options.js";
import {BlockInput, ImportBlockOpts} from "./types.js";

/**
 * Verifies some early cheap sanity checks on the block before running the full state transition.
 *
 * - Parent is known to the fork-choice
 * - Check skipped slots limit
 * - check_block_relevancy()
 *   - Block not in the future
 *   - Not genesis block
 *   - Block's slot is < Infinity
 *   - Not finalized slot
 *   - Not already known
 */
export function verifyBlocksSanityChecks(
  chain: {
    forkChoice: IForkChoice;
    clock: IClock;
    config: ChainForkConfig;
    opts: IChainOptions;
    blacklistedBlocks: Map<RootHex, Slot | null>;
  },
  blocks: BlockInput[],
  opts: ImportBlockOpts
): {
  relevantBlocks: BlockInput[];
  parentSlots: Slot[];
  parentBlock: ProtoBlock | null;
} {
  if (blocks.length === 0) {
    throw Error("Empty partiallyVerifiedBlocks");
  }

  const relevantBlocks: BlockInput[] = [];
  const parentSlots: Slot[] = [];
  let parentBlock: ProtoBlock | null = null;

  for (const blockInput of blocks) {
    const {block} = blockInput;
    const blockSlot = block.message.slot;
    const blockHash = toRootHex(chain.config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message));

    if (chain.blacklistedBlocks.has(blockHash)) {
      // Blacklisting blocks via CLI flag only requires to set the block hash
      if (chain.blacklistedBlocks.get(blockHash) === null) {
        // Set actual slot observed when processing the block
        chain.blacklistedBlocks.set(blockHash, blockSlot);
      }
      throw new BlockError(block, {code: BlockErrorCode.BLACKLISTED_BLOCK});
    }

    if (chain.blacklistedBlocks.has(toRootHex(block.message.parentRoot))) {
      chain.blacklistedBlocks.set(blockHash, blockSlot);
      throw new BlockError(block, {code: BlockErrorCode.BLACKLISTED_BLOCK});
    }

    // Not genesis block
    // IGNORE if `partiallyVerifiedBlock.ignoreIfKnown`
    if (blockSlot === 0) {
      if (opts.ignoreIfKnown) {
        continue;
      }
      throw new BlockError(block, {code: BlockErrorCode.GENESIS_BLOCK});
    }

    // Not finalized slot
    // IGNORE if `partiallyVerifiedBlock.ignoreIfFinalized`
    const finalizedSlot = computeStartSlotAtEpoch(chain.forkChoice.getFinalizedCheckpoint().epoch);
    if (blockSlot <= finalizedSlot) {
      if (opts.ignoreIfFinalized) {
        continue;
      }
      throw new BlockError(block, {code: BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT, blockSlot, finalizedSlot});
    }

    const relevantLastBlock = relevantBlocks.at(-1);
    let parentBlockSlot: Slot;

    if (relevantLastBlock) {
      parentBlockSlot = relevantLastBlock.block.message.slot;
    } else {
      // When importing a block segment, only the first NON-IGNORED block must be known to the fork-choice.
      const parentRoot = toRootHex(block.message.parentRoot);
      parentBlock = chain.forkChoice.getBlockHex(parentRoot);
      if (!parentBlock) {
        throw new BlockError(block, {code: BlockErrorCode.PARENT_UNKNOWN, parentRoot});
      }
      // Parent is known to the fork-choice
      parentBlockSlot = parentBlock.slot;
    }

    // Block not in the future, also checks for infinity
    const currentSlot = chain.clock.currentSlot;
    if (blockSlot > currentSlot) {
      throw new BlockError(block, {code: BlockErrorCode.FUTURE_SLOT, blockSlot, currentSlot});
    }

    // Not already known
    // IGNORE if `partiallyVerifiedBlock.ignoreIfKnown`
    if (chain.forkChoice.hasBlockHex(blockHash)) {
      if (opts.ignoreIfKnown) {
        continue;
      }

      throw new BlockError(block, {code: BlockErrorCode.ALREADY_KNOWN, root: blockHash});
    }

    // Block is relevant
    relevantBlocks.push(blockInput);
    parentSlots.push(parentBlockSlot);
  }

  // Just assert to be over cautious and for purposes to be more explicit for someone
  // going through the code segment
  if (parentBlock === null && relevantBlocks.length > 0) {
    throw Error(`Internal error, parentBlock should not be null for relevantBlocks=${relevantBlocks.length}`);
  }

  return {relevantBlocks, parentSlots, parentBlock};
}
