import {
  AnchorProvider,
  BorshAccountsCoder,
  Program,
  utils,
} from "@project-serum/anchor";
import { SignerWallet } from "@saberhq/solana-contrib";
import type { Commitment, Connection } from "@solana/web3.js";
import { Keypair, PublicKey } from "@solana/web3.js";

import type { AccountData } from "../../types";
import type {
  MINTER_PROGRAM,
  MinterData,
  RedeemedMintsData,
  RedeemedMintsDataRaw,
} from "./constants";
import {
  MINTER_ADDRESS,
  MINTER_DISCRIMINATOR,
  MINTER_IDL,
  MINTER_PROJECT_ID_OFFSET,
  REDEEMED_MINTS_ACCOUNT_NAME,
} from "./constants";

const getProgram = (
  connection: Connection,
  commitment: Commitment = "processed"
) => {
  const provider = new AnchorProvider(
    connection,
    new SignerWallet(Keypair.generate()),
    {
      commitment,
    }
  );
  return new Program<MINTER_PROGRAM>(MINTER_IDL, MINTER_ADDRESS, provider);
};

export const getMinter = async (
  connection: Connection,
  minterId: PublicKey,
  commitment?: Commitment
): Promise<AccountData<MinterData>> => {
  const program = getProgram(connection, commitment);

  const parsed = await program.account.minter.fetch(minterId);
  return {
    parsed,
    pubkey: minterId,
  };
};

export const getMinters = async (
  connection: Connection,
  minterIds: PublicKey[],
  commitment?: Commitment
): Promise<AccountData<MinterData>[]> => {
  const program = getProgram(connection, commitment);
  const minters = (await program.account.minter.fetchMultiple(
    minterIds
  )) as MinterData[];
  return minters.map((tm, i) => ({
    parsed: tm,
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    pubkey: minterIds[i]!,
  }));
};

export const getAllMinters = async (
  connection: Connection,
  commitment?: Commitment
): Promise<AccountData<MinterData>[]> => {
  if (!connection) return [];
  const programAccounts = await connection.getProgramAccounts(MINTER_ADDRESS, {
    filters: [
      {
        memcmp: {
          offset: 0,
          bytes: utils.bytes.bs58.encode(
            BorshAccountsCoder.accountDiscriminator(MINTER_DISCRIMINATOR)
          ),
        },
      },
    ],
    commitment,
  });
  const minterDatas: AccountData<MinterData>[] = [];
  const coder = new BorshAccountsCoder(MINTER_IDL);
  programAccounts.forEach((account) => {
    try {
      const minterData: MinterData = coder.decode(
        MINTER_DISCRIMINATOR,
        account.account.data
      );
      if (minterData) {
        minterDatas.push({
          ...account,
          parsed: minterData,
        });
      }
      // eslint-disable-next-line no-empty
    } catch (e) {}
  });
  return minterDatas.sort((a, b) =>
    a.pubkey.toBase58().localeCompare(b.pubkey.toBase58())
  );
};

export const getMintersByProjectId = async (
  connection: Connection,
  projectId: PublicKey,
  commitment?: Commitment
): Promise<AccountData<MinterData>[]> => {
  if (!connection) return [];
  const programAccounts = await connection.getProgramAccounts(MINTER_ADDRESS, {
    filters: [
      {
        memcmp: {
          offset: 0,
          bytes: utils.bytes.bs58.encode(
            BorshAccountsCoder.accountDiscriminator(MINTER_DISCRIMINATOR)
          ),
        },
      },
      {
        memcmp: {
          offset: MINTER_PROJECT_ID_OFFSET,
          bytes: projectId.toBase58(),
        },
      },
    ],
    commitment,
  });
  const minterDatas: AccountData<MinterData>[] = [];
  const coder = new BorshAccountsCoder(MINTER_IDL);
  programAccounts.forEach((account) => {
    try {
      const minterData: MinterData = coder.decode(
        MINTER_DISCRIMINATOR,
        account.account.data
      );
      if (minterData) {
        minterDatas.push({
          ...account,
          parsed: minterData,
        });
      }
      // eslint-disable-next-line no-empty
    } catch (e) {}
  });
  return minterDatas.sort((a, b) =>
    a.pubkey.toBase58().localeCompare(b.pubkey.toBase58())
  );
};

export const getRedeemedMints = async (
  connection: Connection,
  redeemedMintsId: PublicKey,
  commitment?: Commitment
): Promise<AccountData<RedeemedMintsData> | null> => {
  const program = getProgram(connection, commitment);

  const accountInfo = await program.account.redeemedMints.getAccountInfo(
    redeemedMintsId
  );
  if (!accountInfo) return null;

  const coder = new BorshAccountsCoder(MINTER_IDL);

  try {
    const redeemedMintsDataRaw: RedeemedMintsDataRaw = coder.decodeUnchecked(
      REDEEMED_MINTS_ACCOUNT_NAME,
      accountInfo.data
    );
    if (!redeemedMintsDataRaw) return null;

    const mints: PublicKey[] = [];
    const mintsFieldData = accountInfo.data.subarray(8 + 4 + 32);
    for (let i = 0; i < redeemedMintsDataRaw.total; i++) {
      mints.push(new PublicKey(mintsFieldData.slice(i * 32, (i + 1) * 32)));
    }
    return {
      pubkey: redeemedMintsId,
      parsed: { ...redeemedMintsDataRaw, mints },
    };
  } catch (e) {
    console.error(e);
    return null;
  }
};
