import { Struct, u64, u32, Bytes, Variant, _void, blockHeader, Blake2256 } from '@polkadot-api/substrate-bindings';
import { Binary, Enum } from 'polkadot-api';
import { getStorageCodecs, getCallCodec, getConstant } from '../codecs.js';
import { getRuntimeVersion, runRuntimeCall } from '../executor.js';
import { logger } from '../logger.js';
import { insertValue, deleteValue, getNode } from '../storage.js';
import { paraInherentEnterInherent } from './para-enter.js';
import { setValidationDataInherent } from './set-validation-data.js';
import { getCurrentSlot } from './slot-utils.js';
import { timestampInherent } from './timestamp.js';

const log = logger.child({ module: "block-builder" });
const CODE_KEY = "0x3a636f6465";
const createBlock = async (chain, params) => {
  const parentHash = params.parent;
  const parent = chain.getBlock(parentHash);
  if (!parent) throw new Error("Block not found");
  const height = params.unsafeBlockHeight ?? parent.header.number + 1;
  const extrinsics = [
    await timestampInherent(chain, parent),
    await setValidationDataInherent(chain, parent, params.xcm),
    await paraInherentEnterInherent(chain, parent),
    ...params.transactions
  ].filter((v) => v !== null);
  const storageOverrides = Object.fromEntries(
    Object.entries(params.storage).map(([key, value]) => [
      key,
      value == null ? null : Binary.toHex(value)
    ])
  );
  const result = await buildBlock(
    chain,
    height,
    parent,
    extrinsics,
    storageOverrides,
    params.disableOnIdle
  );
  const encodedFinalHeader = Binary.fromHex(result.header);
  const finalHeader = blockHeader.dec(encodedFinalHeader);
  const blockHash = Binary.toHex(Blake2256(encodedFinalHeader));
  let newStorageRoot = parent.storageRoot;
  for (const key in result.storageDiff) {
    const binKey = Binary.fromHex(key);
    const value = result.storageDiff[key];
    if (value != null) {
      newStorageRoot = insertValue(
        newStorageRoot,
        binKey,
        binKey.length * 2,
        typeof value === "string" ? Binary.fromHex(value) : value
      );
    } else {
      newStorageRoot = deleteValue(newStorageRoot, binKey, binKey.length * 2);
    }
  }
  const codeNode = getNode(
    newStorageRoot,
    Binary.fromHex(CODE_KEY),
    CODE_KEY.length - 2
  );
  if (!codeNode?.value) {
    throw new Error("Unexpected: new block doesn't have code");
  }
  const code = codeNode.value;
  const hasNewRuntime = CODE_KEY in result.storageDiff;
  const runtime = hasNewRuntime ? await getRuntimeVersion(code) : parent.runtime;
  const block = {
    hash: blockHash,
    parent: parentHash,
    code,
    storageRoot: newStorageRoot,
    header: finalHeader,
    runtime,
    body: result.body,
    hasNewRuntime: hasNewRuntime || void 0,
    children: []
  };
  parent.children.push(blockHash);
  return block;
};
const buildBlock = async (chain, height, parent, extrinsics, storageOverrides, disableIdleHook) => {
  const parentHash = parent.hash;
  const digests = await buildNextDigests(chain, parent);
  const provisionalHeader = {
    parentHash,
    number: height,
    stateRoot: "0x0000000000000000000000000000000000000000000000000000000000000000",
    extrinsicRoot: "0x0000000000000000000000000000000000000000000000000000000000000000",
    digests
  };
  const systemNumberCodec = await getStorageCodecs(parent, "System", "Number");
  if (systemNumberCodec) {
    storageOverrides = {
      ...storageOverrides,
      [systemNumberCodec.keys.enc()]: Binary.toHex(
        systemNumberCodec.value.enc(height - 1)
      )
    };
  }
  const applyExtCodec = await getCallCodec(
    parent,
    "BlockBuilder",
    "apply_extrinsic"
  );
  if (!applyExtCodec) {
    throw new Error("BlockBuilder_apply_extrinsic API is required");
  }
  log.debug("initialise block");
  const initResponse = await runRuntimeCall({
    chain,
    hash: parentHash,
    call: "Core_initialize_block",
    params: Binary.toHex(blockHeader.enc(provisionalHeader)),
    storageOverrides
  });
  storageOverrides = {
    ...storageOverrides,
    ...Object.fromEntries(initResponse.storageDiff)
  };
  const body = [];
  for (const extrinsic of extrinsics) {
    try {
      log.debug("apply extrinsic " + Binary.toHex(extrinsic));
      const applyResponse = await runRuntimeCall({
        chain,
        hash: parentHash,
        call: "BlockBuilder_apply_extrinsic",
        params: Binary.toHex(extrinsic),
        storageOverrides,
        mockSignatureHost: true
      });
      const decodedResult = applyExtCodec.value.dec(applyResponse.result);
      if (!decodedResult.success) {
        log.info(
          {
            extrinsic: Binary.toHex(extrinsic),
            error: decodedResult.value
          },
          "excluding extrinsic from block"
        );
        continue;
      }
      body.push(extrinsic);
      storageOverrides = {
        ...storageOverrides,
        ...Object.fromEntries(applyResponse.storageDiff)
      };
    } catch (ex) {
      log.error(ex, "failed to apply extrinsic");
    }
  }
  log.debug("finalize block");
  let originalWeight;
  if (disableIdleHook) {
    try {
      const blockWeights = await getConstant(parent, "System", "BlockWeights");
      const blockWeightCodecs = await getStorageCodecs(
        parent,
        "System",
        "BlockWeight"
      );
      if (!blockWeightCodecs) throw null;
      const key = blockWeightCodecs.keys.enc();
      const value = blockWeightCodecs.value.enc({
        normal: blockWeights.max_block,
        operational: blockWeights.max_block,
        mandatory: blockWeights.max_block
      });
      originalWeight = storageOverrides[key] ? {
        key,
        value: storageOverrides[key]
      } : void 0;
      storageOverrides[key] = Binary.toHex(value);
    } catch {
    }
  }
  const finalizeResponse = await runRuntimeCall({
    chain,
    hash: parentHash,
    call: "BlockBuilder_finalize_block",
    params: "0x",
    storageOverrides
  });
  if (originalWeight) {
    storageOverrides[originalWeight.key] = originalWeight.value;
  }
  storageOverrides = {
    ...storageOverrides,
    ...Object.fromEntries(finalizeResponse.storageDiff)
  };
  return {
    header: finalizeResponse.result,
    body,
    storageDiff: storageOverrides
  };
};
const buildNextDigests = async (chain, parent) => {
  const currentSlot = await getCurrentSlot(chain, parent);
  const nextSlot = currentSlot + 1n;
  const nextSlotPayload = Binary.toHex(u64.enc(nextSlot));
  if (parent.header.digests.length === 0) {
    return [Enum("preRuntime", { engine: "aura", payload: nextSlotPayload })];
  }
  return parent.header.digests.map((digest) => {
    if (digest.type !== "preRuntime") return digest;
    if (digest.value.engine === "aura") {
      return Enum("preRuntime", {
        engine: "aura",
        payload: nextSlotPayload
      });
    }
    if (digest.value.engine === "BABE" || digest.value.engine === "babe") {
      const preDigest = BabePreDigest.dec(digest.value.payload);
      if (preDigest.type === "Unknown") return digest;
      return Enum("preRuntime", {
        engine: digest.value.engine,
        payload: Binary.toHex(
          BabePreDigest.enc({
            ...preDigest,
            value: {
              ...preDigest.value,
              slot: nextSlot
            }
          })
        )
      });
    }
    return digest;
  });
};
const DigestWithVRF = Struct({
  authority_index: u32,
  slot: u64,
  vrf_signature: Struct({
    pre_output: Bytes(32),
    proof: Bytes(64)
  })
});
const BabePreDigest = Variant({
  Unknown: _void,
  Primary: DigestWithVRF,
  SecondaryPlain: Struct({
    authority_index: u32,
    slot: u64
  }),
  SecondaryVRF: DigestWithVRF
});

export { createBlock };
//# sourceMappingURL=create-block.js.map
