/**
 * Copyright (c) 2025, Everstake.
 * Licensed under the BSD-3-Clause License. See LICENSE file for details.
 */

import {
  Address,
  Account,
  createAddressWithSeed,
  createDefaultRpcTransport,
  createSolanaRpcFromTransport,
  mainnet,
  devnet,
  ClusterUrl,
  address,
  TransactionMessageWithBlockhashLifetime,
  createNoopSigner,
  pipe,
  CompilableTransactionMessage,
  createTransactionMessage,
  setTransactionMessageFeePayer,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstruction,
  RpcFromTransport,
  SolanaRpcApiFromTransport,
  RpcTransportFromClusterUrl,
  IInstruction,
  generateKeyPair,
  createSignerFromKeyPair,
  partiallySignTransactionMessageWithSigners,
  parseBase64RpcAccount,
  prependTransactionMessageInstruction,
} from '@solana/kit';

import {
  getCreateAccountWithSeedInstruction,
  getCreateAccountInstruction,
  getTransferSolInstruction,
  getAllocateWithSeedInstruction,
} from '@solana-program/system';

import {
  getSetComputeUnitLimitInstruction,
  getSetComputeUnitPriceInstruction,
} from '@solana-program/compute-budget';

import { Blockchain } from '../../utils';
import { ERROR_MESSAGES } from './constants/errors';
import {
  DEVNET_VALIDATOR_ADDRESS,
  FILTER_DATA_SIZE,
  FILTER_OFFSET,
  MAINNET_VALIDATOR_ADDRESS,
  MIN_AMOUNT,
  Network,
  StakeState,
  STAKE_ACCOUNT_V2_SIZE,
  ADDRESS_DEFAULT,
  STAKE_HISTORY_ACCOUNT,
  STAKE_CONFIG_ACCOUNT,
  MAX_DEACTIVATE_ACCOUNTS_WITH_SPLIT,
  MAX_CLAIM_ACCOUNTS,
  MAX_DEACTIVATE_ACCOUNTS,
} from './constants';
import {
  ApiResponse,
  CreateAccountResponse,
  ClaimResponse,
  StakeResponse,
  Delegations,
  UnstakeResponse,
  Params,
  RpcConfig,
} from './types';

import {
  getWithdrawInstruction,
  getDelegateStakeInstruction,
  getDeactivateInstruction,
  getInitializeInstruction,
  getSplitInstruction,
  STAKE_PROGRAM_ADDRESS,
  decodeStakeStateAccount,
  StakeStateAccount,
  StakeStateV2,
} from '@solana-program/stake';

/**
 * The `Solana` class extends the `Blockchain` class and provides methods for interacting with the Solana blockchain.
 *
 * @property connection - The connection to the Solana blockchain.
 * @property ERROR_MESSAGES - The error messages for the Solana class.
 * @property ORIGINAL_ERROR_MESSAGES - The original error messages for the Solana class.

 * @throws Throws an error if there's an issue establishing the connection.
 */
export class Solana extends Blockchain {
  private connection!: RpcFromTransport<
    SolanaRpcApiFromTransport<RpcTransportFromClusterUrl<ClusterUrl>>,
    RpcTransportFromClusterUrl<ClusterUrl>
  >;
  private validator: Address;
  protected ERROR_MESSAGES = ERROR_MESSAGES;
  protected ORIGINAL_ERROR_MESSAGES = {};

