import { parachain as _allDescriptors, relay as _allDescriptors$1 } from '../.papi/descriptors/dist/index.js';
import { concatMapEager } from '@polkadot-api/observable-client';
import { Tuple, Struct, bool, u32, Bytes } from '@polkadot-api/substrate-bindings';
import { Binary, CompatibilityLevel, Enum } from 'polkadot-api';
import { firstValueFrom, from, catchError, map, filter } from 'rxjs';
import { getStorageCodecs, getConstant } from './codecs.js';
import { getNode, insertValue } from './storage.js';
import { logger } from './logger.js';

const log = logger.child({ module: "xcm" });
const attachRelay = async (relayClient, parachainClient, chain, xcm) => {
  const paraId = await getConstant(
    chain.getBlock(await firstValueFrom(chain.finalized$)),
    "ParachainSystem",
    "SelfParaId"
  );
  if (paraId == null) {
    throw new Error("Could not get parachain ID");
  }
  const typedApi = relayClient.getTypedApi(_allDescriptors$1);
  const staticApis = await typedApi.getStaticApis();
  if (!staticApis.compat.query.Dmp.DownwardMessageQueues.isCompatible(
    CompatibilityLevel.Partial
  )) {
    throw new Error("Dmp queue incompatible");
  }
  const parachainApi = parachainClient.getTypedApi(_allDescriptors);
  if (!(await parachainApi.getStaticApis()).compat.query.ParachainSystem.UpwardMessages.isCompatible(
    CompatibilityLevel.Partial
  )) {
    throw new Error("UpwardMessageQueue incompatible");
  }
  log.debug("watching dmp queue");
  let lastSentAt = 0;
  typedApi.query.Dmp.DownwardMessageQueues.watchValue(paraId).pipe(
    map(({ block, value }) => ({
      block,
      messages: value.filter((v) => v.sent_at > lastSentAt)
    })),
    filter(({ messages }) => messages.length > 0)
  ).subscribe(({ block, messages }) => {
    relayClient._request("forklift_xcm_consume_dmp", [block.hash, paraId]);
    lastSentAt = messages.map((m) => m.sent_at).reduce((a, b) => Math.max(a, b));
    xcm.pushDmp(messages);
  });
  log.debug("watching ump queue");
  parachainClient.finalizedBlock$.pipe(
    concatMapEager(
      (block) => from(
        parachainApi.query.ParachainSystem.UpwardMessages.getValue({
          at: block.hash
        })
      ).pipe(
        catchError((ex) => {
          log.error(ex, "error reading upward messages");
          return [];
        })
      )
    )
  ).subscribe((messages) => {
    relayClient._request("forklift_xcm_push_ump", [
      paraId,
      messages.map(Binary.toHex)
    ]);
  });
};
const consumeDmp = async (chain, hash, paraId) => {
  const blocks = await firstValueFrom(chain.blocks$);
  const targetBlock = blocks[hash];
  if (!targetBlock) {
    throw new Error("Block not found");
  }
  const codecs = await getStorageCodecs(
    targetBlock,
    "Dmp",
    "DownwardMessageQueues"
  );
  if (!codecs) {
    throw new Error("Dmp.DownwardMessageQueues not found");
  }
  const dmqKey = codecs.keys.enc(paraId);
  const dmqBinKey = Binary.fromHex(dmqKey);
  const targetDmqNode = await chain.getStorage(hash, dmqKey);
  const targetDmq = targetDmqNode.value ? codecs.value.dec(targetDmqNode.value) : [];
  if (targetDmq.length === 0) return;
  const getDmpMessageKey = (msg) => `${msg.sent_at}_${Binary.toHex(msg.msg)}`;
  const targetDmqKeys = new Set(targetDmq.map(getDmpMessageKey));
  let blocksToUpdate = [hash];
  while (blocksToUpdate.length) {
    const hash2 = blocksToUpdate.pop();
    const block = blocks[hash2];
    blocksToUpdate = [...block.children, ...blocksToUpdate];
    const childDmqNode = getNode(
      block.storageRoot,
      dmqBinKey,
      dmqBinKey.length * 2
    );
    const childDmq = childDmqNode?.value ? codecs.value.dec(childDmqNode.value) : [];
    const newChildDmq = childDmq.filter(
      (v) => !targetDmqKeys.has(getDmpMessageKey(v))
    );
    block.storageRoot = insertValue(
      block.storageRoot,
      dmqBinKey,
      dmqBinKey.length * 2,
      codecs.value.enc(newChildDmq)
    );
  }
  log.info({ paraId, count: targetDmq.length }, "cleared DMP messages");
};
const pushUmp = async (chain, paraId, messages) => {
  if (messages.length === 0) return;
  const finalizedHash = await firstValueFrom(chain.finalized$);
  const blocks = await firstValueFrom(chain.blocks$);
  const finalizedBlock = blocks[finalizedHash];
  if (!finalizedBlock) throw new Error(" lock not found");
  const [pageCodecs, bookCodecs, serviceHeadCodecs] = await Promise.all([
    getStorageCodecs(finalizedBlock, "MessageQueue", "Pages"),
    getStorageCodecs(finalizedBlock, "MessageQueue", "BookStateFor"),
    getStorageCodecs(finalizedBlock, "MessageQueue", "ServiceHead")
  ]);
  if (!(serviceHeadCodecs && bookCodecs && pageCodecs))
    throw new Error("MessageQueue codecs not found");
  const totalMsgSize = messages.reduce((acc, p) => acc + p.length, 0);
  const lastOffset = messages.slice(0, -1).reduce((acc, msg) => acc + 5 + msg.length, 0);
  const heap = heapEncoder(messages);
  const origin = Enum("Ump", Enum("Para", paraId));
  const bookBinKey = Binary.fromHex(bookCodecs.keys.enc(origin));
  const serviceHeadBinKey = Binary.fromHex(serviceHeadCodecs.keys.enc());
  let blocksToUpdate = [finalizedHash];
  while (blocksToUpdate.length) {
    const hash = blocksToUpdate.pop();
    const block = blocks[hash];
    blocksToUpdate = [...block.children, ...blocksToUpdate];
    const existingBookNode = getNode(
      block.storageRoot,
      bookBinKey,
      bookBinKey.length * 2
    );
    const existingBook = existingBookNode?.value ? bookCodecs.value.dec(existingBookNode.value) : null;
    const wasEmpty = !existingBook || existingBook.begin === existingBook.end;
    const pageIndex = existingBook ? existingBook.end : 0;
    const pageBinKey = Binary.fromHex(pageCodecs.keys.enc(origin, pageIndex));
    const newBook = {
      begin: existingBook?.begin ?? 0,
      end: pageIndex + 1,
      count: (existingBook?.count ?? 0) + 1,
      ready_neighbours: existingBook?.ready_neighbours ?? void 0,
      message_count: (existingBook?.message_count ?? 0n) + BigInt(messages.length),
      size: (existingBook?.size ?? 0n) + BigInt(totalMsgSize)
    };
    const page = {
      remaining: messages.length,
      remaining_size: totalMsgSize,
      first_index: 0,
      first: 0,
      last: lastOffset,
      heap
    };
    block.storageRoot = insertValue(
      block.storageRoot,
      pageBinKey,
      pageBinKey.length * 2,
      pageCodecs.value.enc(page)
    );
    if (wasEmpty) {
      const serviceHeadNode = getNode(
        block.storageRoot,
        serviceHeadBinKey,
        serviceHeadBinKey.length * 2
      );
      const existingServiceHead = serviceHeadNode?.value ? serviceHeadCodecs.value.dec(serviceHeadNode.value) : null;
      if (existingServiceHead) {
        newBook.ready_neighbours = { prev: origin, next: existingServiceHead };
        const existingHeadBookBinKey = Binary.fromHex(
          bookCodecs.keys.enc(existingServiceHead)
        );
        const existingHeadBookNode = getNode(
          block.storageRoot,
          existingHeadBookBinKey,
          existingHeadBookBinKey.length * 2
        );
        const existingHeadBook = existingHeadBookNode?.value ? bookCodecs.value.dec(existingHeadBookNode.value) : null;
        if (existingHeadBook?.ready_neighbours) {
          existingHeadBook.ready_neighbours.prev = origin;
          block.storageRoot = insertValue(
            block.storageRoot,
            existingHeadBookBinKey,
            existingHeadBookBinKey.length * 2,
            bookCodecs.value.enc(existingHeadBook)
          );
        }
      } else {
        newBook.ready_neighbours = { prev: origin, next: origin };
      }
      block.storageRoot = insertValue(
        block.storageRoot,
        serviceHeadBinKey,
        serviceHeadBinKey.length * 2,
        serviceHeadCodecs.value.enc(origin)
      );
    }
    block.storageRoot = insertValue(
      block.storageRoot,
      bookBinKey,
      bookBinKey.length * 2,
      bookCodecs.value.enc(newBook)
    );
  }
};
const attachSibling = async (siblingClient, selfClient, chain, xcm) => {
  const selfParaId = await getConstant(
    chain.getBlock(await firstValueFrom(chain.finalized$)),
    "ParachainSystem",
    "SelfParaId"
  );
  if (selfParaId == null) throw new Error("Could not get self parachain ID");
  const siblingApi = siblingClient.getTypedApi(_allDescriptors);
  const selfApi = selfClient.getTypedApi(_allDescriptors);
  const staticApis = await siblingApi.getStaticApis();
  if (!staticApis.compat.query.ParachainSystem.HrmpOutboundMessages.isCompatible(
    CompatibilityLevel.Partial
  )) {
    throw new Error("HrmpOutboundMessages incompatible");
  }
  const siblingParaId = await siblingApi.constants.ParachainSystem.SelfParaId();
  log.info({ selfParaId, siblingParaId }, "attaching HRMP channel");
  chain.openHrmpChannel(siblingParaId);
  siblingClient._request("forklift_xcm_open_hrmp_channel", [selfParaId]);
  siblingClient.finalizedBlock$.pipe(
    concatMapEager(
      (block) => from(
        siblingApi.query.ParachainSystem.HrmpOutboundMessages.getValue({
          at: block.hash
        })
      ).pipe(
        catchError((ex) => {
          log.error(ex, "error reading sibling HRMP outbound messages");
          return [];
        })
      )
    )
  ).subscribe((messages) => {
    const forSelf = messages.filter((m) => m.recipient === selfParaId);
    if (forSelf.length > 0) {
      xcm.pushHrmp(
        siblingParaId,
        forSelf.map((m) => m.data)
      );
    }
  });
  selfClient.finalizedBlock$.pipe(
    concatMapEager(
      (block) => from(
        selfApi.query.ParachainSystem.HrmpOutboundMessages.getValue({
          at: block.hash
        })
      ).pipe(
        catchError((ex) => {
          log.error(ex, "error reading self HRMP outbound messages");
          return [];
        })
      )
    )
  ).subscribe((messages) => {
    const forSibling = messages.filter((m) => m.recipient === siblingParaId);
    if (forSibling.length > 0) {
      siblingClient._request("forklift_xcm_push_hrmp", [
        selfParaId,
        forSibling.map((m) => Binary.toHex(m.data))
      ]);
    }
  });
};
const heapEncoder = (value) => Tuple(
  ...value.map(
    (coded) => Struct({
      length: u32,
      is_processed: bool,
      payload: Bytes(coded.length)
    })
  )
).enc(
  value.map((payload) => ({
    length: payload.length,
    is_processed: false,
    payload
  }))
);

export { attachRelay, attachSibling, consumeDmp, pushUmp };
//# sourceMappingURL=xcm.js.map
