import {
  constructClaim,
  constructClaimKey,
  constructContinueTx,
  constructStore,
  Context,
  GData,
  MethodCallback,
  NewTx,
  QueueInput,
} from '@coinweb/contract-kit';
import { DataUnverified } from '@coinweb/contract-kit/dist/types/operations/data';

import {
  checkIsNewHistoryAccessId,
  constructCurrentHistoryAccess,
  constructHistoryItem,
  createRawMetadata,
  setMetadata,
  validateMetadata,
} from '../../module/history/onchain';
import { ContractCallMetadataInterface, HISTORY_CODES, HistoryItem } from '../../module/history/shared';
import { HexBigInt, toHex32 } from '../../shared';
import { TX_STATUS_ERROR } from '../constants';
import { Exception } from '../features/exception';
import { queue, queueUnlockFee } from '../features/queue';

import { setContext } from './context';

const MAX_FAILS = 5;

const getFeesInContextMutableLink = (context: Context) => {
  return (context.tx.ops[2] as GData<DataUnverified>).DataOp as { UnVerified: HexBigInt };
};

const getOps = (tx: NewTx) => {
  switch (true) {
    case 'Continue' in tx:
      return tx.Continue.tx.ops;

    case 'Jump' in tx:
      return tx.Jump.tx.ops;

    default:
      return [];
  }
};

const addMetadataToArgs = (args: unknown[], metadata: ContractCallMetadataInterface) => {
  const existingMetadataIndex = args.findLastIndex(validateMetadata);

  if (existingMetadataIndex === -1) {
    return [...args, metadata];
  }

  return args;
};

const updateCallsWithDefaultMetadata = (tx: NewTx, rawMetadata: ContractCallMetadataInterface) => {
  const ops = getOps(tx);

  ops.forEach((op, i) => {
    if ('CallOp' in op) {
      const argsOp = ops[i + 1];

      if (argsOp && 'DataOp' in argsOp && 'UnVerified' in argsOp.DataOp) {
        if (!argsOp.DataOp.UnVerified) {
          argsOp.DataOp.UnVerified = [rawMetadata];
        } else if (Array.isArray(argsOp.DataOp.UnVerified)) {
          if (argsOp.DataOp.UnVerified[0] === 'GLOBAL_QUEUE') {
            const callInfo = JSON.parse(argsOp.DataOp.UnVerified[1] as string) as QueueInput;

            const argsWithMetadata = addMetadataToArgs(callInfo.methodInfo.methodArgs, rawMetadata);

            callInfo.methodInfo.methodArgs = argsWithMetadata;
            argsOp.DataOp.UnVerified[1] = JSON.stringify(callInfo);
          } else {
            const argsWithMetadata = addMetadataToArgs(argsOp.DataOp.UnVerified, rawMetadata);

            argsOp.DataOp.UnVerified = argsWithMetadata;
          }
        }
      }
    }
  });
};

const extractErrorMessage = (error: unknown) => {
  if (typeof error === 'string') {
    return error;
  }

  if (error instanceof Error) {
    return error.message;
  }

  return 'An unknown error has occurred.';
};

const extractHistoryItem = (error: unknown): HistoryItem => {
  return error instanceof Exception
    ? error.historyItem
    : {
        code: HISTORY_CODES.ERROR,
        message: extractErrorMessage(error),
      };
};

export const defaultErrorHandler = (context: Context, errors: unknown[]) => {
  const errorQueue = [...errors];
  const historyItems: HistoryItem[] = errorQueue.map((error) => extractHistoryItem(error));
  const messages: string[] = errorQueue.map((error) => extractErrorMessage(error));

  let i = 0;
  let fails = 0;
  let txs: NewTx[] = [];

  while (i < errorQueue.length && fails <= MAX_FAILS) {
    const error = errorQueue[i];

    txs = txs.length
      ? txs
      : [
          constructContinueTx(context, [
            constructStore(
              constructClaim(
                constructClaimKey(TX_STATUS_ERROR, context.call.txid),
                {
                  message: messages,
                },
                toHex32(0n)
              )
            ),
            ...constructCurrentHistoryAccess(),
            ...historyItems.flatMap((historyItem) =>
              constructHistoryItem({
                historyItem,
              })
            ),
          ]),
        ];

    const txFee = 100n * BigInt(errorQueue.length) + 100n + 100n + queueUnlockFee; //history fee + storeOp error + passCweb + queue fee

    const fees = getFeesInContextMutableLink(context);

    fees.UnVerified = toHex32(BigInt(fees.UnVerified) - txFee);

    try {
      if (error instanceof Exception && error.execute) {
        const newTxs = error.execute(context);

        txs.concat(...newTxs);
      }
    } catch (execError) {
      errorQueue.push(execError);
      historyItems.push(extractHistoryItem(execError));
      messages.push(extractErrorMessage(errorQueue));
      txs = [];

      fails++;
    }

    fees.UnVerified = toHex32(BigInt(fees.UnVerified) + txFee);
    i++;
  }

  txs.push(...queue.gateway.unlock(context));

  return txs;
};

export const createHandler = (
  handler: (context: Context) => NewTx[],
  errorHandler?: (context: Context, error: unknown) => NewTx[]
): MethodCallback => {
  return (ctx: Context) => {
    setContext(ctx);
    setMetadata(ctx);

    const isNewHistoryAccessId = checkIsNewHistoryAccessId();

    if (isNewHistoryAccessId) {
      const fees = getFeesInContextMutableLink(ctx);

      fees.UnVerified = toHex32(BigInt(fees.UnVerified) - 200n);
    }

    try {
      const handlerTxs = handler(ctx);

      const rawMetadata = createRawMetadata();
      if (rawMetadata) {
        handlerTxs.forEach((tx) => {
          updateCallsWithDefaultMetadata(tx, rawMetadata);
        });
      }

      if (isNewHistoryAccessId) {
        handlerTxs.push(constructContinueTx(ctx, constructCurrentHistoryAccess()));
      }

      return handlerTxs;
    } catch (err) {
      let handlerTxs: NewTx[];

      if (errorHandler) {
        try {
          handlerTxs = errorHandler(ctx, err);
        } catch (secondErr) {
          handlerTxs = defaultErrorHandler(ctx, [err, secondErr]);
        }
      }

      handlerTxs = defaultErrorHandler(ctx, [err]);

      const rawMetadata = createRawMetadata();

      if (rawMetadata) {
        handlerTxs.forEach((tx) => {
          updateCallsWithDefaultMetadata(tx, rawMetadata);
        });
      }

      return handlerTxs;
    }
  };
};