  constructor(network: Network = Network.Mainnet, rpcConfig: RpcConfig = {}) {
    super();
    if (rpcConfig.rpc && !this.isValidURL(rpcConfig.rpc)) {
      throw this.throwError('INVALID_RPC_ERROR');
    }

    switch (network) {
      case Network.Mainnet:
        rpcConfig.rpc =
          rpcConfig.rpc || mainnet('https://api.mainnet-beta.solana.com');
        this.validator = MAINNET_VALIDATOR_ADDRESS;
        break;
      case Network.Devnet:
        rpcConfig.rpc =
          rpcConfig.rpc || devnet('https://api.devnet.solana.com');
        this.validator = DEVNET_VALIDATOR_ADDRESS;
        break;
      default:
        throw this.throwError('UNSUPPORTED_NETWORK_ERROR');
    }

    try {
      const transport = createDefaultRpcTransport({
        url: rpcConfig.rpc,
        headers: {
          'User-Agent': rpcConfig.userAgent || '',
        },
      });

      this.connection = createSolanaRpcFromTransport(transport);
    } catch (error) {
      throw this.handleError('CONNECTION_ERROR', error);
    }
  }

  /**
   * Creates a new stake account.
   *
   * @param address - The public key of the account as PublicKey.
   * @param lamports  - The amount to stake in lamports.
   * @param source  - stake source
   * @param lockup - stake account lockup
   *
   * @throws  Throws an error if the lamports is less than the minimum amount.
   * @throws  Throws an error if there's an issue creating the stake account.
   *
   * @returns Returns a promise that resolves with the versioned transaction of the stake account creation and the public key of the stake account.
   *
   */
  public async createAccount(
    sender: string,
    amountInLamports: bigint,
    source: string,
    // lockup: Lockup | null = Lockup.default,
    params?: Params,
  ): Promise<ApiResponse<CreateAccountResponse>> {
    // Check if the amount is greater than or equal to the minimum amount
    if (amountInLamports < MIN_AMOUNT) {
      this.throwError('MIN_AMOUNT_ERROR', MIN_AMOUNT.toString());
    }

    try {
      // Get the minimum balance for rent exemption
      const minimumRent = await this.connection
        .getMinimumBalanceForRentExemption(
          //TODO get from account when it's would be available
          BigInt(STAKE_ACCOUNT_V2_SIZE),
        )
        .send();

      //  lockup = lockup || Lockup.default;

      const [
        createAccountInstruction,
        initializeInstruction,
        stakeAccountPubkey,
      ] =
        source === null
          ? // TODO fix create account sign
            await this.createAccountTx(
              address(sender),
              BigInt(amountInLamports) + minimumRent,
              // lockup,
            )
          : await this.createAccountWithSeedTx(
              address(sender),
              BigInt(amountInLamports) + minimumRent,
              source,
              // lockup,
            );

      let transactionMessage = await this.baseTx(sender, params);
      transactionMessage = appendTransactionMessageInstruction(
        createAccountInstruction,
        transactionMessage,
      );
      transactionMessage = appendTransactionMessageInstruction(
        initializeInstruction,
        transactionMessage,
      );
      const signedTransactionMessage =
        source === null
          ? await partiallySignTransactionMessageWithSigners(transactionMessage)
          : transactionMessage;

      return {
        result: {
          transaction: signedTransactionMessage,
          stakeAccount: stakeAccountPubkey,
        },
      };
    } catch (error) {
      throw this.handleError('CREATE_ACCOUNT_ERROR', error);
    }
  }

  /**
   * Delegates a specified amount from a stake account to a validator.
   *
   * @param address - The public key of the account.
   * @param lamports - The amount in lamports to be delegated.
   * @param stakeAccount - The public key of the stake account.
   *
   * @throws Throws an error if the amount is less than the minimum amount, or if there's an issue during the delegation process.
   *
   * @returns Returns a promise that resolves with the delegation transaction.
   *
   */
  public async delegate(
    sender: string,
    lamports: bigint,
    stakeAccount: string,
    params?: Params,
  ): Promise<ApiResponse<TransactionMessageWithBlockhashLifetime>> {
    if (lamports < MIN_AMOUNT) {
      this.throwError('MIN_AMOUNT_ERROR', MIN_AMOUNT.toString());
    }

    try {
      const delegateInstruction = getDelegateStakeInstruction({
        stake: address(stakeAccount),
        vote: this.validator,
        stakeHistory: STAKE_HISTORY_ACCOUNT,
        unused: STAKE_CONFIG_ACCOUNT,
        stakeAuthority: createNoopSigner(address(sender)),
      });

      let transactionMessage = await this.baseTx(sender, params);
      transactionMessage = appendTransactionMessageInstruction(
        delegateInstruction,
        transactionMessage,
      );

      return { result: transactionMessage };
    } catch (error) {
      throw this.handleError('DELEGATE_ERROR', error);
    }
  }

