// @ts-ignore
import * as anchor from '@project-serum/anchor';

import { TOKEN_PROGRAM_ID} from "@solana/spl-token";
const { BN, web3, Program, AnchorProvider } = anchor
const { PublicKey, SystemProgram, Transaction } = web3
const utf8 = anchor.utils.bytes.utf8;
const defaultAccounts = {
  tokenProgram: TOKEN_PROGRAM_ID,
  clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
  systemProgram: SystemProgram.programId,
}

import {IDL as idl, UnlocStaking} from '../types/unloc_staking'
import { Connection, Keypair } from '@solana/web3.js';
import { createAssociatedTokenAccountIfNotExist2, sendTransaction } from '../utils';
import { STAKING_PID } from '../global-config';
let StakingProgram:anchor.Program<UnlocStaking> = null as unknown as anchor.Program<UnlocStaking>

export function initStakingProgram(
  connection: Connection,
  wallet: any,
  programId: any = STAKING_PID
) {

  const provider = new anchor.AnchorProvider(
    connection,
    wallet,
    {
      commitment: 'confirmed',
    }
    
  );
  // Generate the program client from IDL.
  const program = new (anchor as any).Program(idl, programId, provider) as anchor.Program<UnlocStaking>;
  StakingProgram = program

}

export interface ExtraRewardConfigs{
  duration: any
  extraPercentage: any
}

export async function getStakingStateAddress(){
  const [stateSigner, stateBump] = await anchor.web3.PublicKey.findProgramAddress(
    [utf8.encode('state')],
    StakingProgram.programId
  );
  return stateSigner
}

async function getStakingExtraRewardAddress(){
  const [stateSigner, stateBump] = await anchor.web3.PublicKey.findProgramAddress(
    [utf8.encode('extra')],
    StakingProgram.programId
  );
  return stateSigner
}

async function getStakingPoolAddressFromMint(mint:string){
  const [stateSigner, stateBump] = await anchor.web3.PublicKey.findProgramAddress(
    [new PublicKey(mint).toBuffer()],
    StakingProgram.programId
  );
  return stateSigner
}
const ACC_PRECISION = new BN(100 * 1000 * 1000 * 1000);
const FULL_100 = new BN (100 * 1000 * 1000 * 1000);
export function estimateRewards(
  stateData:any,
  extraConfigData:any,
  poolData:any,
  userData:any,
){
  const currentTimeStamp = Math.ceil(new Date().getTime() / 1000);

  const duration = new BN(Math.max(currentTimeStamp - poolData.lastRewardTime, 0))

  const reward_per_share = stateData.tokenPerSecond.mul(duration).mul(ACC_PRECISION).div(poolData.amount);
  const acc_reward_per_share = poolData.accRewardPerShare.add(reward_per_share);

  let extraPercentage = new BN(0)
  extraConfigData.configs.forEach((item:any)=>{
    if(item.duration.toString() === userData.lockDuration.toString())
    {
      extraPercentage = item.extraPercentage
      return;
    }
  })

  const pending_amount = userData.amount.mul( acc_reward_per_share).div(ACC_PRECISION).sub(userData.rewardDebt);
  const extra_amount = userData.extraReward.add(pending_amount.mul(extraPercentage).div(FULL_100));
  const total_reward = userData.rewardAmount.add(pending_amount).add(extra_amount)

  return total_reward.toString()
}


