import { AnchorProvider, BN, Idl, Program, Provider } from "@coral-xyz/anchor";
import FARMS_IDL from "./rpc_client/farms.json";

import {
  Connection,
  GetProgramAccountsFilter,
  PublicKey,
  sendAndConfirmTransaction,
  Signer,
  Transaction,
  TransactionInstruction,
  TransactionSignature,
} from "@solana/web3.js";
import {
  calculateCurrentRewardPerToken,
  calculatePendingRewards,
  getReadOnlyWallet,
  SIZE_FARM_STATE,
  SIZE_GLOBAL_CONFIG,
} from "./utils";
import {
  getAssociatedTokenAddress,
  getTreasuryVaultPDA,
  getUserStatePDA,
  collToLamportsDecimal,
  getFarmVaultPDA,
  getFarmAuthorityPDA,
  getRewardVaultPDA,
  lamportsToCollDecimal,
  getTreasuryAuthorityPDA,
  createKeypairRentExemptIx,
  scaleDownWads,
  createAddExtraComputeUnitsTransaction,
} from "./utils";
import { UserState, UserStateFields } from "./rpc_client/accounts";
import { PendingReward, UserFarm } from "./models";
import {
  FarmState,
  FarmStateFields,
  GlobalConfig,
} from "./rpc_client/accounts";
import * as farmOperations from "./utils/operations";
import Decimal from "decimal.js";
import { Keypair } from "@solana/web3.js";
import {
  GlobalConfigOptionKind,
  FarmConfigOptionKind,
  TimeUnit,
} from "./rpc_client/types/index";
import { FarmAndKey, UserAndKey } from "./models";

export const farmsId = new PublicKey(
  "FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr",
);

export class Farms {
  private readonly _connection: Connection;
  private readonly _provider: Provider;
  private readonly _farmsProgram: Program;
  private readonly _farmsProgramId: PublicKey;

  constructor(connection: Connection) {
    this._connection = connection;
    this._provider = new AnchorProvider(connection, getReadOnlyWallet(), {
      commitment: connection.commitment,
    });
    this._farmsProgramId = farmsId;
    this._farmsProgram = new Program(
      FARMS_IDL as Idl,
      this._farmsProgramId,
      this._provider,
    );
  }

  getConnection() {
    return this._connection;
  }

  getProgramID() {
    return this._farmsProgramId;
  }

  getProgram() {
    return this._farmsProgram;
  }

  async getAllUserStatesForUser(user: PublicKey): Promise<Array<UserAndKey>> {
    let filters: GetProgramAccountsFilter[] = [];

    filters.push({
      memcmp: {
        bytes: user.toBase58(),
        offset: 48,
      },
    });

    filters.push({
      dataSize: UserState.layout.span + 8,
    });

    const userStates = (
      await this._farmsProgram.account.userState.all(filters)
    ).map((x) => {
      let res: UserAndKey = {
        userState: new UserState(x.account as unknown as UserStateFields),
        key: x.publicKey,
      };
      return res;
    });

    return userStates;
  }

  async getAllUserStates(): Promise<UserAndKey[]> {
    return (
      await this._farmsProgram.account.userState.all([
        {
          dataSize: UserState.layout.span + 8,
        },
      ])
    ).map((x) => {
      const userAndKey: UserAndKey = {
        userState: new UserState(x.account as unknown as UserStateFields),
        key: x.publicKey,
      };
      return userAndKey;
    });
  }

  async getFarmsForMint(mint: PublicKey): Promise<Array<FarmAndKey>> {
    let filters: GetProgramAccountsFilter[] = [];

    filters.push({
      memcmp: {
        bytes: mint.toBase58(),
        offset: 72,
      },
    });

    filters.push({
      dataSize: FarmState.layout.span + 8,
    });

    const farms = (await this._farmsProgram.account.farmState.all(filters)).map(
      (x) => {
        let res: FarmAndKey = {
          farmState: new FarmState(x.account as unknown as FarmStateFields),
          key: x.publicKey,
        };
        return res;
      },
    );

    return farms;
  }

  async getAllFarmStates(): Promise<FarmAndKey[]> {
    return (
      await this._farmsProgram.account.farmState.all([
        {
          dataSize: FarmState.layout.span + 8,
        },
      ])
    ).map((x): FarmAndKey => {
      const farmAndKey: FarmAndKey = {
        farmState: new FarmState(x.account as unknown as FarmStateFields),
        key: x.publicKey,
      };
      return farmAndKey;
    });
  }