  /**
   * Deactivates a stake account.
   *
   * @param address - The public key of the account.
   * @param stakeAccountPublicKey - The public key of the stake account.
   * @throws Throws an error if there's an issue during the deactivation process.
   * @returns Returns a promise that resolves with the deactivation transaction.
   *
   */
  public async deactivate(
    sender: string,
    stakeAccountPublicKey: string,
    params?: Params,
  ): Promise<ApiResponse<TransactionMessageWithBlockhashLifetime>> {
    try {
      const deactivateInstruction = getDeactivateInstruction({
        stake: address(stakeAccountPublicKey),
        stakeAuthority: createNoopSigner(address(sender)),
      });
      let transactionMessage = await this.baseTx(sender, params);
      transactionMessage = appendTransactionMessageInstruction(
        deactivateInstruction,
        transactionMessage,
      );

      return { result: transactionMessage };
    } catch (error) {
      throw this.handleError('DEACTIVATE_ERROR', error);
    }
  }

  /**
   * Withdraws a specified amount from a stake account.
   *
   * @param address - The public key of the account.
   * @param stakeAccountPublicKey - The public key of the stake account.
   * @param stakeBalance - The amount in lamports to be withdrawn from the stake account.
   *
   * @throws Throws an error if there's an issue during the withdrawal process.
   *
   * @returns Returns a promise that resolves with the withdrawal transaction.
   *
   */
  public async withdraw(
    sender: Address,
    stakeAccountPublicKey: Address,
    stakeBalance: bigint,
    params?: Params,
  ): Promise<ApiResponse<TransactionMessageWithBlockhashLifetime>> {
    try {
      // Create the withdraw instruction
      const withdrawInstruction = getWithdrawInstruction({
        stake: stakeAccountPublicKey,
        recipient: sender,
        stakeHistory: STAKE_HISTORY_ACCOUNT,
        withdrawAuthority: createNoopSigner(address(sender)),
        args: stakeBalance,
      });

      let transactionMessage = await this.baseTx(sender, params);
      transactionMessage = appendTransactionMessageInstruction(
        withdrawInstruction,
        transactionMessage,
      );

      return { result: transactionMessage };
    } catch (error) {
      throw this.handleError('WITHDRAW_ERROR', error);
    }
  }

  /**
   * Fetches the delegations of a given account.
   *
   * @param address - The public key of the account.
   *
   * @throws Throws an error if there's an issue fetching the delegations.
   *
   * @returns Returns a promise that resolves with the delegations of the account.
   *
   */
  public async getDelegations(
    address: string,
  ): Promise<ApiResponse<Delegations>> {
    try {
      // Fetch the accounts
      const accounts = await this.connection
        .getProgramAccounts(STAKE_PROGRAM_ADDRESS, {
          encoding: 'base64',
          filters: [
            {
              dataSize: FILTER_DATA_SIZE, // Token account size
            },
            {
              memcmp: {
                bytes: address,
                encoding: 'base58',
                offset: FILTER_OFFSET,
              },
            },
          ],
        })
        .send();

      const acs = accounts.map((account) => {
        const acc = parseBase64RpcAccount(account.pubkey, account.account);

        return decodeStakeStateAccount(acc);
      });

      return { result: acs };
    } catch (error) {
      throw this.handleError('GET_DELEGATIONS_ERROR', error);
    }
  }

