import { PublicKey } from '@metaplex-foundation/umi-public-keys';
import { Amount, SolAmount } from './Amount';
import type { Instruction } from './Instruction';
import type { Commitment } from './RpcInterface';

/**
 * The maximum amount of bytes that can be used for a transaction.
 * @category Transactions
 */
export const TRANSACTION_SIZE_LIMIT = 1232;

/**
 * The version of a transaction.
 * - Legacy is the very first iteration of Solana transactions.
 * - V0 introduces the concept of versioned transaction for
 * the first time and adds supports for address lookup tables.
 *
 * @category Transactions
 */
export type TransactionVersion = 'legacy' | 0;

/**
 * A Uint8Array that represents a serialized transaction.
 * @category Transactions
 */
export type SerializedTransaction = Uint8Array;

/**
 * A Uint8Array that represents the serialized message of a transaction.
 * @category Transactions
 */
export type SerializedTransactionMessage = Uint8Array;

/**
 * A Uint8Array that represents a transaction signature.
 * @category Transactions
 */
export type TransactionSignature = Uint8Array;

/**
 * Defines a transaction error.
 * @category Transactions
 */
export type TransactionError = {} | string;

/**
 * Defines a blockhash.
 * @category Transactions
 */
export type Blockhash = string;

/**
 * Defines a blockhash with its expiry block height.
 * @category Transactions
 */
export type BlockhashWithExpiryBlockHeight = {
  blockhash: Blockhash;
  lastValidBlockHeight: number;
};

/**
 * Defines a transaction.
 * @category Transactions
 */
export interface Transaction {
  readonly message: TransactionMessage;
  readonly serializedMessage: SerializedTransactionMessage;
  readonly signatures: TransactionSignature[];
}

/**
 * Defines the message of a transaction.
 * @category Transactions
 */
export interface TransactionMessage {
  readonly version: TransactionVersion;
  readonly header: TransactionMessageHeader;
  readonly accounts: PublicKey[];
  readonly blockhash: Blockhash;
  readonly instructions: CompiledInstruction[];
  readonly addressLookupTables: CompiledAddressLookupTable[];
}

/**
 * Defines the header of a transaction message.
 * @category Transactions
 */
export type TransactionMessageHeader = {
  readonly numRequiredSignatures: number;
  readonly numReadonlySignedAccounts: number;
  readonly numReadonlyUnsignedAccounts: number;
};

/**
 * Defines an instruction that uses indexes to reference accounts.
 * @category Transactions
 */
export type CompiledInstruction = {
  readonly programIndex: number;
  readonly accountIndexes: number[];
  readonly data: Uint8Array;
};

/**
 * Defines an address lookup table that uses indexes to reference accounts.
 * @category Transactions
 */
export type CompiledAddressLookupTable = {
  readonly publicKey: PublicKey;
  readonly writableIndexes: number[];
  readonly readonlyIndexes: number[];
};

/**
 * Defines a transaction with its post-execution metadata.
 * @category Transactions
 */
export type TransactionWithMeta = Transaction & {
  readonly meta: TransactionMeta;
};

/**
 * Defines the post-execution metadata of a transaction.
 * @category Transactions
 */
export type TransactionMeta = {
  readonly fee: SolAmount;
  readonly logs: string[];
  readonly preBalances: SolAmount[];
  readonly postBalances: SolAmount[];
  readonly preTokenBalances: TransactionMetaTokenBalance[];
  readonly postTokenBalances: TransactionMetaTokenBalance[];
  readonly innerInstructions: TransactionMetaInnerInstruction[] | null;
  readonly loadedAddresses: TransactionMetaLoadedAddresses;
  readonly computeUnitsConsumed: bigint | null;
  readonly costUnits: bigint | null;
  readonly err: TransactionError | null;
};

/**
 * The balance of a token account before or after a transaction.
 * @category Transactions
 */
export type TransactionMetaTokenBalance = {
  accountIndex: number;
  amount: Amount;
  mint: PublicKey;
  owner: PublicKey | null;
};

/**
 * The cross program invoked instructions of an instruction.
 * @category Transactions
 */
export type TransactionMetaInnerInstruction = {
  index: number;
  instructions: CompiledInstruction[];
};

/**
 * The collection of addresses loaded using address lookup tables.
 * @category Transactions
 */
export type TransactionMetaLoadedAddresses = {
  writable: PublicKey[];
  readonly: PublicKey[];
};

/**
 * Defines the various ways to create a transaction.
 * @category Transactions
 */
export type TransactionInput = TransactionInputLegacy | TransactionInputV0;

/**
 * Defines transaction input for legacy transactions.
 * @category Transactions
 */
export type TransactionInputLegacy = TransactionInputBase & {
  version: 'legacy';
};

/**
 * Defines transaction input for V0 transactions.
 * @category Transactions
 */
export type TransactionInputV0 = TransactionInputBase & {
  version?: 0;
  addressLookupTables?: AddressLookupTableInput[];
};

/**
 * Defines common transaction input.
 * @category Transactions
 */
export type TransactionInputBase = {
  payer: PublicKey;
  instructions: Instruction[];
  blockhash: Blockhash;
  signatures?: TransactionSignature[];
};

/**
 * The required data to add an address lookup table to a transaction.
 * @category Transactions
 */
export type AddressLookupTableInput = {
  publicKey: PublicKey;
  addresses: PublicKey[];
};

/**
 * The status of a sent transaction.
 * @category Transactions
 */
export type TransactionStatus = {
  /** When the transaction was processed. */
  slot: number;
  /** The number of blocks that have been confirmed and voted on in the fork containing `slot`. */
  confirmations: number | null;
  /** The transaction error, if any. */
  error: TransactionError | null;
  /** The cluster confirmation status, if any. */
  commitment: Commitment | null;
};

/**
 * Adds a given signature to the transaction's signature array
 * and returns the updated transaction as a new object.
 *
 * @category Transactions
 */
export const addTransactionSignature = (
  transaction: Transaction,
  signature: TransactionSignature,
  signerPublicKey: PublicKey
): Transaction => {
  const maxSigners = transaction.message.header.numRequiredSignatures;
  const signerPublicKeys = transaction.message.accounts.slice(0, maxSigners);
  const signerIndex = signerPublicKeys.findIndex(
    (key) => key === signerPublicKey
  );

  if (signerIndex < 0) {
    throw new Error(
      'The provided signer is not required to sign this transaction.'
    );
  }

  const newSignatures = [...transaction.signatures];
  newSignatures[signerIndex] = signature;
  return { ...transaction, signatures: newSignatures };
};