  async getAllFarmStatesByPubkeys(keys: string[]): Promise<FarmAndKey[]> {
    return (
      await this._farmsProgram.account.farmState.all([
        {
          dataSize: FarmState.layout.span + 8,
        },
      ])
    )
      .filter((x) => {
        return keys.includes(x.publicKey.toString());
      })
      .map((x) => {
        const farmState: FarmState = new FarmState(
          x.account as unknown as FarmStateFields,
        );

        return {
          farmState,
          key: x.publicKey,
        };
      });
  }

  async getStakedAmountForMintForFarm(
    mint: PublicKey,
    farm: PublicKey,
  ): Promise<Decimal> {
    const farms = await this.getFarmsForMint(mint);

    for (let index = 0; index < farms.length; index++) {
      if (farms[index].key.toString() === farm.toString()) {
        return lamportsToCollDecimal(
          new Decimal(
            scaleDownWads(farms[index].farmState.totalActiveStakeScaled),
          ),
          farms[index].farmState.token.decimals.toNumber(),
        );
      }
    }
    throw Error("No Farm found");
  }

  async getStakedAmountForMint(mint: PublicKey): Promise<Decimal> {
    const farms = await this.getFarmsForMint(mint);

    let totalStaked = new Decimal(0);
    for (let index = 0; index < farms.length; index++) {
      totalStaked = totalStaked.add(
        lamportsToCollDecimal(
          new Decimal(farms[index].farmState.totalStakedAmount.toString()),
          farms[index].farmState.token.decimals.toNumber(),
        ),
      );
    }

    return totalStaked;
  }

  async getUserStateKeyForUserForFarm(
    user: PublicKey,
    farm: PublicKey,
  ): Promise<Array<PublicKey>> {
    const userStates = await this.getAllUserStatesForUser(user);
    const userStateKeysForFarm: PublicKey[] = [];

    for (let index = 0; index < userStates.length; index++) {
      if (
        userStates[index].userState.farmState.toString() === farm.toString()
      ) {
        userStateKeysForFarm.push(userStates[index].key);
      }
    }

    if (userStateKeysForFarm.length === 0) {
      throw Error("No user state found for user " + user + " for farm " + farm);
    } else {
      return userStateKeysForFarm;
    }
  }

