import {
  constructBlock,
  constructContinueTx,
  constructContractRef,
  FullCallInfo,
  NewTx,
  passCwebFrom,
  PreparedOperation,
  sendCwebInterface,
} from '@coinweb/contract-kit';
import { CwebBlock } from '@coinweb/contract-kit/dist/esm/operations/block';
import { GBlock } from '@coinweb/contract-kit/dist/types/operations/generics';

import { ExecutorMethodArgs, ResolvedOp, ResolvedSlotOp, Task } from '../../../types';
import { constructFundsClaimRangRead, constructFundsClaimStore } from '../../claims/funds';
import { constructResultBlockFilter, constructResultClaimTake, resultKey } from '../../claims/result';
import { context, getRawContext } from '../../context';
import { isPreparedBlockOp, isPreparedExecOp } from '../typeGuards';

export const prepareInThreadTxs = ({
  cwebPerCall,
  outThreadTasksCount,
  outThreadFee,
  tasks,
}: {
  tasks: Task[];
  cwebPerCall: bigint;
  outThreadTasksCount: number;
  outThreadFee: bigint;
}): {
  txs: NewTx[];
  calls: number;
} => {
  if (!tasks.length) {
    const { constructSendCweb } = sendCwebInterface();
    const { availableCweb, takeOps, storedCweb } = context.funds;
    const { user } = context;

    const restOfAvailableCweb = availableCweb - outThreadFee;

    const restOfCweb = restOfAvailableCweb + storedCweb - BigInt(takeOps.length) * 100n - 3000n;

    const txs =
      restOfCweb > 0n
        ? [
            constructContinueTx(getRawContext(), [
              passCwebFrom(context.issuer, restOfAvailableCweb),
              ...takeOps,
              ...constructSendCweb(restOfCweb, user, null),
            ]),
          ]
        : [];

    return { txs, calls: 0 };
  }

  let txFee = 0n;
  let callsPrepared = 0;
  const resolvedSlotOps = new Array(outThreadTasksCount).fill({ SlotOp: 0 }) satisfies ResolvedSlotOp[];

  const resolvedChildOps: ResolvedOp[] = [...context.ops, ...resolvedSlotOps];

  //Arg for the main call
  const callArgs: PreparedOperation[] = [];

  //Info for separate child call
  const childCalls: FullCallInfo[] = [];

  //Block ops for separate child call
  const childBlocks = tasks
    .filter((task): task is { op: GBlock<CwebBlock>; batchId: number } => isPreparedBlockOp(task.op))
    .map(({ op }) => op);

  tasks.forEach((task) => {
    if (isPreparedExecOp(task.op)) {
      console.log('Child call info');
      const id = task.op.ExecOp.id;

      callArgs.push(constructBlock([constructResultBlockFilter(id)]), constructResultClaimTake(id));
      txFee += 200n;

      childCalls.push({
        callInfo: {
          ref: constructContractRef(context.issuer, []),
          methodInfo: {
            methodName: context.methodName,
            methodArgs: [
              context.initialArgs,
              [...resolvedChildOps, { ExecOp: { id } }],
              context.user,
              id,
              context.thisId,
              true,
            ] satisfies ExecutorMethodArgs,
          },
          contractInfo: {
            providedCweb: cwebPerCall - 700n,
            authenticated: null,
          },
          contractArgs: [],
        },
      });

      callsPrepared++;
    } else {
      callArgs.push(task.op);
      txFee += 100n;
    }

    resolvedChildOps.push({ SlotOp: 0 });
  });

  const returnTxs: NewTx[] = [];

  if (callArgs.length) {
    const { storedCweb, takeOps } = context.funds;

    const latestCallArg = callArgs.at(-1)!;

    if ('StoreOp' in latestCallArg && (latestCallArg.StoreOp.key.first_part as [string])[0] === resultKey) {
      //SAVE RESULT CLAIMS
      console.log('SAVE RESULT CLAIMS', JSON.stringify(callArgs));

      if (callArgs.length > 1) {
        throw new Error('Unexpected count of result ops');
      }

      const resultOps = [];

      if (context.needSaveResult) {
        resultOps.push(...callArgs);
      }

      if (context.parentId) {
        const { availableCweb, storedCweb, takeOps } = context.funds;

        const cwebToStore = availableCweb + storedCweb - BigInt(takeOps.length) * 100n - 500n;

        resultOps.push(
          constructFundsClaimStore(context.parentId, cwebToStore),
          ...takeOps,
          passCwebFrom(context.issuer, availableCweb)
        );
      }

      if (resultOps.length) {
        returnTxs.push(constructContinueTx(getRawContext(), resultOps));
      }
    } else {
      txFee += 800n + outThreadFee + BigInt(takeOps.length) * 100n;
      callsPrepared++;

      returnTxs.push(
        constructContinueTx(
          getRawContext(),
          [passCwebFrom(context.issuer, cwebPerCall - outThreadFee), ...takeOps],
          [
            {
              callInfo: {
                ref: constructContractRef(context.issuer, []),
                methodInfo: {
                  methodName: context.methodName,
                  methodArgs: [
                    context.initialArgs,
                    [...context.ops, ...resolvedSlotOps],
                    context.user,
                    context.thisId,
                    context.parentId,
                    context.needSaveResult,
                    takeOps.map((op) => (op.TakeOp.key.second_part as [string])[0]),
                  ] satisfies ExecutorMethodArgs,
                },
                contractInfo: {
                  providedCweb: cwebPerCall - txFee + storedCweb,
                  authenticated: null,
                },
                contractArgs: [constructFundsClaimRangRead(context.thisId), ...callArgs],
              },
            },
          ]
        )
      );

      if (childCalls.length) {
        returnTxs.push(constructContinueTx(getRawContext(), childBlocks, childCalls));
      }
    }
  }

  return { txs: returnTxs, calls: callsPrepared };
};