export function estimateStakingRewardsPerSec(
  stateData:any,
  extraConfigData:any,
  poolData:any,
  userData:any,
){
  const currentTimeStamp = Math.ceil(new Date().getTime() / 1000);

  const duration = new BN(Math.max(currentTimeStamp - poolData.lastRewardTime, 0))

  const reward_per_share = stateData.tokenPerSecond.mul(duration).mul(ACC_PRECISION).div(poolData.amount);
  const acc_reward_per_share = poolData.accRewardPerShare.add(reward_per_share);

  let extraPercentage = new BN(0)
  extraConfigData.configs.forEach((item:any)=>{
    if(item.duration.toString() === userData.lockDuration.toString())
    {
      extraPercentage = item.extraPercentage
      return;
    }
  })

  const pending_amount = userData.amount.mul( acc_reward_per_share).div(ACC_PRECISION).sub(userData.rewardDebt);
  const extra_amount = userData.extraReward.add(pending_amount.mul(extraPercentage).div(FULL_100));
  const total_reward = userData.rewardAmount.add(pending_amount).add(extra_amount)
  return ((total_reward / 1000000000) / ((currentTimeStamp - poolData.lastRewardTime.toString()) * 1000));
}

export async function createStakingState(
  connection:Connection,
  wallet: any,
  tokenPerSecond: number,
  earlyUnlocFee: number,
  profileLevels: anchor.BN[],
  rewardMint: string,
)
{
  const transaction = new Transaction()

  const [stateSigner, stateBump] = await anchor.web3.PublicKey.findProgramAddress(
    [utf8.encode('state')],
    StakingProgram.programId
  );

  const stateRewardVault = await createAssociatedTokenAccountIfNotExist2(
    null,
    stateSigner,
    wallet.publicKey,
    rewardMint,
    transaction
  )

  const tx = await StakingProgram.rpc.createState(stateBump, new BN(tokenPerSecond),new BN(earlyUnlocFee), profileLevels, {
      accounts: {
        state: stateSigner,
        rewardMint: rewardMint,
        rewardVault: stateRewardVault,
        feeVault: stateRewardVault,
        authority: wallet.publicKey,
        ...defaultAccounts
      },
      preInstructions: transaction.instructions
    })
  return tx;
}

export async function getStakingState(){
  const stateSigner = await getStakingStateAddress();
  return await StakingProgram.account.stateAccount.fetch(stateSigner)
}

export async function getStakingExtraRewardConfigs(){
  const [extraRewardSigner, extraRewardBump] = await anchor.web3.PublicKey.findProgramAddress(
    [utf8.encode('extra')],
    StakingProgram.programId
  );
  const extraRewardConfigs = await StakingProgram.account.extraRewardsAccount.fetchNullable(extraRewardSigner)
  // assert.ok(extraRewardConfigs.configs.length === 3)
  // assert.ok(new BN(1).eq(extraRewardConfigs.configs[1].duration))
  // assert.ok(getNumber(50).eq(extraRewardConfigs.configs[1].extraPercentage))
  return extraRewardConfigs
}


export async function createStakingExtraReward(
  connection:Connection,
  wallet: any,
){
  const transaction = new Transaction()
  const signers: Keypair[] = []

  const [extraRewardSigner, extraRewardBump] = await anchor.web3.PublicKey.findProgramAddress(
    [utf8.encode('extra')],
    StakingProgram.programId
  );
  console.log(extraRewardSigner.toString(), extraRewardBump)

  const tx = await StakingProgram.instruction.createExtraRewardConfigs(extraRewardBump,
    [
      { duration: new BN(43200 * 60), extraPercentage: new BN(0) },
      { duration: new BN(129600 * 60), extraPercentage: new BN(10 * 1000 * 1000 * 1000) },
      { duration: new BN(259200 * 60), extraPercentage: new BN(30 * 1000 * 1000 * 1000) },
      { duration: new BN(525600 * 60), extraPercentage: new BN(100 * 1000 * 1000 * 1000) }
    ],
    {
      accounts: {
        extraRewardAccount: extraRewardSigner,
        authority: wallet.publicKey,
        ...defaultAccounts
      },
    })
  transaction.add(tx)
  return await sendTransaction(connection, wallet, transaction, signers)
}