  async getAllFarmsForUser(user: PublicKey): Promise<Map<string, UserFarm>> {
    const userStates = await this.getAllUserStatesForUser(user);

    const farmPks = new Array<string>();
    for (let i = 0; i < userStates.length; i++) {
      farmPks[i] = userStates[i].userState.farmState.toString();
    }

    const farmStates = await this.getAllFarmStatesByPubkeys(farmPks);

    if (!farmStates) {
      throw new Error("Error fetching farms");
    }

    if (farmStates.length === 0) {
      // Return empty if no serializable farm states found
      return new Map<string, UserFarm>();
    }

    const timestamp = new Decimal(
      (await this._connection.getBlockTime(await this._connection.getSlot()))!,
    );

    const userFarms = new Map<string, UserFarm>();

    for (let userState of userStates) {
      const userPendingRewardAmounts: Decimal[] = [];
      let farmState = farmStates.find(
        (farmState) =>
          farmState.key.toString() === userState.userState.farmState.toString(),
      );

      if (!farmState) {
        // Skip farms that are not serializable anymore
        continue;
      }

      let hasReward = false;

      // calculate userState pending rewards
      for (
        let indexReward = 0;
        indexReward < farmState.farmState.rewardInfos.length;
        indexReward++
      ) {
        userPendingRewardAmounts[indexReward] = calculatePendingRewards(
          farmState.farmState,
          userState.userState,
          indexReward,
          timestamp,
        );
        if (userPendingRewardAmounts[indexReward].gt(0)) {
          hasReward = true;
        }
      }

      // add new userFarm state if non empty (has rewards or stake) and not already present
      if (!userFarms.has(userState.userState.farmState.toString())) {
        const userFarm: UserFarm = {
          farm: userState.userState.farmState,
          stakedToken: farmState.farmState.token.mint,
          activeStakeByDelegatee: new Map<string, Decimal>(),
          pendingDepositStakeByDelegatee: new Map<string, Decimal>(),
          pendingWithdrawalUnstakeByDelegatee: new Map<string, Decimal>(),
          pendingRewards: new Array(farmState.farmState.rewardInfos.length)
            .fill(undefined)
            .map(function () {
              return {
                rewardTokenMint: new PublicKey(0),
                cumulatedPendingRewards: new Decimal(0),
                pendingRewardsByDelegatee: new Map<string, Decimal>(),
              };
            }),
        };
        if (
          new Decimal(scaleDownWads(userState.userState.activeStakeScaled)).gt(
            0,
          ) ||
          hasReward
        ) {
          userFarms.set(userState.userState.farmState.toString(), userFarm);
        } else {
          // skip as we are not accounting for empty userFarms
          continue;
        }
      }

      // add new userFarm state if non empty (has rewards or stake) and not already present
      const refUserFarm =
        Object.fromEntries(userFarms)[userState.userState.farmState.toString()];

      if (!refUserFarm) {
        throw new Error("User farm state not loaded properly ");
      }

      const updatedUserFarm = { ...refUserFarm };

      if (
        userState.userState.delegatee.toString() in
        updatedUserFarm.activeStakeByDelegatee
      ) {
        console.error(
          "Delegatee for user for farm already present. There should be only one delegatee for this user for this farm",
        );
        continue;
      }

      // active stake by delegatee
      updatedUserFarm.activeStakeByDelegatee.set(
        userState.userState.delegatee.toString(),
        lamportsToCollDecimal(
          new Decimal(scaleDownWads(userState.userState.activeStakeScaled)),
          farmState.farmState.token.decimals.toNumber(),
        ),
      );

      // pendingDepositStake by delegatee
      updatedUserFarm.pendingDepositStakeByDelegatee.set(
        userState.userState.delegatee.toString(),
        new Decimal(
          scaleDownWads(userState.userState.pendingDepositStakeScaled),
        ),
      );

      // pendingWithdrawalUnstake by delegatee
      updatedUserFarm.pendingWithdrawalUnstakeByDelegatee.set(
        userState.userState.delegatee.toString(),
        new Decimal(
          scaleDownWads(userState.userState.pendingWithdrawalUnstakeScaled),
        ),
      );

      // cummulating rewards
      for (
        let indexReward = 0;
        indexReward < farmState.farmState.rewardInfos.length;
        indexReward++
      ) {
        updatedUserFarm.pendingRewards[indexReward].rewardTokenMint =
          farmState.farmState.rewardInfos[indexReward].token.mint;

        updatedUserFarm.pendingRewards[indexReward].cumulatedPendingRewards =
          updatedUserFarm.pendingRewards[
            indexReward
          ].cumulatedPendingRewards.add(userPendingRewardAmounts[indexReward]);

        updatedUserFarm.pendingRewards[
          indexReward
        ].pendingRewardsByDelegatee.set(
          userState.userState.delegatee.toString(),
          userPendingRewardAmounts[indexReward],
        );
      }

      // set updated userFarm
      userFarms.set(userState.userState.farmState.toString(), updatedUserFarm);
    }

    return userFarms;
  }

  async executeTransaction(
    ix: TransactionInstruction[],
    signer: Keypair,
    extraSigners: Signer[] = [],
  ): Promise<TransactionSignature> {
    const tx = new Transaction();
    let { blockhash } = await this._connection.getLatestBlockhash();
    tx.recentBlockhash = blockhash;
    tx.feePayer = signer.publicKey;
    tx.add(...ix);

    let sig: TransactionSignature = await sendAndConfirmTransaction(
      this._connection,
      tx,
      [signer, ...extraSigners],
      { skipPreflight: true, commitment: "confirmed" },
    );

    return sig;
  }

  async createNewUserIx(
    user: PublicKey,
    farm: PublicKey,
  ): Promise<TransactionInstruction> {
    const userState = getUserStatePDA(this._farmsProgramId, farm, user);

    const ix = farmOperations.initializeUser(farm, user, userState);

    return ix;
  }

  async createNewUser(
    user: Keypair,
    farm: PublicKey,
  ): Promise<TransactionSignature> {
    const ix = await this.createNewUserIx(user.publicKey, farm);

    let sig = await this.executeTransaction([ix], user);
    const userState = getUserStatePDA(
      this._farmsProgramId,
      farm,
      user.publicKey,
    );
    if (process.env.DEBUG === "true") {
      console.log("Initialize User: " + userState);
      console.log("Refresh Farm txn: " + sig.toString());
    }

    return sig;
  }