  /**
   * Stakes a certain amount of lamports.
   *
   * @param sender - The public key of the sender.
   * @param lamports - The number of lamports to stake.
   * @param source  - stake source
   * @param lockup - stake account lockup
   * @returns A promise that resolves to a VersionedTransaction object.
   */
  async stake(
    sender: string,
    lamports: bigint,
    source: string,
    // lockup: Lockup | null = Lockup.default,
    params?: Params,
  ): Promise<ApiResponse<StakeResponse>> {
    try {
      //     lockup = lockup || Lockup.default;
      // Get the minimum balance for rent exemption
      const minimumRent = await this.connection
        .getMinimumBalanceForRentExemption(
          //TODO get from account when would be added
          BigInt(STAKE_ACCOUNT_V2_SIZE),
        )
        .send();

      const [
        createStakeAccountInstruction,
        initializeStakeAccountInstruction,
        stakeAccountPublicKey,
      ] =
        source === null
          ? // TODO fix create account sign
            await this.createAccountTx(
              address(sender),
              BigInt(lamports) + minimumRent,
              // lockup,
            )
          : await this.createAccountWithSeedTx(
              address(sender),
              BigInt(lamports) + minimumRent,
              source,
              // lockup,
            );

      const delegateInstruction = getDelegateStakeInstruction({
        stake: stakeAccountPublicKey,
        vote: this.validator,
        stakeHistory: STAKE_HISTORY_ACCOUNT,
        unused: STAKE_CONFIG_ACCOUNT,
        stakeAuthority: createNoopSigner(address(sender)),
      });

      let transactionMessage = await this.baseTx(sender, params);
      transactionMessage = appendTransactionMessageInstruction(
        createStakeAccountInstruction,
        transactionMessage,
      );
      transactionMessage = appendTransactionMessageInstruction(
        initializeStakeAccountInstruction,
        transactionMessage,
      );
      transactionMessage = appendTransactionMessageInstruction(
        delegateInstruction,
        transactionMessage,
      );

      const signedTransactionMessage =
        source === null
          ? await partiallySignTransactionMessageWithSigners(transactionMessage)
          : transactionMessage;

      return {
        result: {
          stakeTx: signedTransactionMessage,
          stakeAccount: stakeAccountPublicKey,
        },
      };
    } catch (error) {
      throw this.handleError('STAKE_ERROR', error);
    }
  }

  /**
   * Create account Tx, public key and array of keypair.
   *
   * @param address - The public key of the account.
   * @param lamports - The number of lamports to stake.
   * @param lockup - The stake account lockup
   *
   * @throws Throws an error if there's an issue creating an account.
   *
   * @returns Returns a promise that resolves with the Transaction, PublicKey and array of Keypair.
   *
   */
  private async createAccountTx(
    authorityPublicKey: Address,
    lamports: bigint,
    // lockup: Lockup,
  ): Promise<[IInstruction, IInstruction, Address]> {
    const stakeAccountKeyPair = await generateKeyPair();
    const signer = await createSignerFromKeyPair(stakeAccountKeyPair);

    const createAccountInstruction = getCreateAccountInstruction({
      payer: createNoopSigner(authorityPublicKey),
      newAccount: signer,
      lamports: lamports,
      // TODO get from package
      space: STAKE_ACCOUNT_V2_SIZE,
      programAddress: STAKE_PROGRAM_ADDRESS,
    });

    const initializeInstruction = getInitializeInstruction(
      /** Uninitialized stake account */
      {
        stake: signer.address,
        arg0: {
          staker: authorityPublicKey,
          withdrawer: authorityPublicKey,
        },
        arg1: {
          //TODO use default
          unixTimestamp: 0,
          epoch: 0,
          custodian: ADDRESS_DEFAULT,
        },
      },
    );

    return [createAccountInstruction, initializeInstruction, signer.address];
  }