export async function setStakingExtraReward(
  connection:Connection,
  wallet: any,
  extraRewards: ExtraRewardConfigs[],
)
{

  const transaction = new Transaction()
  const signers: Keypair[] = []

  const [extraRewardSigner, extraRewardBump] = await anchor.web3.PublicKey.findProgramAddress(
    [utf8.encode('extra')],
    StakingProgram.programId
  );

  transaction.add(StakingProgram.instruction.setExtraRewardConfigs(extraRewards, {
    accounts: {
      extraRewardAccount: extraRewardSigner,
      authority: wallet.publicKey,
      ...defaultAccounts
    },
  }))
  return await sendTransaction(connection, wallet, transaction, signers)

}


export async  function  fundToStakingProgram(
  connection:Connection,
  wallet: any,
  poolSigner: string,
  masterRewardVault:string,
  amount:any
) {

  const transaction = new Transaction()
  const signers: Keypair[] = []

  const stateSigner = await getStakingStateAddress()
  const state = await getStakingState()
  // await rewardMint.mintTo(stateRewardVault, wallet, [provider.wallet], getNumber(10000).toString())
  const tx = StakingProgram.instruction.fundRewardToken(new BN(amount), {
    accounts: {
      state: stateSigner,
      rewardVault: state.rewardVault,
      userVault: masterRewardVault,
      authority: wallet.publicKey,
      ...defaultAccounts
    }
  })
  transaction.add(tx)
  return await sendTransaction(connection, wallet, transaction, signers)

}


export async function getAllStakingPools(){
  const pools = await StakingProgram.account.farmPoolAccount.all()
  return pools
}

export async function getFirstStakingPool(){
  const pools = await StakingProgram.account.farmPoolAccount.all()
  return pools[0]
}

export async function createStakingPool(
  connection:Connection,
  wallet: any,
  rewardMint: string,
  pools: any[]
) {

  const transaction = new Transaction()
  const signers: Keypair[] = []
  

  const [poolSigner, poolBump] = await anchor.web3.PublicKey.findProgramAddress(
    [new PublicKey(rewardMint).toBuffer()],
    StakingProgram.programId
  );
  const stateSigner = await getStakingStateAddress()

  const poolVault = await createAssociatedTokenAccountIfNotExist2(
    null, 
    poolSigner, 
    wallet.publicKey,  
    rewardMint, 
    transaction)


  // let pools = await StakingProgram.account.farmPoolAccount.all()
  transaction.add(
    StakingProgram.instruction.createPool(poolBump, new BN('0'), new BN('0'), {
      accounts: {
        pool: poolSigner,
        state: stateSigner,
        mint: rewardMint,
        vault: poolVault,
        authority: wallet.publicKey,
        ...defaultAccounts
      },
      remainingAccounts: pools.map(p => ({
        pubkey: p.publicKey,
        isWritable: true,
        isSigner: false
      }))
    })
  )
  // let stateInfo = await program.account.stateAccount.fetch(stateSigner)
  // let poolInfo = await program.account.farmPoolAccount.fetch(poolSigner)
  // assert.ok(poolInfo.point.eq(stateInfo.totalPoint))
  // assert.ok(poolInfo.point.eq(new BN('0')))
  // assert.ok(poolInfo.amountMultipler.eq(new BN(0)))
  return await sendTransaction(connection, wallet, transaction, signers)
}
export async function closeStakingPool(
  connection:Connection,
  wallet: any,
  poolSigner:string,
  pools:any[]
){
  const transaction = new Transaction()
  const signers: Keypair[] = []

  const stateSigner = await getStakingStateAddress()
  
  StakingProgram.instruction.closePool({
    accounts: {
      pool: new PublicKey(poolSigner),
      state: stateSigner,
      authority: wallet,
      ...defaultAccounts
    },
    remainingAccounts: pools.map(p => ({
      pubkey: p.publicKey,
      isWritable: true,
      isSigner: false
    }))
  })
  return await sendTransaction(connection, wallet, transaction, signers)
}