  async stakeIx(
    user: PublicKey,
    farm: PublicKey,
    amountLamports: Decimal,
    stakeTokenMint: PublicKey,
  ): Promise<TransactionInstruction> {
    const farmVault = getFarmVaultPDA(
      this._farmsProgramId,
      farm,
      stakeTokenMint,
    );
    const userStatePk = await getUserStatePDA(this._farmsProgramId, farm, user);
    const userTokenAta = await getAssociatedTokenAddress(user, stakeTokenMint);

    const ix = farmOperations.stake(
      user,
      userStatePk,
      userTokenAta,
      farm,
      farmVault,
      stakeTokenMint,
      new BN(amountLamports.toString()),
    );
    return ix;
  }

  async stake(
    user: Keypair,
    farm: PublicKey,
    amountLamports: Decimal,
    stakeTokenMint: PublicKey,
  ): Promise<TransactionSignature> {
    const ix = await this.stakeIx(
      user.publicKey,
      farm,
      amountLamports,
      stakeTokenMint,
    );

    let increaseComputeIx = createAddExtraComputeUnitsTransaction(
      user.publicKey,
      400_000,
    );

    let sig = await this.executeTransaction([increaseComputeIx, ix], user);

    if (process.env.DEBUG === "true") {
      console.log("User " + " stake " + amountLamports);
      console.log("Stake txn: " + sig.toString());
    }

    return sig;
  }

  async unstakeIx(
    user: PublicKey,
    farm: PublicKey,
    amountLamports: string,
  ): Promise<TransactionInstruction> {
    const userStatePk = getUserStatePDA(this._farmsProgramId, farm, user);

    const ix = farmOperations.unstake(
      user,
      userStatePk,
      farm,
      new BN(amountLamports),
    );
    return ix;
  }

  async unstake(
    user: Keypair,
    farm: PublicKey,
    sharesAmount: string,
  ): Promise<TransactionSignature> {
    const ix = await this.unstakeIx(user.publicKey, farm, sharesAmount);

    let sig = await this.executeTransaction([ix], user);

    if (process.env.DEBUG === "true") {
      console.log("Unstake " + sharesAmount);
      console.log("Unstake txn: " + sig.toString());
    }

    return sig;
  }

  async withdrawUnstakedDepositIx(
    user: PublicKey,
    userState: PublicKey,
    farmState: PublicKey,
    stakeTokenMint: PublicKey,
  ): Promise<TransactionInstruction> {
    const userTokenAta = await getAssociatedTokenAddress(user, stakeTokenMint);
    const farmVault = getFarmVaultPDA(
      this._farmsProgramId,
      farmState,
      stakeTokenMint,
    );
    const farmVaultsAuthority = getFarmAuthorityPDA(
      this._farmsProgramId,
      farmState,
    );

    const ix = farmOperations.withdrawUnstakedDeposit(
      user,
      userState,
      farmState,
      userTokenAta,
      farmVault,
      farmVaultsAuthority,
    );

    return ix;
  }

  async withdrawUnstakedDeposit(
    user: Keypair,
    farmState: PublicKey,
    tokenMint: PublicKey,
    userState: PublicKey,
  ): Promise<TransactionSignature> {
    const ix = await this.withdrawUnstakedDepositIx(
      user.publicKey,
      userState,
      farmState,
      tokenMint,
    );

    let sig = await this.executeTransaction([ix], user);
    if (process.env.DEBUG === "true") {
      console.log("User " + userState + " withdraw unstaked deposit ");
      console.log("Withdraw Unstaked Deposit txn: " + sig.toString());
    }

    return sig;
  }