  /**
   * Create account Tx, public key and array of keypair using seed.
   *
   * @param authorityPublicKey - The public key of the account.
   * @param lamports - The number of lamports to stake.
   * @param source - The stake source
   * @param lockup - The stake account lockup
   *
   * @throws Throws an error if there's an issue creating an account.
   *
   * @returns Returns a promise that resolves with the Transaction, PublicKey and array of Keypair.
   *
   */
  private async createAccountWithSeedTx(
    authorityPublicKey: Address,
    lamports: bigint,
    source: string,
    // lockup: Lockup,
  ): Promise<[IInstruction, IInstruction, Address]> {
    // Format source to
    const seed = this.formatSource(source || '');

    const stakeAccountPubkey = await createAddressWithSeed({
      baseAddress: authorityPublicKey,
      programAddress: STAKE_PROGRAM_ADDRESS,
      seed: seed,
    });

    const createAccountInstruction = getCreateAccountWithSeedInstruction({
      payer: createNoopSigner(authorityPublicKey),
      newAccount: stakeAccountPubkey,
      baseAccount: createNoopSigner(authorityPublicKey),
      base: address(authorityPublicKey),
      seed: seed,
      amount: lamports,
      // TODO get from package
      space: STAKE_ACCOUNT_V2_SIZE,
      programAddress: STAKE_PROGRAM_ADDRESS,
    });

    const initializeInstruction = getInitializeInstruction(
      /** Uninitialized stake account */
      {
        stake: stakeAccountPubkey,
        arg0: {
          staker: authorityPublicKey,
          withdrawer: authorityPublicKey,
        },
        arg1: {
          //TODO implement Lockup
          unixTimestamp: 0,
          epoch: 0,
          custodian: ADDRESS_DEFAULT,
        },
      },
    );

    return [
      createAccountInstruction,
      initializeInstruction,
      stakeAccountPubkey,
    ];
  }

