import { blockHeader } from '@polkadot-api/substrate-bindings';
import { Binary } from 'polkadot-api';
import { Subscription, firstValueFrom, combineLatest, withLatestFrom, from, map, merge, filter } from 'rxjs';
import { finalizedAndPruned$ } from '../chain.js';
import { logger } from '../logger.js';
import { runRuntimeCall } from '../executor.js';
import { getParams, respond, errorResponse, getUuid } from './rpc_utils.js';

const log = logger.child({ module: "chainHead_v1" });
const followEvent = (subscription, result) => ({
  jsonrpc: "2.0",
  method: "chainHead_v1_followEvent",
  params: {
    subscription,
    result
  }
});
const blockNotPinned = (req) => errorResponse(req, {
  code: -32801,
  message: "Block not pinned"
});
const blockNotReadable = (req) => errorResponse(req, {
  code: -32603,
  message: "Block not readable"
});
const chainHead_v1_follow = async (con, req, { chain }) => {
  const { withRuntime } = getParams(req, ["withRuntime"]);
  const subId = getUuid();
  const ctx = con.context.chainHead_v1_subs[subId] = {
    pinnedBlocks: /* @__PURE__ */ new Set(),
    operations: {},
    subscription: new Subscription()
  };
  con.send(respond(req, subId));
  const [finalized, blocks] = await firstValueFrom(
    combineLatest([chain.finalized$, chain.blocks$])
  );
  const finalizedBlockHashes = [finalized];
  ctx.pinnedBlocks.add(finalized);
  let block = finalized;
  let nextBlockHeight = blocks[block].header.number;
  for (let i = 0; i < 5 && blocks[block]?.parent in blocks; i++) {
    block = blocks[block].parent;
    if (!blocks[block] || blocks[block].header.number !== nextBlockHeight - 1)
      break;
    nextBlockHeight = blocks[block].header.number;
    ctx.pinnedBlocks.add(block);
    finalizedBlockHashes.push(block);
  }
  finalizedBlockHashes.reverse();
  con.send(
    followEvent(subId, {
      event: "initialized",
      finalizedBlockHashes,
      finalizedBlockRuntime: withRuntime ? blocks[finalized].runtime : void 0
    })
  );
  const sendChildren = (hash) => {
    const block2 = blocks[hash];
    for (const child of block2.children) {
      const childBlock = blocks[child];
      if (!childBlock) {
        log.error({ parent: hash, child }, "child block not found");
        continue;
      }
      ctx.pinnedBlocks.add(child);
      con.send(
        followEvent(subId, {
          event: "newBlock",
          blockHash: child,
          parentBlockHash: hash,
          newRuntime: withRuntime ? childBlock.hasNewRuntime : void 0
        })
      );
      sendChildren(child);
    }
  };
  sendChildren(finalized);
  ctx.subscription.add(
    con.disconnect$.subscribe(() => ctx.subscription.unsubscribe())
  );
  ctx.subscription.add(
    chain.newBlocks$.pipe(withLatestFrom(chain.blocks$)).subscribe(([blockHash, blocks2]) => {
      const block2 = blocks2[blockHash];
      const prevBlock = blocks2[block2.parent];
      if (prevBlock) {
        if (block2.header.number != prevBlock.header.number + 1) {
          for (const op of Object.values(ctx.operations)) {
            op.unsubscribe();
          }
          ctx.subscription.unsubscribe();
          delete con.context.chainHead_v1_subs[subId];
          setTimeout(() => {
            con.send(
              followEvent(subId, {
                event: "stop"
              })
            );
          });
          return;
        }
      }
      ctx.pinnedBlocks.add(blockHash);
      con.send(
        followEvent(subId, {
          event: "newBlock",
          blockHash,
          parentBlockHash: block2.parent,
          newRuntime: withRuntime && block2.hasNewRuntime ? block2.runtime : void 0
        })
      );
    })
  );
  ctx.subscription.add(
    chain.best$.subscribe((bestBlockHash) => {
      con.send(
        followEvent(subId, { event: "bestBlockChanged", bestBlockHash })
      );
    })
  );
  ctx.subscription.add(
    finalizedAndPruned$(chain).subscribe(({ finalized: finalized2, pruned }) => {
      con.send(
        followEvent(subId, {
          event: "finalized",
          finalizedBlockHashes: finalized2,
          prunedBlockHashes: pruned
        })
      );
    })
  );
};
const chainHead_v1_unfollow = async (con, req) => {
  const { followSubscription } = getParams(req, ["followSubscription"]);
  const followSub = con.context.chainHead_v1_subs[followSubscription];
  con.send(respond(req, null));
  if (!followSub) return;
  for (const op of Object.values(followSub.operations)) {
    op.unsubscribe();
  }
  followSub.subscription.unsubscribe();
  delete con.context.chainHead_v1_subs[followSubscription];
};
const chainHead_v1_header = (con, req, { chain }) => {
  const { followSubscription, hash } = getParams(req, [
    "followSubscription",
    "hash"
  ]);
  const sub = con.context.chainHead_v1_subs[followSubscription];
  if (!sub) return con.send(respond(req, null));
  if (!sub.pinnedBlocks.has(hash)) return con.send(blockNotPinned(req));
  const block = chain.getBlock(hash);
  if (!block) return con.send(blockNotReadable(req));
  con.send(respond(req, Binary.toHex(blockHeader.enc(block.header))));
};
const chainHead_v1_storage = (con, req, { chain }) => {
  const { followSubscription, hash, items, childTrie } = getParams(req, [
    "followSubscription",
    "hash",
    "items",
    "childTrie"
  ]);
  const sub = con.context.chainHead_v1_subs[followSubscription];
  if (!sub) return con.send(respond(req, { result: "limitReached" }));
  if (!sub.pinnedBlocks.has(hash)) return con.send(blockNotPinned(req));
  if (childTrie)
    return con.send(blockNotReadable(req));
  const block = chain.getBlock(hash);
  if (!block) return con.send(blockNotReadable(req));
  const opId = getUuid();
  const subscription = new Subscription();
  sub.operations[opId] = subscription;
  const cleanup = () => {
    delete sub.operations[opId];
    subscription.unsubscribe();
  };
  con.send(
    respond(req, {
      result: "started",
      operationId: opId,
      discardedItems: []
    })
  );
  subscription.add(
    resolveStorageOperation(chain, hash, items).subscribe({
      next: (items2) => con.send(
        followEvent(followSubscription, {
          event: "operationStorageItems",
          operationId: opId,
          items: items2
        })
      ),
      error: (e) => {
        log.error(e, "storage operation error");
        con.send(
          followEvent(followSubscription, {
            event: "operationError",
            operationId: opId,
            error: e.message
          })
        );
        cleanup();
      },
      complete: () => {
        con.send(
          followEvent(followSubscription, {
            event: "operationStorageDone",
            operationId: opId
          })
        );
        cleanup();
      }
    })
  );
};
const resolveStorageOperation = (chain, hash, items) => {
  const nodeQueries = items.filter(
    (it) => it.type === "closestDescendantMerkleValue" || it.type === "hash" || it.type === "value"
  );
  const nodeQueries$ = from(
    nodeQueries.length ? chain.getStorageBatch(
      hash,
      nodeQueries.map((it) => it.key)
    ) : []
  ).pipe(
    map(
      (nodes) => nodes.flatMap((node, i) => {
        const { key, type } = nodeQueries[i];
        if (type === "closestDescendantMerkleValue") {
          return [{ key, [type]: Binary.toHex(node.hash) }];
        }
        if (!node.value) return [];
        return [
          type === "hash" ? { key, hash: Binary.toHex(node.hash) } : { key, value: Binary.toHex(node.value) }
        ];
      })
    )
  );
  const descendantQueries = items.filter(
    (it) => it.type === "descendantsHashes" || it.type === "descendantsValues"
  );
  const descendantQueries$ = descendantQueries.map(
    (query) => from(chain.getStorageDescendants(hash, query.key)).pipe(
      map(
        (nodes) => Object.entries(nodes).map(
          ([key, node]) => query.type === "descendantsHashes" ? {
            key,
            hash: Binary.toHex(node.hash)
          } : node.value ? {
            key,
            value: Binary.toHex(node.value)
          } : null
        ).filter((v) => v != null)
      )
    )
  );
  return merge(nodeQueries$, ...descendantQueries$).pipe(
    filter((v) => v.length > 0)
  );
};
const chainHead_v1_call = (con, req, { chain }) => {
  const {
    followSubscription,
    hash,
    function: fnName,
    callParameters
  } = getParams(req, [
    "followSubscription",
    "hash",
    "function",
    "callParameters"
  ]);
  const sub = con.context.chainHead_v1_subs[followSubscription];
  if (!sub) return con.send(respond(req, { result: "limitReached" }));
  if (!sub.pinnedBlocks.has(hash)) return con.send(blockNotPinned(req));
  const block = chain.getBlock(hash);
  if (!block) return con.send(blockNotReadable(req));
  const opId = getUuid();
  const subscription = new Subscription();
  sub.operations[opId] = subscription;
  const cleanup = () => {
    delete sub.operations[opId];
    subscription.unsubscribe();
  };
  con.send(
    respond(req, {
      result: "started",
      operationId: opId
    })
  );
  subscription.add(
    from(
      runRuntimeCall({
        chain,
        hash,
        call: fnName,
        params: callParameters
      })
    ).subscribe({
      next: (output) => con.send(
        followEvent(followSubscription, {
          event: "operationCallDone",
          operationId: opId,
          output: output.result
        })
      ),
      error: (e) => {
        log.error(e, "call operation error");
        con.send(
          followEvent(followSubscription, {
            event: "operationError",
            operationId: opId,
            error: e.message
          })
        );
        cleanup();
      },
      complete: () => {
        cleanup();
      }
    })
  );
};
const chainHead_v1_body = (con, req, { chain }) => {
  const { followSubscription, hash } = getParams(req, [
    "followSubscription",
    "hash"
  ]);
  const sub = con.context.chainHead_v1_subs[followSubscription];
  if (!sub) return con.send(respond(req, { result: "limitReached" }));
  if (!sub.pinnedBlocks.has(hash)) return con.send(blockNotPinned(req));
  const block = chain.getBlock(hash);
  if (!block) return con.send(blockNotReadable(req));
  const opId = getUuid();
  con.send(
    respond(req, {
      result: "started",
      operationId: opId
    })
  );
  con.send(
    followEvent(followSubscription, {
      event: "operationBodyDone",
      operationId: opId,
      value: block.body.map(Binary.toHex)
    })
  );
};
const chainHead_v1_stopOperation = (con, req) => {
  const { followSubscription, operationId } = getParams(req, [
    "followSubscription",
    "operationId"
  ]);
  con.send(respond(req, null));
  const sub = con.context.chainHead_v1_subs[followSubscription];
  if (!sub?.operations[operationId]) return;
  sub.operations[operationId].unsubscribe();
  delete sub.operations[operationId];
};
const chainHead_v1_unpin = (con, req) => {
  const { followSubscription, hashOrHashes } = getParams(req, [
    "followSubscription",
    "hashOrHashes"
  ]);
  const sub = con.context.chainHead_v1_subs[followSubscription];
  if (!sub) return con.send(respond(req, null));
  const hashes = Array.isArray(hashOrHashes) ? hashOrHashes : [hashOrHashes];
  const uniqueHashes = new Set(hashes);
  if (uniqueHashes.size != hashes.length)
    return con.send(
      errorResponse(req, {
        code: -32804,
        message: "Duplicate hashes"
      })
    );
  if (hashes.some((hash) => !sub.pinnedBlocks.has(hash)))
    return con.send(blockNotPinned(req));
  hashes.forEach((hash) => sub.pinnedBlocks.delete(hash));
  return con.send(respond(req, null));
};

export { chainHead_v1_body, chainHead_v1_call, chainHead_v1_follow, chainHead_v1_header, chainHead_v1_stopOperation, chainHead_v1_storage, chainHead_v1_unfollow, chainHead_v1_unpin, resolveStorageOperation };
//# sourceMappingURL=chainHead_v1.js.map