  async claimForUserForFarmRewardIx(
    user: PublicKey,
    farm: PublicKey,
    rewardMint: PublicKey,
    rewardIndex = -1,
  ): Promise<TransactionInstruction[]> {
    const userStatePks = await this.getUserStateKeyForUserForFarm(user, farm);
    const farmState = await FarmState.fetch(this._connection, farm);
    if (!farmState) {
      throw new Error(`Farm not found ${farm.toString()}`);
    }

    const userRewardAta = await getAssociatedTokenAddress(user, rewardMint);
    const treasuryVault = getTreasuryVaultPDA(
      this._farmsProgramId,
      farmState.globalConfig,
      rewardMint,
    );
    // find rewardIndex if not defined
    if (rewardIndex === -1) {
      for (let i = 0; farmState.rewardInfos.length; i++) {
        if (
          farmState.rewardInfos[i].token.mint.toString() ==
          rewardMint.toString()
        ) {
          rewardIndex = i;
          break;
        }
      }
    }

    const ixns: TransactionInstruction[] = [];
    for (
      let userStateIndex = 0;
      userStateIndex < userStatePks.length;
      userStateIndex++
    ) {
      const ix = farmOperations.harvestReward(
        user,
        userStatePks[userStateIndex],
        userRewardAta,
        farmState.globalConfig,
        treasuryVault,
        farm,
        farmState.rewardInfos[rewardIndex].rewardsVault,
        farmState.farmVaultsAuthority,
        rewardIndex,
      );
      ixns.push(ix);
    }
    return ixns;
  }

  async claimForUserForFarmReward(
    user: Keypair,
    farm: PublicKey,
    rewardMint: PublicKey,
    rewardIndex = -1,
  ): Promise<TransactionSignature> {
    const ixns = await this.claimForUserForFarmRewardIx(
      user.publicKey,
      farm,
      rewardMint,
      rewardIndex,
    );

    let sig = await this.executeTransaction(ixns, user);

    if (process.env.DEBUG === "true") {
      console.log("Harvest reward " + rewardIndex);
      console.log("HarvestReward txn: " + sig.toString());
    }

    return sig;
  }

  async claimForUserForFarmAllRewardsIx(
    user: PublicKey,
    farm: PublicKey,
  ): Promise<Array<TransactionInstruction>> {
    const farmState = await FarmState.fetch(this._connection, farm);
    const userStatePks = await this.getUserStateKeyForUserForFarm(user, farm);
    const ixs = new Array<TransactionInstruction>();

    if (!farmState) {
      throw new Error(`Farm not found ${farm.toString()}`);
    }

    for (
      let userStateIndex = 0;
      userStateIndex < userStatePks.length;
      userStateIndex++
    ) {
      for (
        let rewardIndex = 0;
        rewardIndex < farmState.numRewardTokens.toNumber();
        rewardIndex++
      ) {
        const userRewardAta = await getAssociatedTokenAddress(
          user,
          farmState.rewardInfos[rewardIndex].token.mint,
        );
        const treasuryVault = getTreasuryVaultPDA(
          this._farmsProgramId,
          farmState.globalConfig,
          farmState.rewardInfos[rewardIndex].token.mint,
        );
        ixs.push(
          farmOperations.harvestReward(
            user,
            userStatePks[userStateIndex],
            userRewardAta,
            farmState.globalConfig,
            treasuryVault,
            farm,
            farmState.rewardInfos[rewardIndex].rewardsVault,
            farmState.farmVaultsAuthority,
            rewardIndex,
          ),
        );
      }
    }

    return ixs;
  }

  async claimForUserForFarmAllRewards(
    user: Keypair,
    farm: PublicKey,
  ): Promise<Array<TransactionSignature>> {
    const ixs = await this.claimForUserForFarmAllRewardsIx(
      user.publicKey,
      farm,
    );
    const sigs = new Array<TransactionSignature>();

    for (let i = 0; i < ixs.length; i++) {
      sigs[i] = await this.executeTransaction([ixs[i]], user);
    }

    return sigs;
  }

  async transferOwnershipIx(
    user: PublicKey,
    userState: PublicKey,
    newUser: PublicKey,
  ): Promise<TransactionInstruction> {
    const ix = farmOperations.transferOwnership(user, userState, newUser);

    return ix;
  }

  async transferOwnership(
    user: Keypair,
    userState: PublicKey,
    newUser: PublicKey,
  ): Promise<TransactionSignature> {
    const ix = await this.transferOwnershipIx(
      user.publicKey,
      userState,
      newUser,
    );

    let sig = await this.executeTransaction([ix], user);

    if (process.env.DEBUG === "true") {
      console.log(
        "Transfer User " +
          userState +
          " ownership from " +
          user.publicKey +
          " to " +
          newUser,
      );
      console.log("Transfer User Ownership txn: " + sig.toString());
    }

    return sig;
  }

