import { constructTake, extractRead, GRead, GTake, ResolvedOperation, ResolvedRead } from '@coinweb/contract-kit';
import { CwebTake } from '@coinweb/contract-kit/dist/types/operations/take';

import { ResolvedOp, TypedClaim } from '../../types';
import { resultKey } from '../claims';
import {
  mutexBlockLockKey,
  mutexBlockUnlockKey,
  mutexExecOpsKey,
  MutexExecOpsResult,
  MutexNotifyLockState,
} from '../mutex';
import { isResolvedBlockOp, isResolvedReadOp, isResolvedTakeOp } from '../utils';

const extractFunds = (fundsOp: GRead<ResolvedRead>, takenFundsIds: string[]) => {
  if (!fundsOp) {
    return {
      takeOps: [],
      amount: 0n,
    };
  }

  const claims = extractRead(fundsOp);

  const newClaims = claims?.filter(({ content }) => !takenFundsIds.includes((content.key.second_part as [string])[0]));

  if (!newClaims?.length) {
    return {
      takeOps: [],
      amount: 0n,
    };
  }

  return {
    takeOps: newClaims.map(({ content }) => constructTake(content.key)),
    amount: newClaims.reduce((acc, { content }) => acc + BigInt(content.fees_stored), 0n),
  };
};

export const extractOps = ({
  contractArgs,
  takenFundsIds,
  execOpsIndexes,
}: {
  contractArgs: ResolvedOperation[];
  takenFundsIds: string[];
  execOpsIndexes: number[];
}) => {
  if (!contractArgs.length) {
    return {
      extractedOps: [],
      fundsTakeOps: [],
      storedCweb: 0n,
      executionOpsTakeOp: null,
    };
  }

  const [fundsOp, ...resolvedOps] = contractArgs;

  if (!isResolvedReadOp(fundsOp)) {
    throw new Error('Wrong funds claims');
  }

  const extractedOps: ResolvedOp[] = [];
  let executedOps: MutexExecOpsResult = [];
  let executionOpsProfit: bigint = 0n;
  let executionOpsTakeOp: GTake<CwebTake> | null = null;

  let i = 0;

  while (i < resolvedOps.length) {
    const op = resolvedOps[i];

    if (isResolvedBlockOp(op)) {
      const { first } = op.BlockOp.blocks_on[0][0];

      console.log('first >>>', JSON.stringify(first));

      //Maybe it is needed to check more conditions here
      if (Array.isArray(first)) {
        switch (first[0]) {
          case resultKey: {
            const nextAfterBlock = resolvedOps[i + 1];

            if (!isResolvedTakeOp(nextAfterBlock)) {
              throw new Error('Wrong subcall result');
            }

            extractedOps.push({ ChildOp: 0 }, ...(nextAfterBlock.TakeOp.result as TypedClaim<ResolvedOp[]>).body);

            i += 2;
            continue;
          }
          case mutexBlockLockKey: {
            const nextAfterBlock = resolvedOps[i + 1];

            if (!isResolvedTakeOp(nextAfterBlock)) {
              throw new Error('Wrong mutex lock result');
            }

            extractedOps.push({
              LockOp: nextAfterBlock.TakeOp.result.body as MutexNotifyLockState,
            });

            i += 2;
            continue;
          }
          case mutexBlockUnlockKey: {
            const nextAfterBlock = resolvedOps[i + 1];

            if (!isResolvedTakeOp(nextAfterBlock)) {
              throw new Error('Wrong mutex unlock result');
            }

            extractedOps.push({ UnlockOp: 0 });

            i += 2;
            continue;
          }
          case mutexExecOpsKey: {
            const nextAfterBlock = resolvedOps[i + 1];

            const execOpResultClaim = extractRead(nextAfterBlock)?.[0]?.content;

            if (!execOpResultClaim) {
              throw new Error('Wrong mutex exec result claim');
            }

            executedOps = execOpResultClaim.body as MutexExecOpsResult;
            executionOpsProfit = BigInt(execOpResultClaim.fees_stored);
            executionOpsTakeOp = constructTake(execOpResultClaim.key);
            i += 2;
            continue;
          }

          default:
            break;
        }
      }
    }

    if (isResolvedTakeOp(op)) {
      const keyFirstPart = op.TakeOp.result.key.first_part;
      if (Array.isArray(keyFirstPart) && keyFirstPart[0] === mutexExecOpsKey) {
        i++;
        continue;
      }
    }

    extractedOps.push(op);
    i++;
  }

  const allOps: ResolvedOp[] = [];

  const totalOpsCount = extractedOps.length + executedOps.length;

  for (let i = 0; i < totalOpsCount; i++) {
    if (execOpsIndexes.includes(i)) {
      const op = executedOps.shift();

      if (!op) {
        throw new Error('Wrong mutex exec result place');
      }

      if (op.ok) {
        allOps.push(op.resolved);
      } else {
        allOps.push({ SlotOp: { ok: false, error: op.error } });
      }
    } else {
      const op = extractedOps.shift();

      if (!op) {
        throw new Error('Contract call args parsing error');
      }

      allOps.push(op);
    }
  }

  const { takeOps, amount } = extractFunds(fundsOp, takenFundsIds);

  return {
    fundsTakeOps: takeOps,
    storedCweb: amount + executionOpsProfit,
    extractedOps: allOps,
    executionOpsTakeOp,
  };
};
