import type { Idl } from '@project-serum/anchor';
import type { PublicKey, SendTransactionError } from '@solana/web3.js';

import {
  BLAZE_ADDRESS,
  BLAZE_IDL
} from './programs/blockasset-blaze/constants';

export type ErrorCode = {
  code: string;
  message: string;
};

export const NATIVE_ERRORS: ErrorCode[] = [
  {
    code: 'Blockhash not found',
    message: 'Blockhash not found. Transaction may or may not have gone through'
  },
  {
    code: 'Transaction was not confirmed in',
    message:
      'Transaction timed out waiting on confirmation from Solana. It may or may not have gone through'
  },
  // token program errors
  {
    code: '0x1',
    message:
      'Insufficient funds. User does not have enough balance of token to complete the transaction'
  },
  {
    code: '0x4',
    message:
      'Invalid owner. The user is likely not mint authority of this token.'
  },
  {
    code: '91',
    message: 'Token is not ellgible for original receipts'
  },
  // anchor errors
  {
    code: '100',
    message: 'InstructionMissing: 8 byte instruction identifier not provided'
  },
  {
    code: '101',
    message: 'InstructionFallbackNotFound: Fallback functions are not supported'
  },
  {
    code: '102',
    message:
      'InstructionDidNotDeserialize: The program could not deserialize the given instruction'
  },
  {
    code: '103',
    message:
      'InstructionDidNotSerialize: The program could not serialize the given instruction'
  },
  {
    code: '1000',
    message:
      'IdlInstructionStub: The program was compiled without idl instructions'
  },
  {
    code: '1001',
    message:
      'IdlInstructionInvalidProgram: Invalid program given to the IDL instruction'
  },
  { code: '2000', message: 'ConstraintMut: A mut constraint was violated' },
  {
    code: '2001',
    message: 'ConstraintHasOne: A has one constraint was violated'
  },
  {
    code: '2002',
    message: 'ConstraintSigner: A signer constraint as violated'
  },
  { code: '2003', message: 'ConstraintRaw: A raw constraint was violated' },
  {
    code: '2004',
    message: 'ConstraintOwner: An owner constraint was violated'
  },
  {
    code: '2005',
    message: 'ConstraintRentExempt: A rent exemption constraint was violated'
  },
  {
    code: '2006',
    message: 'ConstraintSeeds: A seeds constraint was violated'
  },
  {
    code: '2007',
    message: 'ConstraintExecutable: An executable constraint was violated'
  },
  {
    code: '2008',
    message: 'ConstraintState: A state constraint was violated'
  },
  {
    code: '2009',
    message: 'ConstraintAssociated: An associated constraint was violated'
  },
  {
    code: '2010',
    message:
      'ConstraintAssociatedInit: An associated init constraint was violated'
  },
  {
    code: '2011',
    message: 'ConstraintClose: A close constraint was violated'
  },
  {
    code: '2012',
    message: 'ConstraintAddress: An address constraint was violated'
  },
  {
    code: '2013',
    message: 'ConstraintZero: Expected zero account discriminant'
  },
  {
    code: '2014',
    message: 'ConstraintTokenMint: A token mint constraint was violated'
  },
  {
    code: '2015',
    message: 'ConstraintTokenOwner: A token owner constraint was violated'
  },
  {
    code: '2016',
    message:
      'ConstraintMintMintAuthority: A mint mint authority constraint was violated'
  },
  {
    code: '2017',
    message:
      'ConstraintMintFreezeAuthority: A mint freeze authority constraint was violated'
  },
  {
    code: '2018',
    message: 'ConstraintMintDecimals: A mint decimals constraint was violated'
  },
  {
    code: '2019',
    message: 'ConstraintSpace: A space constraint was violated'
  },
  {
    code: '3000',
    message:
      'AccountDiscriminatorAlreadySet: The account discriminator was already set on this account'
  },
  {
    code: '3001',
    message:
      'AccountDiscriminatorNotFound: No 8 byte discriminator was found on the account'
  },
  {
    code: '3002',
    message:
      'AccountDiscriminatorMismatch: 8 byte discriminator did not match what was expected'
  },
  {
    code: '3003',
    message: 'AccountDidNotDeserialize: Failed to deserialize the account'
  },
  {
    code: '3004',
    message: 'AccountDidNotSerialize: Failed to serialize the account'
  },
  {
    code: '3005',
    message:
      'AccountNotEnoughKeys: Not enough account keys given to the instruction'
  },
  {
    code: '3006',
    message: 'AccountNotMutable: The given account is not mutable'
  },
  {
    code: '3007',
    message:
      'AccountNotProgramOwned: The given account is not owned by the executing program'
  },
  {
    code: '3008',
    message: 'InvalidProgramId: Program ID was not as expected'
  },
  {
    code: '3009',
    message: 'InvalidProgramExecutable: Program account is not executable'
  },
  {
    code: '3010',
    message: 'AccountNotSigner: The given account did not sign'
  },
  {
    code: '3011',
    message:
      'AccountNotSystemOwned: The given account is not owned by the system program'
  },
  {
    code: '3012',
    message:
      'AccountNotInitialized: The program expected this account to be already initialized'
  },
  {
    code: '3013',
    message:
      'AccountNotProgramData: The given account is not a program data account'
  },
  {
    code: '3014',
    message:
      'AccountNotAssociatedTokenAccount: The given account is not the associated token account'
  },
  {
    code: '4000',
    message:
      'StateInvalidAddress: The given state account does not have the correct address'
  },
  {
    code: '5000',
    message:
      'Deprecated: The API being used is deprecated and should no longer be used'
  }
].reverse();