export async function changeStakingPoolAmountMultipler(
  connection:Connection,
  wallet: any,
  poolSigner:string,
) {
  
  const transaction = new Transaction()
  const signers: Keypair[] = []

  const stateSigner = await getStakingStateAddress()

  const tx = StakingProgram.instruction.changePoolAmountMultipler(new BN(1), {
    accounts: {
      pool: new PublicKey(poolSigner),
      state: stateSigner,
      authority: wallet.publicKey,
      ...defaultAccounts
    }
  })

  transaction.add(tx);

  return await sendTransaction(connection, wallet, transaction, signers)
}
export async function changeStakingTokenPerSecond(
  connection:Connection,
  wallet: any,
  pools:any[],
  tokenPerSecond: number
){
  const transaction = new Transaction()
  const signers: Keypair[] = []

  const stateSigner = await getStakingStateAddress()
  const tx = StakingProgram.instruction.changeTokensPerSecond(new BN(tokenPerSecond), {
    accounts: {
      state: stateSigner,
      authority: wallet.publicKey,
      ...defaultAccounts
    },
    remainingAccounts: pools.map(p => ({
      pubkey: p.publicKey,
      isWritable: true,
      isSigner: false
    }))
  })

  transaction.add(tx);
  return await sendTransaction(connection, wallet, transaction, signers)
}
export async function changeStakingPoolPoint(
  connection:Connection,
  wallet: any,
  poolSigner: string,
) {

  const transaction = new Transaction()
  const signers: Keypair[] = []
  
  const stateSigner = await getStakingStateAddress()

  let pools = await getAllStakingPools()
  
  const ix = StakingProgram.instruction.changePoolPoint(new BN(1000), {
    accounts: {
      pool: new PublicKey(poolSigner),
      state: stateSigner,
      authority: wallet.publicKey,
      ...defaultAccounts
    },
    remainingAccounts: pools.map((p : any) => ({
      pubkey: p.publicKey,
      isWritable: true,
      isSigner: false
    }))
  })
  transaction.add(ix);
  return await sendTransaction(connection, wallet, transaction, signers)
}

export async function getStakingPoolUserAccount(
  wallet: any,
  poolSigner: any,
){
  
  const [poolUserAccount, bump1] = await PublicKey.findProgramAddress([
    poolSigner.toBuffer(), wallet.publicKey.toBuffer()
  ], StakingProgram.programId)

  return await StakingProgram.account.farmPoolUserAccount.fetchNullable(poolUserAccount)
}

const ONE_YEAR_SECOND = 365 * 24 * 3600;
export const TIERS_XCRP = [0, 200, 2000]
export function calculateTiers(amount: number, lockDuration: number)
{
  const rate = Number((lockDuration / ONE_YEAR_SECOND).toFixed(3));
  const xCRP = rate * amount;
  let i = 0;
  for(; i < TIERS_XCRP.length; i ++)
  {
    if (TIERS_XCRP[i] > xCRP) {
      break;
    }
  }
  i --;
  return {
    xCRP: Number(xCRP.toFixed(2)), 
    tiers: i,
  };
}

export async function stake (
  connection:Connection,
  wallet: any,

  poolSigner:string,

  rewardMint: string,
  poolVault: string,

  rewardUserVault: string,

  amount:any, 
  lock = 0,
  signers: Keypair[] = []
) {

  const transaction = new Transaction()

  const stateSigner = await getStakingStateAddress()
  const state = await getStakingState()
  const extraRewardSigner = await getStakingExtraRewardAddress()
  // const poolSigner = await getPoolAddressFromMint(rewardMint)

  const [userAccount, bump1] = await PublicKey.findProgramAddress([
    new PublicKey(poolSigner).toBuffer(), wallet.publicKey.toBuffer()
  ], StakingProgram.programId)
  
  const userAccountData = await StakingProgram.account.farmPoolUserAccount.fetchNullable(userAccount)
  if(!userAccountData){
    console.log("You are the new user to stake")
    transaction.add( StakingProgram.instruction.createUser(bump1, {
      accounts: {
        user: userAccount,
        state: stateSigner,
        pool: poolSigner,
        authority: wallet.publicKey,
        ...defaultAccounts
      }
    }));
  }
  try {
    const txHash = await StakingProgram.rpc.stake(new BN(amount), new BN(lock), {
      accounts: {
        mint: rewardMint,
        extraRewardAccount: extraRewardSigner,
        poolVault: poolVault,
        userVault: rewardUserVault,
        feeVault: state.feeVault,
        user: userAccount,
        state: stateSigner,
        pool: poolSigner,
        authority: wallet.publicKey,
        ...defaultAccounts
      },
      preInstructions: transaction.instructions,
      signers
    });
    return txHash
  } catch(e) {
    console.log(e);
  }
  return '';
}

