import { Binary } from 'polkadot-api';
import { BehaviorSubject, Subject, filter, pairwise, withLatestFrom, map } from 'rxjs';
import { createBlock } from './block-builder/create-block.js';
import { setBlockMeta } from './codecs.js';
import { logger } from './logger.js';
import { insertValue, createRoot, getDiff, getDescendantNodes, getNode, forEachDescendant, getSoftDeletedDescendantKeys, deleteValue } from './storage.js';

const log = logger.child({ module: "chain" });
const CODE_KEY = "0x3a636f6465";
const createChain = (source, executor) => {
  const blocks$ = new BehaviorSubject({});
  const newBlocks$ = new Subject();
  const bestSrc$ = new BehaviorSubject(null);
  const best$ = bestSrc$.pipe(filter((v) => v != null));
  const finalizedSrc$ = new BehaviorSubject(null);
  const finalized$ = finalizedSrc$.pipe(filter((v) => v != null));
  const asyncInitialBlock = source.block.then(async (block) => {
    log.debug("loading code");
    const code = await source.getStorage(CODE_KEY);
    if (!code) {
      throw new Error("No runtime code found at source block");
    }
    log.debug("code loaded, getting runtime");
    const initialRuntime = await executor.getRuntimeVersion(code);
    log.debug("runtime loaded");
    const result = {
      hash: block.blockHash,
      parent: "0x0000000000000000000000000000000000000000000000000000000000000000",
      header: block.header,
      body: block.body,
      code,
      storageRoot: insertValue(
        createRoot(),
        Binary.fromHex(CODE_KEY),
        CODE_KEY.length - 2,
        code
      ),
      runtime: initialRuntime,
      children: []
    };
    blocks$.next({
      [result.hash]: result
    });
    setBlockMeta(chain, result);
    bestSrc$.next(result.hash);
    finalizedSrc$.next(result.hash);
    return result;
  });
  asyncInitialBlock.catch((ex) => {
    log.error(ex);
  });
  const getBlock = (hash) => blocks$.getValue()[hash];
  const assertBlock = (hash) => {
    const block = getBlock(hash);
    if (!block) {
      throw new Error(`Block not found`);
    }
    return block;
  };
  const isDescendant = (parentHash, descendantHash) => {
    const parent = getBlock(parentHash);
    let block = getBlock(descendantHash);
    while (block.header.number > parent.header.number) {
      block = getBlock(block.parent);
    }
    return block.hash === parent.hash;
  };
  const assertFinalizedDescendant = (hash) => {
    if (!isDescendant(finalizedSrc$.getValue(), hash)) {
      throw new Error(`Block is not a descendant of finalized`);
    }
  };
  const changeFinalized = (hash) => {
    assertBlock(hash);
    assertFinalizedDescendant(hash);
    if (!isDescendant(hash, bestSrc$.getValue())) {
      bestSrc$.next(hash);
    }
    finalizedSrc$.next(hash);
  };
  const setStorage = (hash, changes) => {
    const block = assertBlock(hash);
    for (const key in changes) {
      const binKey = Binary.fromHex(key);
      if (changes[key]) {
        block.storageRoot = insertValue(
          block.storageRoot,
          binKey,
          binKey.length * 2,
          changes[key]
        );
      } else {
        block.storageRoot = deleteValue(
          block.storageRoot,
          binKey,
          binKey.length * 2
        );
      }
    }
  };
  const getStorage = async (hash, key) => {
    const initialBlock = await asyncInitialBlock;
    const block = assertBlock(hash);
    const binKey = Binary.fromHex(key);
    const node = getNode(block.storageRoot, binKey, binKey.length * 2) ?? // The initialBlock's storage might mutate as data is loaded from source
    // and because of the immutable storage structure, newer blocks won't see that state.
    getNode(initialBlock.storageRoot, binKey, binKey.length * 2);
    if (node?.value !== void 0) {
      return node;
    }
    const sourceResult = await source.getStorage(key);
    initialBlock.storageRoot = insertValue(
      initialBlock.storageRoot,
      binKey,
      binKey.length * 2,
      sourceResult
    );
    return getNode(initialBlock.storageRoot, binKey, binKey.length * 2);
  };
  const getStorageBatch = async (hash, keys) => {
    const initialBlock = await asyncInitialBlock;
    const block = assertBlock(hash);
    const keysIndexed = keys.map((key, idx) => ({ key, idx }));
    const pending = new Array();
    const result = keysIndexed.map(({ key, idx }) => {
      const binKey = Binary.fromHex(key);
      const node = getNode(block.storageRoot, binKey, binKey.length * 2) ?? // The initialBlock's storage might mutate as data is loaded from source
      // and because of the immutable storage structure, newer blocks won't see that state.
      getNode(initialBlock.storageRoot, binKey, binKey.length * 2);
      if (node?.value !== void 0) {
        return node;
      }
      pending.push({ key, idx, binKey });
      return null;
    });
    const loadedResults = await source.getStorageBatch(
      pending.map(({ key }) => key)
    );
    loadedResults.forEach((res, i) => {
      const { idx, binKey } = pending[i];
      initialBlock.storageRoot = insertValue(
        initialBlock.storageRoot,
        binKey,
        binKey.length * 2,
        res
      );
      result[idx] = getNode(
        initialBlock.storageRoot,
        binKey,
        binKey.length * 2
      );
    });
    return result;
  };
  const getStorageDescendants = async (hash, prefix) => {
    const initialBlock = await asyncInitialBlock;
    const block = assertBlock(hash);
    const binPrefix = Binary.fromHex(prefix);
    const getNodeDescendants = (node) => node ? Object.fromEntries(
      getDescendantNodes(node, binPrefix, binPrefix.length * 2).map(
        ({ key, node: node2 }) => [Binary.toHex(key), node2]
      )
    ) : {};
    const blockNode = getNode(
      block.storageRoot,
      binPrefix,
      binPrefix.length * 2
    );
    if (blockNode?.exhaustive) {
      return getNodeDescendants(blockNode);
    }
    let rootNode = getNode(
      initialBlock.storageRoot,
      binPrefix,
      binPrefix.length * 2
    );
    if (!rootNode?.exhaustive) {
      const sourceDescendants = await source.getStorageDescendants(prefix);
      if (!Object.keys(sourceDescendants).length)
        initialBlock.storageRoot = insertValue(
          initialBlock.storageRoot,
          binPrefix,
          binPrefix.length * 2,
          null
        );
      for (const key in sourceDescendants) {
        const binKey = Binary.fromHex(key);
        initialBlock.storageRoot = insertValue(
          initialBlock.storageRoot,
          binKey,
          binKey.length * 2,
          sourceDescendants[key]
        );
      }
      rootNode = getNode(
        initialBlock.storageRoot,
        binPrefix,
        binPrefix.length * 2
      );
      rootNode.exhaustive = true;
      forEachDescendant(rootNode, (node) => node.exhaustive = true);
    }
    const deletedKeys = blockNode ? new Set(
      getSoftDeletedDescendantKeys(
        blockNode,
        binPrefix,
        binPrefix.length * 2
      ).map(Binary.toHex)
    ) : null;
    const rootDescendants = getNodeDescendants(rootNode);
    return {
      ...deletedKeys ? Object.fromEntries(
        Object.entries(rootDescendants).filter(
          ([key]) => !deletedKeys.has(key)
        )
      ) : rootDescendants,
      ...getNodeDescendants(blockNode)
    };
  };
  const getStorageDiff = async (hash, baseHash) => {
    const target = getBlock(hash);
    if (!target) {
      throw new Error(`Block not found`);
    }
    const base = getBlock(baseHash ?? target.parent);
    if (!base) {
      if (baseHash) {
        throw new Error(`Base block not found`);
      }
      return {};
    }
    const diff = getDiff(
      base.storageRoot,
      (await asyncInitialBlock).storageRoot,
      target.storageRoot
    );
    const prevs = Object.fromEntries(
      getDescendantNodes(diff.prev, new Uint8Array(), 0).map(
        ({ key, node }) => [Binary.toHex(key), node.value]
      )
    );
    const inserts = getDescendantNodes(diff.insert, new Uint8Array(), 0).map(
      ({ key, node }) => [Binary.toHex(key), node.value]
    );
    const deletes = diff.deleteValues.map(
      ({ key }) => [Binary.toHex(key), null]
    );
    return Object.fromEntries(
      [...inserts, ...deletes].map(([key, value]) => [
        key,
        { value, prev: prevs[key] }
      ])
    );
  };
  const newBlock = async (params) => {
    assertBlock(params.parent);
    assertFinalizedDescendant(params.parent);
    const block = await createBlock(chain, params);
    const existingBlock = getBlock(block.hash);
    if (existingBlock) {
      logger.info(`Discarding new block ${block.hash}: it already exists`);
      return existingBlock;
    }
    assertFinalizedDescendant(params.parent);
    blocks$.next({
      ...blocks$.getValue(),
      [block.hash]: block
    });
    setBlockMeta(chain, block);
    newBlocks$.next(block.hash);
    const previousBestHash = bestSrc$.getValue();
    const previousBest = previousBestHash && blocks$.getValue()[previousBestHash];
    if (!previousBest || previousBest.header.number < block.header.number) {
      bestSrc$.next(block.hash);
    }
    return block;
  };
  const hrmpEgressChannels = /* @__PURE__ */ new Set();
  const chain = {
    blocks$: blocks$.asObservable(),
    newBlocks$: newBlocks$.asObservable(),
    best$,
    finalized$,
    executor,
    getBlock,
    newBlock,
    changeFinalized,
    setStorage,
    getStorage,
    getStorageBatch,
    getStorageDescendants,
    getStorageDiff,
    hrmpChannels: hrmpEgressChannels,
    openHrmpChannel: (recipientParaId) => hrmpEgressChannels.add(recipientParaId)
  };
  return chain;
};
const finalizedAndPruned$ = (chain) => chain.finalized$.pipe(
  pairwise(),
  withLatestFrom(chain.blocks$),
  map(([[prev, next], blocks]) => {
    const finalized = [next];
    let blockHash = next;
    while (blocks[blockHash]?.parent !== prev && blocks[blockHash]?.parent in blocks) {
      blockHash = blocks[blockHash].parent;
      finalized.push(blockHash);
    }
    finalized.reverse();
    const pruned = [];
    const pruneBranch = (hash) => {
      const block = blocks[hash];
      if (!block) return;
      pruned.push(hash);
      block.children.forEach(pruneBranch);
    };
    let i = 0;
    blockHash = prev;
    while (blockHash !== next) {
      const block = blocks[blockHash];
      for (const child of block.children) {
        if (child === finalized[i]) continue;
        pruneBranch(child);
      }
      blockHash = finalized[i];
      i++;
    }
    return {
      finalized,
      pruned
    };
  })
);

export { createChain, finalizedAndPruned$ };
//# sourceMappingURL=chain.js.map