  /** unstake - unstake
   * @param {string} sender - account blockchain address (staker)
   * @param {bigint} lamports - lamport amount
   * @param {string} source - stake source
   * @returns {Promise<object>} Promise object with Versioned Tx
   */
  public async unstake(
    sender: string,
    lamports: bigint,
    source: string,
    params?: Params,
  ): Promise<ApiResponse<UnstakeResponse>> {
    try {
      const stakeAccounts = (await this.getDelegations(sender)).result;

      const epoch =
        params?.epoch || (await this.connection.getEpochInfo().send()).epoch;
      const tm = this.timestampInSec();

      let unstakeAmount = lamports;
      let totalActiveStake: bigint = 0n;
      const activeStakeAccounts = stakeAccounts.filter((acc) => {
        if (acc.data.state.__kind !== 'Stake') {
          return false;
        }

        const isActive = !(
          isLockupInForce(acc.data, epoch, BigInt(tm)) ||
          stakeAccountState(acc.data, epoch) !== StakeState.Active
        );

        if (isActive) {
          totalActiveStake =
            totalActiveStake + acc.data.state.fields[1].delegation.stake;
        }

        return isActive;
      });

      if (totalActiveStake < lamports)
        throw this.throwError('NOT_ENOUGH_ACTIVE_STAKE_ERROR');

      // ASC sort if num of accounts less than threshold otherwise DESC sorting
      activeStakeAccounts.sort((a, b): number => {
        const stakeA = isStake(a.data.state)
          ? a.data.state.fields[1].delegation.stake
          : 0n;
        const stakeB = isStake(b.data.state)
          ? b.data.state.fields[1].delegation.stake
          : 0n;

        if (activeStakeAccounts.length < MAX_DEACTIVATE_ACCOUNTS_WITH_SPLIT) {
          return Number(stakeA - stakeB);
        }

        return Number(stakeB - stakeA);
      });

      const accountsToDeactivate: Delegations = [];
      const accountsToSplit: [Account<StakeStateAccount, Address>, bigint][] =
        [];

      let i = 0;
      while (lamports > 0n && i < activeStakeAccounts.length) {
        const acc = activeStakeAccounts[i];
        if (acc === undefined || !isStake(acc.data.state)) {
          i++;
          continue;
        }

        const stakeAmount = acc.data.state.fields[1].delegation.stake;

        // If reminder amount less than min stake amount stake account automatically become disabled
        const isBelowThreshold =
          stakeAmount <= lamports || stakeAmount - lamports < MIN_AMOUNT;
        if (isBelowThreshold) {
          accountsToDeactivate.push(acc);
          lamports = lamports - stakeAmount;
          i++;

          // Max num of deactivate instructions reached
          if (accountsToDeactivate.length === MAX_DEACTIVATE_ACCOUNTS) {
            unstakeAmount -= lamports;
            break;
          }
          continue;
        }

        // Max num of deactivate instructions with split reached
        if (accountsToDeactivate.length > MAX_DEACTIVATE_ACCOUNTS_WITH_SPLIT) {
          unstakeAmount -= lamports;
          break;
        }

        accountsToSplit.push([acc, lamports]);
        break;
      }

      const senderPublicKey = address(sender);
      let transactionMessage = await this.baseTx(sender, params);

      // Get the minimum balance for rent exemption. Send request only if split required
      const minimumRent =
        accountsToSplit.length > 0
          ? await this.connection
              .getMinimumBalanceForRentExemption(
                //TODO get from account when it's would be available
                BigInt(STAKE_ACCOUNT_V2_SIZE),
              )
              .send()
          : 0n;

      for (const acc of accountsToSplit) {
        const [splitInstructions, newStakeAccountPubkey] = await this.split(
          senderPublicKey,
          acc[1],
          acc[0].address,
          source,
          // Need additional value for rent
          minimumRent,
        );

        splitInstructions.forEach(
          (splitInstruction) =>
            (transactionMessage = appendTransactionMessageInstruction(
              splitInstruction,
              transactionMessage,
            )),
        );

        const deactivateInstruction = getDeactivateInstruction({
          stake: newStakeAccountPubkey,
          stakeAuthority: createNoopSigner(address(sender)),
        });

        transactionMessage = appendTransactionMessageInstruction(
          deactivateInstruction,
          transactionMessage,
        );
      }

      accountsToDeactivate.forEach((acc) => {
        const deactivateInstruction = getDeactivateInstruction({
          stake: acc.address,
          stakeAuthority: createNoopSigner(address(sender)),
        });

        transactionMessage = appendTransactionMessageInstruction(
          deactivateInstruction,
          transactionMessage,
        );
      });

      if (transactionMessage.instructions.length === 0) {
        this.handleError('UNSTAKE_ERROR', 'zero instructions');
      }

      return {
        result: { unstakeTx: transactionMessage, unstakeAmount: unstakeAmount },
      };
    } catch (error) {
      throw this.handleError('UNSTAKE_ERROR', error);
    }
  }

  /**
   * Split existing account to create a new one
   *
   * @param authorityPublicKey - The public key of the account.
   * @param lamports - The number of lamports to stake.
   * @param oldStakeAccountPubkey -The public key of the old account.
   * @param source - The stake source
   *
   * @throws Throws an error if there's an issue splitting an account.
   *
   * @returns Returns a promise that resolves with the Transaction, PublicKey and array of Keypair.
   *
   */
  private async split(
    authorityPublicKey: Address,
    lamports: bigint,
    oldStakeAccountPubkey: Address,
    source: string,
    rentExemptReserve?: bigint,
  ): Promise<[Array<IInstruction>, Address]> {
    // Format source to
    const seed = this.formatSource(source);

    const newStakeAccountPubkey = await createAddressWithSeed({
      baseAddress: authorityPublicKey,
      programAddress: STAKE_PROGRAM_ADDRESS,
      seed,
    });

    const instructions: Array<IInstruction> = [];

    // TODO add support split w\o seed
    const allocateWithSeedInstruction = getAllocateWithSeedInstruction({
      newAccount: newStakeAccountPubkey,
      baseAccount: createNoopSigner(address(authorityPublicKey)),
      base: authorityPublicKey,
      seed: seed,
      //TODO get from library if possible
      space: STAKE_ACCOUNT_V2_SIZE,
      programAddress: STAKE_PROGRAM_ADDRESS,
    });

    instructions.push(allocateWithSeedInstruction);

    //If creates new account need to top up balance by rent amount
    if (rentExemptReserve && rentExemptReserve > 0) {
      const rentTransferInstruction = getTransferSolInstruction({
        source: createNoopSigner(authorityPublicKey),
        destination: newStakeAccountPubkey,
        amount: rentExemptReserve,
      });
      instructions.push(rentTransferInstruction);
    }

    const splitInstruction = getSplitInstruction({
      stake: oldStakeAccountPubkey,
      splitStake: newStakeAccountPubkey,
      stakeAuthority: createNoopSigner(authorityPublicKey),
      args: lamports,
    });

    instructions.push(splitInstruction);

    return [instructions, newStakeAccountPubkey];
  }