  async transferOwnershipAllUserStatesIx(
    user: PublicKey,
    newUser: PublicKey,
  ): Promise<Array<TransactionInstruction>> {
    const userStates = await this.getAllUserStatesForUser(user);

    const ixs = new Array<TransactionInstruction>();
    for (let index = 0; index < userStates.length; index++) {
      ixs[index] = farmOperations.transferOwnership(
        user,
        userStates[index].key,
        newUser,
      );
    }

    return ixs;
  }

  async transferOwnershipAllUserStates(
    user: Keypair,
    newUser: PublicKey,
  ): Promise<Array<TransactionSignature>> {
    const ixs = await this.transferOwnershipAllUserStatesIx(
      user.publicKey,
      newUser,
    );

    const sigs = new Array<TransactionSignature>();
    for (let i = 0; i < ixs.length; i++) {
      sigs[i] = await this.executeTransaction([ixs[i]], user);
    }

    return sigs;
  }

  async createFarmIx(
    admin: PublicKey,
    farm: Keypair,
    globalConfig: PublicKey,
    stakeTokenMint: PublicKey,
  ): Promise<TransactionInstruction[]> {
    const farmVault = getFarmVaultPDA(
      this._farmsProgramId,
      farm.publicKey,
      stakeTokenMint,
    );
    const farmVaultAuthority = getFarmAuthorityPDA(
      this._farmsProgramId,
      farm.publicKey,
    );

    let ixs: TransactionInstruction[] = [];
    ixs.push(
      await createKeypairRentExemptIx(
        this._provider.connection,
        admin,
        farm,
        SIZE_FARM_STATE,
        this._farmsProgramId,
      ),
    );

    ixs.push(
      farmOperations.initializeFarm(
        globalConfig,
        admin,
        farm.publicKey,
        farmVault,
        farmVaultAuthority,
        stakeTokenMint,
      ),
    );
    return ixs;
  }

  async createFarm(
    admin: Keypair,
    globalConfig: PublicKey,
    farm: Keypair,
    mint: PublicKey,
  ): Promise<TransactionSignature> {
    const ix = await this.createFarmIx(
      admin.publicKey,
      farm,
      globalConfig,
      mint,
    );

    let sig = await this.executeTransaction(ix, admin, [farm]);

    if (process.env.DEBUG === "true") {
      console.log("Initialize Farm: " + farm.toString());
      console.log("Initialize Farm txn: " + sig.toString());
    }

    return sig;
  }

  async addRewardToFarmIx(
    admin: PublicKey,
    globalConfig: PublicKey,
    farm: PublicKey,
    mint: PublicKey,
  ): Promise<TransactionInstruction> {
    const globalConfigState = await GlobalConfig.fetch(
      this._connection,
      globalConfig,
    );
    if (!globalConfigState) {
      throw new Error("Could not fetch global config");
    }
    const treasuryVault = getTreasuryVaultPDA(
      this._farmsProgramId,
      globalConfig,
      mint,
    );
    const farmState = await FarmState.fetch(this._connection, farm);
    if (!farmState) {
      throw new Error(`Could not fetch farm state ${farm.toBase58()}`);
    }
    const rewardVault = getRewardVaultPDA(this._farmsProgramId, farm, mint);

    const ix = farmOperations.initializeReward(
      globalConfig,
      globalConfigState.treasuryVaultsAuthority,
      treasuryVault,
      admin,
      farm,
      rewardVault,
      farmState.farmVaultsAuthority,
      mint,
    );
    return ix;
  }

  async addRewardToFarm(
    admin: Keypair,
    globalConfig: PublicKey,
    farm: PublicKey,
    mint: PublicKey,
  ): Promise<TransactionSignature> {
    const ix = await this.addRewardToFarmIx(
      admin.publicKey,
      globalConfig,
      farm,
      mint,
    );

    let sig = await this.executeTransaction([ix], admin);

    if (process.env.DEBUG === "true") {
      console.log("Initialize Reward: " + mint);
      console.log("Initialize Reward txn: " + sig.toString());
    }

    return sig;
  }

