import {
  constructBlock,
  constructContinueTx,
  constructContractRef,
  FullCallInfo,
  GTake,
  NewTx,
  passCwebFrom,
  PreparedOperation,
  sendCwebInterface,
} from '@coinweb/contract-kit';
import { CwebTake } from '@coinweb/contract-kit/dist/types/operations/take';

import { ExecutorMethodArgs, PreparedExtendedStoreOp, PreparedOp, ResolvedSlotOp } from '../../../types';
import { constructFundsClaimRangRead, constructFundsClaimStore } from '../../claims/funds';
import { constructResultBlockFilter, constructResultClaimTake, resultKey } from '../../claims/result';
import { context, getRawContext } from '../../context';
import { constructExecOpsCall, constructLockCall, constructUnlockCall, mutexExecOpsKey } from '../../mutex';
import {
  isPreparedExecOp,
  isPreparedLockOp,
  isPreparedStoreOp,
  isPreparedTakeOp,
  isPreparedUnlockOp,
} from '../../utils/opTypeGuards';
import { uuid } from '../../utils/uuid';

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

    const restOfAvailableCweb = availableCweb - outThreadFee;

    const fee = BigInt(takeOps.length) * 100n + 3000n; //TODO: Remove magic numbers
    const restOfCweb = restOfAvailableCweb + storedCweb - fee;

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

    return { txs, calls: 0, txFee: fee };
  }

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

  //Mutex exec ops
  const preparedExecOps: (PreparedExtendedStoreOp | GTake<CwebTake>)[] = [];
  const excOpsIndexes: number[] = [];

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

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

  //Info for separate parallel calls
  const parallelCalls: FullCallInfo[] = [];

  const outThreadOps: PreparedOperation[] = [];

  ops.forEach((op, i) => {
    switch (true) {
      case isPreparedExecOp(op): {
        const id = op.ExecOp.id;

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

        const childOps = [
          ...context.ops,
          ...resolvedSlotOps,
          ...ops.map((_, j) => (i === j ? { ExecOp: { id } } : { SlotOp: { ok: true } })),
        ];

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

        callsPrepared++;
        txFee += 700n;

        break;
      }
      case isPreparedStoreOp(op): {
        if ((op.StoreOp.key.first_part as [string])[0] !== resultKey) {
          preparedExecOps.push(op);
          excOpsIndexes.push(i);
        } else {
          callArgs.push(op);
          txFee += 100n;
        }
        break;
      }
      case isPreparedTakeOp(op): {
        //TODO: simplify to decrease coupling
        if ((op.TakeOp.key.first_part as [string])[0] !== mutexExecOpsKey) {
          preparedExecOps.push(op);
          excOpsIndexes.push(i);
        } else {
          callArgs.push(op);
          txFee += 100n;
        }
        break;
      }
      case isPreparedLockOp(op): {
        const {
          callInfo,
          fee,
          inThreadOps,
          outThreadOps: outThreadOpsFromLockCall,
        } = constructLockCall({
          issuer: context.issuer,
          lockId: op.LockOp.lockId,
          timestamp: op.LockOp.timestamp,
          keys: op.LockOp.keys,
          processId: context.thisId,
        });

        parallelCalls.push({ callInfo });
        callArgs.push(...inThreadOps);
        txFee += fee;

        outThreadOps.push(...outThreadOpsFromLockCall);

        break;
      }
      case isPreparedUnlockOp(op): {
        console.log('prepareInThreadTxs >>> unlockOp');
        const { callInfo, fee, ops } = constructUnlockCall(
          context.issuer,
          op.UnlockOp.lockId,
          op.UnlockOp.timestamp,
          true
        );

        parallelCalls.push({ callInfo });
        callArgs.push(...ops);
        txFee += fee;

        break;
      }
      default:
        callArgs.push(op);
        txFee += 100n;
    }
  });

  if (preparedExecOps.length > 0) {
    const execId = uuid();

    const { callInfo, fee, ops } = constructExecOpsCall({
      issuer: context.issuer,
      ops: preparedExecOps,
      processId: context.thisId,
      execId,
    });

    parallelCalls.push({ callInfo });
    txFee += fee;

    console.log(txFee);
    console.log(context.funds.availableCweb);

    callArgs.push(...ops);
  }

  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');
      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;

        console.log('cwebToStore: ', cwebToStore);

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

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

      console.log('provided cweb: ', cwebPerCall - txFee + storedCweb);

      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]),
                    excOpsIndexes,
                  ] satisfies ExecutorMethodArgs,
                },
                contractInfo: {
                  providedCweb: cwebPerCall - txFee + storedCweb,
                  authenticated: null,
                },
                contractArgs: [constructFundsClaimRangRead(context.thisId), ...callArgs],
              },
            },
          ]
        )
      );

      if (parallelCalls.length || outThreadOps.length) {
        returnTxs.push(constructContinueTx(getRawContext(), outThreadOps, parallelCalls));
      }
    }
  }

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