  /**
   * Claim makes withdrawal from all sender's deactivated accounts.
   *
   * @param sender - The sender solana address.
   *
   * @throws Throws an error if there's an issue while claiming a stake.
   *
   * @returns Returns a promise that resolves with a Versioned Transaction.
   *
   */
  public async claim(
    sender: string,
    params?: Params,
  ): Promise<ApiResponse<ClaimResponse>> {
    try {
      const delegations = await this.getDelegations(sender);

      const epoch =
        params?.epoch || (await this.connection.getEpochInfo().send()).epoch;
      const tm = this.timestampInSec();

      const deactivatedStakeAccounts = delegations.result.filter((acc) => {
        return (
          !isLockupInForce(acc.data, epoch, BigInt(tm)) &&
          stakeAccountState(acc.data, epoch) === StakeState.Deactivated
        );
      });

      if (deactivatedStakeAccounts.length === 0)
        throw this.throwError('NOTHING_TO_CLAIM_ERROR');

      let transactionMessage = await this.baseTx(sender, params);

      let totalClaimableStake = 0n;
      let accountsForClaim = 0;
      for (const acc of deactivatedStakeAccounts) {
        // Create the withdraw instruction
        const withdrawInstruction = getWithdrawInstruction({
          stake: acc.address,
          recipient: address(sender),
          stakeHistory: STAKE_HISTORY_ACCOUNT,
          withdrawAuthority: createNoopSigner(address(sender)),
          args: acc.lamports,
        });

        transactionMessage = appendTransactionMessageInstruction(
          withdrawInstruction,
          transactionMessage,
        );

        totalClaimableStake += acc.lamports;
        accountsForClaim++;

        if (accountsForClaim === MAX_CLAIM_ACCOUNTS) {
          break;
        }
      }

      return {
        result: {
          claimTx: transactionMessage,
          totalClaimAmount: totalClaimableStake,
        },
      };
    } catch (error) {
      throw this.handleError('CLAIM_ERROR', error);
    }
  }

  /**
   * Merge two accounts into a new one
   *
   * @param authorityPublicKey - The public key of the account.
   * @param stakeAccount1 - The public key of the first account.
   * @param stakeAccount2 - The public key of the second account.
   *
   * @throws Throws an error if there's an issue while merging an account.
   *
   * @returns Returns a promise that resolves with the Transaction, PublicKey and array of Keypair.
   *
   */
  // private async merge(
  //   authorityPublicKey: PublicKey,
  //   stakeAccount1: PublicKey,
  //   stakeAccount2: PublicKey,
  // ) {
  //   const mergeStakeAccountTx = StakeProgram.merge({
  //     stakePubkey: stakeAccount1,
  //     sourceStakePubKey: stakeAccount2,
  //     authorizedPubkey: authorityPublicKey,
  //   });

  //   return [mergeStakeAccountTx];
  // }