export async function createStakingUser (
  connection:Connection,
  wallet: any,

  poolSigner:string,
  signers: Keypair[] = []
) {

  const stateSigner = await getStakingStateAddress()

  const [userAccount, bump1] = await PublicKey.findProgramAddress([
    new PublicKey(poolSigner).toBuffer(), wallet.publicKey.toBuffer()
  ], StakingProgram.programId)
  
  const userAccountData = await StakingProgram.account.farmPoolUserAccount.fetchNullable(userAccount)
  if(!userAccountData){
    console.log("You are the new user to stake")
    try{
      const tx = await StakingProgram.rpc.createUser(bump1, {
        accounts: {
          user: userAccount,
          state: stateSigner,
          pool: poolSigner,
          authority: wallet.publicKey,
          ...defaultAccounts
        },
        signers
      });
      return tx;
    } catch(e) {
      console.log(e);
      return null
    }
  }
  return null
  
}

export async function unstake (
  connection:Connection,
  wallet: any,
  
  poolSigner:string,
  rewardMint: string,
  poolVault: string,
  rewardUserVault:string,
  amount: number,
  signers: Keypair[] = []
) {

  const stateSigner = await getStakingStateAddress()
  const programState = await getStakingState()
  
  const extraRewardSigner = await getStakingExtraRewardAddress()
  // const poolSigner = await getPoolAddressFromMint(rewardMint)

  const [poolUserAccount, bump1] = await PublicKey.findProgramAddress([
    new PublicKey(poolSigner).toBuffer(), wallet.publicKey.toBuffer()
  ], StakingProgram.programId)

  try {
    const txHash = await StakingProgram.rpc.unstake(new BN(amount), {
      accounts: {
        mint: rewardMint,
        extraRewardAccount: extraRewardSigner,
        poolVault: poolVault,
        userVault: rewardUserVault,
        feeVault: programState.feeVault,
        user: poolUserAccount,
        state: stateSigner,
        pool: poolSigner,
        authority: wallet.publicKey,
        ...defaultAccounts
      },
      signers
    })
    return txHash;
  } catch(e) {
    console.log(e);
    return null
  }
}

export async function harvest (
  connection:Connection,
  wallet: any,
  poolSigner: string,
  rewardMint: string,
  rewardUserVault: string,
  stateRewardVault: string,
  signers: Keypair[] = []
) {

  const stateSigner = await getStakingStateAddress()
  const extraRewardSigner = await getStakingExtraRewardAddress()
  // const poolSigner = await getPoolAddressFromMint(rewardMint)

  const [poolUserAccount, bump1] = await PublicKey.findProgramAddress([
    new PublicKey(poolSigner).toBuffer(), wallet.publicKey.toBuffer()
  ], StakingProgram.programId)


  try {
    const txHash = await StakingProgram.rpc.harvest({
      accounts: {
        mint: rewardMint,
        extraRewardAccount: extraRewardSigner,
        rewardVault: stateRewardVault,
        userVault: rewardUserVault,
        user: poolUserAccount,
        state: stateSigner,
        pool: poolSigner,
        authority: wallet.publicKey,
        ...defaultAccounts
      },
      signers
    });
    return txHash;
  } catch(e) {
    console.log(e);
    return null
  }
}