  async addRewardAmountToFarmIx(
    admin: PublicKey,
    farm: PublicKey,
    mint: PublicKey,
    amount: Decimal,
  ): Promise<TransactionInstruction> {
    const farmState = await FarmState.fetch(this._connection, farm);
    if (!farmState) {
      throw new Error(`Could not fetch farm state ${farm.toBase58()}`);
    }
    let amountLamports = new BN(
      collToLamportsDecimal(
        amount,
        farmState.token.decimals.toNumber(),
      ).toString(),
    );
    const adminRewardAta = await getAssociatedTokenAddress(admin, mint);

    let rewardIndex = -1;
    for (let i = 0; farmState.rewardInfos.length; i++) {
      if (farmState.rewardInfos[i].token.mint.toString() === mint.toString()) {
        rewardIndex = i;
        break;
      }
    }

    const ix = farmOperations.addReward(
      admin,
      farm,
      farmState.rewardInfos[rewardIndex].rewardsVault,
      farmState.farmVaultsAuthority,
      adminRewardAta,
      mint,
      rewardIndex,
      amountLamports,
    );
    return ix;
  }

  async addRewardAmountToFarm(
    admin: Keypair,
    farm: PublicKey,
    mint: PublicKey,
    amount: Decimal,
  ): Promise<TransactionSignature> {
    const ix = await this.addRewardAmountToFarmIx(
      admin.publicKey,
      farm,
      mint,
      amount,
    );

    let sig = await this.executeTransaction([ix], admin);

    if (process.env.DEBUG === "true") {
      console.log("Add Reward: " + mint + " amount: " + amount);
      console.log("Add Reward txn: " + sig.toString());
    }

    return sig;
  }

  async updateRewardToFarmIx(
    admin: PublicKey,
    farm: PublicKey,
    mint: PublicKey,
    mode: FarmConfigOptionKind,
    value: number,
  ): Promise<TransactionInstruction> {
    const farmState = await FarmState.fetch(this._connection, farm);
    let rewardIndex = -1;
    if (!farmState) {
      throw new Error(`Could not fetch farm state ${farm.toBase58()}`);
    }
    for (let i = 0; farmState.rewardInfos.length; i++) {
      if (farmState.rewardInfos[i].token.mint.toString() === mint.toString()) {
        rewardIndex = i;
        break;
      }
    }

    const ix = farmOperations.updateRewardConfig(
      admin,
      farm,
      rewardIndex,
      mode,
      value,
    );
    return ix;
  }

  async updateRewardToFarm(
    admin: Keypair,
    farm: PublicKey,
    mint: PublicKey,
    mode: FarmConfigOptionKind,
    value: number,
  ): Promise<TransactionSignature> {
    const ix = await this.updateRewardToFarmIx(
      admin.publicKey,
      farm,
      mint,
      mode,
      value,
    );

    let sig = await this.executeTransaction([ix], admin);

    if (process.env.DEBUG === "true") {
      console.log(
        "Update Reward: " +
          mint +
          " mode: " +
          mode.discriminator +
          " value: " +
          value,
      );
      console.log("Update Reward txn: " + sig.toString());
    }

    return sig;
  }

  async refreshFarmIx(farm: PublicKey): Promise<TransactionInstruction> {
    const ix = farmOperations.refreshFarm(farm);

    return ix;
  }

  async refreshFarm(
    payer: Keypair,
    farm: PublicKey,
  ): Promise<TransactionSignature> {
    const ix = await this.refreshFarmIx(farm);

    let sig = await this.executeTransaction([ix], payer);

    if (process.env.DEBUG === "true") {
      console.log("Refresh Farm: " + farm);
      console.log("Refresh Farm txn: " + sig.toString());
    }

    return sig;
  }

  async refreshUserIx(
    userState: PublicKey,
    farmState: PublicKey,
  ): Promise<TransactionInstruction> {
    const ix = farmOperations.refreshUserState(userState, farmState);

    return ix;
  }

  async refreshUser(
    payer: Keypair,
    userState: PublicKey,
    farmState: PublicKey,
  ): Promise<TransactionSignature> {
    const ix = await this.refreshUserIx(userState, farmState);

    let sig = await this.executeTransaction([ix], payer);

    if (process.env.DEBUG === "true") {
      console.log("Refresh User: " + userState);
      console.log("Refresh User txn: " + sig.toString());
    }

    return sig;
  }