  private async baseTx(
    sender: string,
    params?: Params,
  ): Promise<
    CompilableTransactionMessage & TransactionMessageWithBlockhashLifetime
  > {
    const finalLatestBlockhash =
      params?.finalLatestBlockhash ||
      (await this.connection.getLatestBlockhash().send()).value;

    let transactionMessage = pipe(
      createTransactionMessage({ version: 0 }),
      (tx) => setTransactionMessageFeePayer(address(sender), tx),
      (tx) =>
        setTransactionMessageLifetimeUsingBlockhash(finalLatestBlockhash, tx),
    );

    if (
      params?.computeUnitLimit !== undefined &&
      params?.computeUnitLimit > 0
    ) {
      const unitLimitInstruction = getSetComputeUnitLimitInstruction({
        /** Transaction compute unit limit used for prioritization fees. */
        units: params?.computeUnitLimit,
      });

      transactionMessage = prependTransactionMessageInstruction(
        unitLimitInstruction,
        transactionMessage,
      );
    }

    if (
      params?.сomputeUnitPrice !== undefined &&
      params?.сomputeUnitPrice > 0
    ) {
      const unitPriceInstruction = getSetComputeUnitPriceInstruction({
        /** Transaction compute unit price used for prioritization fees. */
        microLamports: params?.сomputeUnitPrice,
      });
      transactionMessage = prependTransactionMessageInstruction(
        unitPriceInstruction,
        transactionMessage,
      );
    }

    return transactionMessage;
  }

  /**
   * Generate a unique source for crating an account.
   *
   * @param source - source ID.
   *
   * @returns Returns a unique source for an account.
   *
   */
  private formatSource(source: string): string {
    const timestamp = new Date().getTime();
    source = `everstake ${source}:${timestamp}`;

    return source;
  }

  /**
   * Generate timestamp in seconds.
   *
   * @returns Returns a timestamp in seconds.
   *
   */
  private timestampInSec(): number {
    return (Date.now() / 1000) | 0;
  }
}

//TODO think about export of this methods
/**
 * Determins the current state of a stake account given the current epoch
 * @param currentEpoch
 * @returns `stakeAccount`'s stake state`string`
 */
export function stakeAccountState(
  account: StakeStateAccount,
  currentEpoch: bigint,
): string {
  //TODO check
  if (account.state.__kind !== 'Stake') {
    return StakeState.Inactive;
  }

  const activationEpoch = account.state.fields[1].delegation.activationEpoch;
  const deactivationEpoch =
    account.state.fields[1].delegation.deactivationEpoch;

  if (activationEpoch > currentEpoch) {
    return StakeState.Inactive;
  }
  if (activationEpoch === currentEpoch) {
    // if you activate then deactivate in the same epoch,
    // deactivationEpoch === activationEpoch.
    // if you deactivate then activate again in the same epoch,
    // the deactivationEpoch will be reset to EPOCH_MAX
    if (deactivationEpoch === activationEpoch) return StakeState.Inactive;

    return StakeState.Activating;
  }
  // activationEpoch < currentEpochBN
  if (deactivationEpoch > currentEpoch) return StakeState.Active;
  if (deactivationEpoch === currentEpoch) return StakeState.Deactivating;

  return StakeState.Deactivated;
}

/**
 * Check if lockup is in force
 * @param currEpoch current epoch.
 * @param currUnixTimestamp current unix timetamp.
 * @returns a bool type result.
 */
export function isLockupInForce(
  account: StakeStateAccount,
  currEpoch: bigint,
  currUnixTimestamp: bigint,
): boolean {
  if (
    account.state.__kind !== 'Stake' &&
    account.state.__kind !== 'Initialized'
  ) {
    return false;
  }

  const { unixTimestamp, epoch } = account.state.fields[0].lockup;

  return unixTimestamp > currUnixTimestamp || epoch > currEpoch;
}

export function isStake(
  state: StakeStateV2,
): state is Extract<StakeStateV2, { __kind: 'Stake' }> {
  return state.__kind === 'Stake';
}
