import type { Provider } from "@saberhq/solana-contrib";
import { TransactionEnvelope } from "@saberhq/solana-contrib";
import type { TokenAmount } from "@saberhq/token-utils";
import {
  buildCreateTokenAccountTX,
  createATAInstruction,
  createInitMintTX,
  getATAAddressSync,
  SPLToken,
  TOKEN_PROGRAM_ID,
} from "@saberhq/token-utils";
import type { PublicKey } from "@solana/web3.js";
import { Keypair } from "@solana/web3.js";

import type { InitializeSwapInstruction } from "../index.js";
import { findSwapAuthorityKeySync, SWAP_PROGRAM_ID } from "../index.js";
import type { SwapTokenInfo } from "../instructions/swap.js";
import type { InitializeNewStableSwapArgs } from "./initialize.js";
import { createInitializeStableSwapInstructionsRaw } from "./initialize.js";

const initializeSwapTokenInfoSync = ({
  provider,
  mint,
  authority,
  admin,
  rentExemptTokenAccountBalance,
}: {
  rentExemptTokenAccountBalance: number;
  provider: Provider;
  mint: PublicKey;
  authority: PublicKey;
  admin: PublicKey;
}): {
  info: SwapTokenInfo;
  instructions: TransactionEnvelope;
} => {
  // Create Swap Token Account
  const { key: tokenAccount, tx: createSwapTokenAccountInstructions } =
    buildCreateTokenAccountTX({
      provider,
      mint,
      owner: authority,
      payer: provider.wallet.publicKey,
      rentExemptAccountBalance: rentExemptTokenAccountBalance,
    });

  // Create Admin Fee Account
  const { key: adminFeeAccount, tx: createAdminFeeAccountInstructions } =
    buildCreateTokenAccountTX({
      provider,
      mint,
      owner: admin,
      payer: provider.wallet.publicKey,
      rentExemptAccountBalance: rentExemptTokenAccountBalance,
    });

  return {
    info: {
      mint,
      reserve: tokenAccount,
      adminFeeAccount: adminFeeAccount,
    },
    instructions: createSwapTokenAccountInstructions.combine(
      createAdminFeeAccountInstructions,
    ),
  };
};

export type InitializeNewStableSwapSimpleArgs = Omit<
  InitializeNewStableSwapArgs,
  | "swapProgramID"
  | "tokenAMint"
  | "tokenBMint"
  | "useAssociatedAccountForInitialLP"
> & {
  reservesA: TokenAmount;
  reservesB: TokenAmount;
};

/**
 * Creates a set of instructions to create a new StableSwap instance.
 *
 * After calling this, you must sign this transaction with the accounts:
 * - payer -- Account that holds the SOL to seed the account.
 * - args.config.stableSwapAccount -- This account is used once then its key is no longer relevant
 * - all returned signers
 */