  async createGlobalConfigIxs(
    admin: PublicKey,
    globalConfig: Keypair,
  ): Promise<TransactionInstruction[]> {
    let ixs: TransactionInstruction[] = [];

    ixs.push(
      await createKeypairRentExemptIx(
        this._provider.connection,
        admin,
        globalConfig,
        SIZE_GLOBAL_CONFIG,
        this._farmsProgramId,
      ),
    );

    const treasuryVaultAuthority = getTreasuryAuthorityPDA(
      this._farmsProgramId,
      globalConfig.publicKey,
    );

    ixs.push(
      farmOperations.initializeGlobalConfig(
        admin,
        globalConfig.publicKey,
        treasuryVaultAuthority,
      ),
    );

    return ixs;
  }

  async createGlobalConfig(
    admin: Keypair,
    globalConfig: Keypair,
  ): Promise<TransactionSignature> {
    const ix = await this.createGlobalConfigIxs(admin.publicKey, globalConfig);
    const sig = await this.executeTransaction(ix, admin, [globalConfig]);

    if (process.env.DEBUG === "true") {
      console.log("Initialize Global Config: " + globalConfig.toString());
      console.log("Initialize Global Config txn: " + sig.toString());
    }

    return sig;
  }

  async updateGlobalConfigIx(
    admin: PublicKey,
    globalConfig: PublicKey,
    mode: GlobalConfigOptionKind,
    flagValue: string,
    flagValueType: string,
  ): Promise<TransactionInstruction> {
    const ix = farmOperations.updateGlobalConfig(
      admin,
      globalConfig,
      mode,
      flagValue,
      flagValueType,
    );

    return ix;
  }

  async updateGlobalConfig(
    admin: Keypair,
    globalConfig: PublicKey,
    mode: GlobalConfigOptionKind,
    flagValue: string,
    flagValueType: string,
  ): Promise<TransactionSignature> {
    const ix = await this.updateGlobalConfigIx(
      admin.publicKey,
      globalConfig,
      mode,
      flagValue,
      flagValueType,
    );

    const sig = await this.executeTransaction([ix], admin);

    if (process.env.DEBUG === "true") {
      console.log(
        "Update Global Config: " +
          globalConfig.toString() +
          " mode: " +
          mode.discriminator +
          " value: " +
          flagValue,
      );
      console.log("Update Global Config txn: " + sig.toString());
    }

    return sig;
  }

  async withdrawTreasuryIx(
    admin: PublicKey,
    globalConfig: PublicKey,
    rewardMint: PublicKey,
    amount: BN,
    withdrawAta?: PublicKey,
  ): Promise<TransactionInstruction> {
    const treasuryVault = getTreasuryVaultPDA(
      this._farmsProgramId,
      globalConfig,
      rewardMint,
    );
    const treasuryVaultAuthority = getTreasuryAuthorityPDA(
      this._farmsProgramId,
      globalConfig,
    );
    if (!withdrawAta) {
      withdrawAta = await getAssociatedTokenAddress(admin, rewardMint);
    }

    const ix = farmOperations.withdrawTreasury(
      admin,
      globalConfig,
      treasuryVault,
      treasuryVaultAuthority,
      withdrawAta,
      amount,
      rewardMint,
    );

    return ix;
  }

  async withdrawTreasury(
    admin: Keypair,
    globalConfig: PublicKey,
    rewardMint: PublicKey,
    amount: BN,
    withdrawAta?: PublicKey,
  ): Promise<TransactionSignature> {
    const ix = await this.withdrawTreasuryIx(
      admin.publicKey,
      globalConfig,
      rewardMint,
      amount,
      withdrawAta,
    );

    const sig = await this.executeTransaction([ix], admin);

    if (process.env.DEBUG === "true") {
      console.log(
        "Admin " +
          admin.publicKey +
          " withdraw treasury of " +
          rewardMint +
          " an amount of " +
          amount,
      );
      console.log("Withdraw treasury txn: " + sig.toString());
    }

    return sig;
  }
}

export async function getCurrentTimeUnit(
  conn: Connection,
  farm: FarmState,
): Promise<Decimal> {
  const slot = await conn.getSlot();
  const timestamp = await conn.getBlockTime(slot);

  if (farm.timeUnit == TimeUnit.Seconds.discriminator) {
    return new Decimal(timestamp!);
  } else {
    return new Decimal(slot);
  }
}

export async function getCurrentRps(
  conn: Connection,
  farm: FarmState,
  rewardIndex: number,
): Promise<number> {
  const currentTimeUnit = new Decimal(await getCurrentTimeUnit(conn, farm));
  return calculateCurrentRewardPerToken(farm, rewardIndex, currentTimeUnit);
}
