import { Binary, Enum } from 'polkadot-api';
import { Subject, Subscription, from, switchMap, withLatestFrom, firstValueFrom, combineLatest } from 'rxjs';
import { finalizedAndPruned$ } from './chain.js';
import { getCallCodec } from './codecs.js';
import { runRuntimeCall } from './executor.js';
import { logger } from './logger.js';

const log = logger.child({ module: "txPool" });
const createTxPool = (chainP) => {
  const blocksTxPool = {};
  const txAdded$ = new Subject();
  const validateTx = async (block, tx) => {
    const codec = await getCallCodec(
      block,
      "TaggedTransactionQueue",
      "validate_transaction"
    );
    if (!codec)
      throw new Error("TaggedTransactionQueue_validate_transaction required");
    const result = await runRuntimeCall({
      chain: await chainP,
      hash: block.hash,
      call: "TaggedTransactionQueue_validate_transaction",
      params: Binary.toHex(
        codec.args.enc([
          Enum("External", void 0),
          Binary.fromOpaque(tx),
          block.hash
        ])
      )
    });
    return codec.value.dec(result.result);
  };
  const getAliveBlocks = async () => {
    const chain = await chainP;
    const [finalized, blocks] = await firstValueFrom(
      combineLatest([chain.finalized$, chain.blocks$])
    );
    const heads = [];
    const include = (hash) => {
      const block = blocks[hash];
      heads.push(block);
      block.children.forEach(include);
    };
    include(finalized);
    return heads;
  };
  const subscription = new Subscription();
  subscription.add(
    from(Promise.resolve(chainP)).pipe(
      switchMap(
        (chain) => chain.newBlocks$.pipe(withLatestFrom(chain.blocks$))
      )
    ).subscribe(([blockHash, blocks]) => {
      const block = blocks[blockHash];
      const parentTxPool = blocksTxPool[block.parent] ?? /* @__PURE__ */ new Map();
      const txPool = blocksTxPool[block.hash] = /* @__PURE__ */ new Map();
      parentTxPool.forEach((validation, tx) => {
        if (block.body.includes(tx)) return;
        txPool.set(tx, validation);
      });
    })
  );
  subscription.add(
    from(Promise.resolve(chainP)).pipe(switchMap((chain) => finalizedAndPruned$(chain))).subscribe(
      ({ pruned }) => pruned.forEach((hash) => {
        const txPool = blocksTxPool[hash];
        if (!txPool) return;
        txPool.clear();
        delete blocksTxPool[hash];
      })
    )
  );
  return {
    async addTx(tx) {
      const blocks = await getAliveBlocks();
      blocks.forEach((block) => {
        var _a;
        const txPool = blocksTxPool[_a = block.hash] ?? (blocksTxPool[_a] = /* @__PURE__ */ new Map());
        if (txPool.has(tx)) return;
        txPool.set(
          tx,
          validateTx(block, tx).then((res) => {
            if (!res.success) {
              log.error(
                { blockHash: block.hash, reason: res.value },
                "invalid transaction"
              );
              throw res.value;
            }
            return {
              priority: res.value.priority,
              provides: new Set(res.value.provides.map(Binary.toHex)),
              requires: new Set(res.value.requires.map(Binary.toHex))
            };
          })
        );
      });
      txAdded$.next();
    },
    getTxsForBlock: async (block) => {
      const blockTxPool = blocksTxPool[block.hash];
      if (!blockTxPool) return [];
      const awaitedTxs = await Promise.all(
        blockTxPool.entries().map(async ([tx, validateP]) => {
          try {
            return { tx, validation: await validateP };
          } catch {
            return { tx, validation: null };
          }
        })
      );
      awaitedTxs.forEach((tx) => {
        if (tx.validation == null) {
          blockTxPool.delete(tx.tx);
        }
      });
      const validTxs = awaitedTxs.filter((v) => v.validation != null);
      return sortValidatedTxs(validTxs);
    },
    destroy() {
      subscription.unsubscribe();
      for (const hash in blocksTxPool) {
        blocksTxPool[hash].clear();
        delete blocksTxPool[hash];
      }
    },
    txAdded$: txAdded$.asObservable()
  };
};
const sortValidatedTxs = (txs) => {
  const result = txs.filter((tx) => tx.validation.requires.size === 0);
  let pending = txs.filter((tx) => tx.validation.requires.size > 0);
  const providedTags = {};
  const includeTags = (tx) => tx.validation.provides.forEach((tag) => {
    const p = tx.validation.priority;
    const v = providedTags[tag];
    providedTags[tag] = v == null ? p : v < p ? p : v;
  });
  result.forEach(includeTags);
  let toPromote;
  do {
    const pendingPromotion = pending.map((original) => {
      const maxPriority = [...original.validation.requires].map((req) => providedTags[req]).reduce(
        (a, b) => a == null || b == null ? void 0 : a > b ? b : a,
        BigInt(Number.MAX_SAFE_INTEGER)
      );
      if (maxPriority == null) {
        return { type: "pending", tx: original };
      }
      return {
        type: "promote",
        tx: {
          ...original,
          validation: {
            ...original.validation,
            // put it below the last tag provider
            priority: maxPriority - 1n
          }
        }
      };
    });
    toPromote = pendingPromotion.filter((pp) => pp.type === "promote").map((pp) => pp.tx);
    pending = pendingPromotion.filter((pp) => pp.type === "pending").map((pp) => pp.tx);
    toPromote.forEach((tx) => {
      includeTags(tx);
      result.push(tx);
    });
  } while (toPromote.length > 0);
  const sorted = result.sort(
    (a, b) => Number(b.validation.priority - a.validation.priority)
  );
  return sorted.map((v) => v.tx);
};

export { createTxPool };
//# sourceMappingURL=txPool.js.map