export const createInitializeStableSwapInstructionsSimple = async ({
  provider,
  adminAccount,
  ampFactor,
  fees,

  reservesA,
  reservesB,
  initialLiquidityProvider = adminAccount,
  swapAccountSigner = Keypair.generate(),
  poolTokenMintSigner = Keypair.generate(),
  seedPoolAccounts,
}: InitializeNewStableSwapSimpleArgs): Promise<{
  initializeArgs: InitializeSwapInstruction;
  /**
   * Lamports needed to be rent exempt.
   */
  balanceNeeded: number;

  instructions: {
    /**
     * Create accounts for the LP token
     */
    createLPTokenMint: TransactionEnvelope;
    /**
     * Create LP token account for the initial LP
     */
    createInitialLPTokenAccount: TransactionEnvelope;
    /**
     * Create accounts for swap token A
     */
    createSwapTokenAAccounts: TransactionEnvelope;
    /**
     * Create accounts for swap token B
     */
    createSwapTokenBAccounts: TransactionEnvelope;
    /**
     * Seed the accounts for the pool
     */
    seedPoolAccounts: TransactionEnvelope;
    /**
     * Initialize the swap
     */
    initializeSwap: TransactionEnvelope;
  };
}> => {
  const rentExemptTokenAccountBalance =
    await SPLToken.getMinBalanceRentForExemptAccount(provider.connection);
  const rentExemptMintBalance = await SPLToken.getMinBalanceRentForExemptMint(
    provider.connection,
  );

  // Create swap account if not specified
  const swapAccount = swapAccountSigner.publicKey;

  // Create authority and nonce
  const [authority, nonce] = findSwapAuthorityKeySync(swapAccount);

  // Create LP token mint
  const { decimals } = reservesA.token;
  if (reservesA.token.decimals !== reservesB.token.decimals) {
    throw new Error("decimals mismatch");
  }
  const createLPTokenMint = createInitMintTX({
    provider,
    mintKP: poolTokenMintSigner,
    mintAuthority: authority,
    decimals,
    rentExemptMintBalance,
  });
  const poolTokenMint = poolTokenMintSigner.publicKey;

  // Create initial LP token account
  const initialLPAccount = getATAAddressSync({
    mint: poolTokenMint,
    owner: initialLiquidityProvider,
  });
  const createInitialLPTokenAccount = new TransactionEnvelope(provider, [
    createATAInstruction({
      address: getATAAddressSync({
        mint: poolTokenMint,
        owner: initialLiquidityProvider,
      }),
      mint: poolTokenMint,
      owner: initialLiquidityProvider,
      payer: provider.wallet.publicKey,
    }),
  ]);

  // Create Swap Token A account
  const { info: tokenA, instructions: createSwapTokenAAccounts } =
    initializeSwapTokenInfoSync({
      provider,
      mint: reservesA.token.mintAccount,
      authority,
      admin: adminAccount,
      rentExemptTokenAccountBalance,
    });

  // Create Swap Token B account
  const { info: tokenB, instructions: createSwapTokenBAccounts } =
    initializeSwapTokenInfoSync({
      provider,
      mint: reservesB.token.mintAccount,
      authority,
      admin: adminAccount,
      rentExemptTokenAccountBalance,
    });

  // Seed the swap's Token A and token B accounts with tokens
  // On testnet, this is usually a mint.
  // On mainnet, this is usually a token transfer.
  const seedPoolAccountsResult = seedPoolAccounts({
    tokenAAccount: tokenA.reserve,
    tokenBAccount: tokenB.reserve,
  });
  const seedPoolAccountsTX = new TransactionEnvelope(
    provider,
    [...seedPoolAccountsResult.instructions],
    [...seedPoolAccountsResult.signers],
  );

  const initializeSwapInstruction: InitializeSwapInstruction = {
    config: {
      swapAccount: swapAccount,
      authority,
      swapProgramID: SWAP_PROGRAM_ID,
      tokenProgramID: TOKEN_PROGRAM_ID,
    },

    adminAccount,
    tokenA,
    tokenB,

    poolTokenMint,
    destinationPoolTokenAccount: initialLPAccount,
    nonce,
    ampFactor,
    fees,
  };
  const {
    balanceNeeded: swapBalanceNeeded,
    instructions: initializeStableSwapInstructions,
  } = await createInitializeStableSwapInstructionsRaw({
    provider,
    initializeSwapInstruction,
  });
  const initializeSwap = new TransactionEnvelope(
    provider,
    [...initializeStableSwapInstructions],
    [swapAccountSigner],
  );

  const instructions = {
    createLPTokenMint,
    createInitialLPTokenAccount,
    createSwapTokenAAccounts,
    createSwapTokenBAccounts,
    seedPoolAccounts: seedPoolAccountsTX,
    initializeSwap,
  };

  return {
    initializeArgs: initializeSwapInstruction,
    balanceNeeded:
      rentExemptMintBalance +
      swapBalanceNeeded +
      rentExemptTokenAccountBalance * 2,
    instructions,
  };
};