export type ErrorOptions = {
  /** ProgramIdls in priority order */
  programIdls?: { idl: Idl; programId: PublicKey }[];
  /** Additional errors by code */
  additionalErrors?: ErrorCode[];
};

export const handleError = (
  e: unknown,
  fallBackMessage = 'Transaction failed',
  // programIdls in priority order
  options: ErrorOptions = {
    programIdls: [{ programId: BLAZE_ADDRESS, idl: BLAZE_IDL }],
    additionalErrors: NATIVE_ERRORS
  }
): string => {
  const programIdls = options.programIdls ?? [];
  const additionalErrors = options.additionalErrors ?? [];
  const hex = (e as SendTransactionError)?.message?.split(' ').at(-1);
  const dec = parseInt(hex || '', 16);
  const logs =
    (e as SendTransactionError)?.logs ?? [
      (e as SendTransactionError)?.message
    ] ?? [(e as Error).toString()] ??
    [];

  const matchedErrors: { programMatch?: boolean; errorMatch?: string }[] = [
    ...[
      ...programIdls.map(({ idl, programId }) => ({
        // match program on any log that includes programId and 'failed'
        programMatch: logs?.some(
          l => l?.includes(programId.toString()) && l.includes('failed')
        ),
        // match error with decimal
        errorMatch: idl.errors?.find(err => err.code === dec)?.msg
      })),
      {
        // match native error with decimal
        errorMatch: additionalErrors.find(err => err.code === dec.toString())
          ?.message
      }
    ],
    ...[
      {
        programMatch: false,
        errorMatch: additionalErrors.find(
          err =>
            // message includes error
            (e as SendTransactionError)?.message?.includes(err.code) ||
            // toString includes error
            (e as Error).toString().includes(err.code) ||
            // any log includes error
            (e as SendTransactionError)?.logs?.some(l =>
              l.toString().includes(err.code)
            )
        )?.message
      },
      ...programIdls.map(({ idl, programId }) => ({
        // match program on any log that includes programId and 'failed'
        programMatch: logs?.some(
          l => l?.includes(programId.toString()) && l.includes('failed')
        ),
        errorMatch: idl.errors?.find(
          err =>
            // message includes error
            (e as SendTransactionError)?.message?.includes(
              err.code.toString()
            ) ||
            // toString includes error
            (e as Error).toString().includes(err.code.toString()) ||
            // any log includes error
            (e as SendTransactionError)?.logs?.some(l =>
              l.toString().includes(err.code.toString())
            )
        )?.msg
      }))
    ]
  ];

  return (
    matchedErrors.find(e => e.programMatch && e.errorMatch)?.errorMatch ||
    matchedErrors.find(e => e.errorMatch)?.errorMatch ||
    fallBackMessage
  );
};
