/**
 * This file contains the underlying implementations for exposed submission API surface in
 * the {@link api/transaction}. By moving the methods out into a separate file,
 * other namespaces and processes can access these methods without depending on the entire
 * transaction namespace and without having a dependency cycle error.
 * @group Implementation
 */

import { AptosConfig } from "../api/aptosConfig.js";
import { Deserializer, MoveVector } from "../bcs/index.js";
import { postAptosFullNode } from "../client/index.js";
import { Account } from "../account/index.js";
import { isKeylessSigner } from "../account/keylessSigner.js";
import { AccountAddress, AccountAddressInput } from "../core/accountAddress.js";
import { AccountAuthenticator } from "../transactions/authenticator/account.js";
import {
  buildTransaction,
  generateTransactionPayload,
  generateSignedTransactionForSimulation,
  generateSignedTransaction,
} from "../transactions/transactionBuilder/transactionBuilder.js";
import {
  InputGenerateTransactionData,
  AnyRawTransaction,
  InputSimulateTransactionData,
  InputGenerateTransactionOptions,
  InputGenerateTransactionPayloadDataWithRemoteABI,
  InputSubmitTransactionData,
  InputGenerateMultiAgentRawTransactionData,
  InputGenerateSingleSignerRawTransactionData,
  AnyTransactionPayloadInstance,
  EntryFunctionABI,
  InputTransactionPluginData,
} from "../transactions/types.js";
import {
  UserTransactionResponse,
  PendingTransactionResponse,
  MimeType,
  HexInput,
  AnyPublicKeyVariant,
} from "../types/index.js";
import { SignedTransaction, TypeTagVector, generateSigningMessageForTransaction } from "../transactions/index.js";
import { SimpleTransaction } from "../transactions/instances/simpleTransaction.js";
import { MultiAgentTransaction } from "../transactions/instances/multiAgentTransaction.js";
import { TransactionPayloadEncryptedPayload } from "../transactions/instances/transactionPayload.js";

/**
 * We are defining function signatures, each with its specific input and output.
 * These are the possible function signature for `generateTransaction` function.
 * When we call `generateTransaction` function with the relevant type properties,
 * Typescript can infer the return type based on the appropriate function overload.
 * @group Implementation
 */
export async function generateTransaction(
  args: { aptosConfig: AptosConfig } & InputGenerateSingleSignerRawTransactionData,
): Promise<SimpleTransaction>;
export async function generateTransaction(
  args: { aptosConfig: AptosConfig } & InputGenerateMultiAgentRawTransactionData,
): Promise<MultiAgentTransaction>;
/**
 * Generates any transaction by passing in the required arguments
 *
 * @param args.sender The transaction sender's account address as a AccountAddressInput
 * @param args.data EntryFunctionData | ScriptData | MultiSigData
 * @param args.feePayerAddress optional. For a fee payer (aka sponsored) transaction
 * @param args.secondarySignerAddresses optional. For a multi-agent or fee payer (aka sponsored) transactions
 * @param args.options optional. GenerateTransactionOptions type
 *
 * @example
 * For a single signer entry function
 * move function name, move function type arguments, move function arguments
 * `
 * data: {
 *  function:"0x1::aptos_account::transfer",
 *  typeArguments:[]
 *  functionArguments :[receiverAddress,10]
 * }
 * `
 *
 * @example
 * For a single signer script function
 * module bytecode, move function type arguments, move function arguments
 * ```
 * data: {
 *  bytecode:"0x001234567",
 *  typeArguments:[],
 *  functionArguments :[receiverAddress,10]
 * }
 * ```
 *
 * @return An instance of a RawTransaction, plus optional secondary/fee payer addresses
 * ```
 * {
 *  rawTransaction: RawTransaction,
 *  secondarySignerAddresses?: Array<AccountAddress>,
 *  feePayerAddress?: AccountAddress
 * }
 * ```
 * @group Implementation
 */
export async function generateTransaction(
  args: { aptosConfig: AptosConfig } & InputGenerateTransactionData,
): Promise<AnyRawTransaction> {
  const payload = await buildTransactionPayload(args);
  return buildRawTransaction(args, payload);
}

/**
 * Builds a transaction payload based on the provided configuration and input data.
 * This function is essential for preparing transaction data for execution on the Aptos blockchain.
 *
 * @param args - The arguments for building the transaction payload.
 * @param args.aptosConfig - Configuration settings for the Aptos network.
 * @param args.data - Input data required to generate the transaction payload, which may include bytecode, multisig address,
 * function name, function arguments, type arguments, and ABI.
 * @returns A promise that resolves to the generated transaction payload instance.
 * @group Implementation
 */
export async function buildTransactionPayload(
  args: { aptosConfig: AptosConfig } & InputGenerateTransactionData,
): Promise<AnyTransactionPayloadInstance> {
  const { aptosConfig, data } = args;
  // Merge in aptosConfig for remote ABI on non-script payloads
  let generateTransactionPayloadData: InputGenerateTransactionPayloadDataWithRemoteABI;
  let payload: AnyTransactionPayloadInstance;

  if ("bytecode" in data) {
    // TODO: Add ABI checking later
    payload = await generateTransactionPayload(data);
  } else if ("multisigAddress" in data) {
    generateTransactionPayloadData = {
      aptosConfig,
      multisigAddress: data.multisigAddress,
      function: data.function,
      functionArguments: data.functionArguments,
      typeArguments: data.typeArguments,
      abi: data.abi,
    };
    payload = await generateTransactionPayload(generateTransactionPayloadData);
  } else {
    generateTransactionPayloadData = {
      aptosConfig,
      function: data.function,
      functionArguments: data.functionArguments,
      typeArguments: data.typeArguments,
      abi: data.abi,
    };
    payload = await generateTransactionPayload(generateTransactionPayloadData);
  }
  return payload;
}

/**
 * Builds a raw transaction based on the provided configuration and payload.
 * This function helps in creating a transaction that can be sent to the Aptos blockchain.
 *
 * @param args - The arguments for generating the transaction.
 * @param args.aptosConfig - The configuration settings for Aptos.
 * @param args.sender - The address of the sender of the transaction.
 * @param args.options - Additional options for the transaction.
 * @param payload - The payload of the transaction, which defines the action to be performed.
 * @group Implementation
 */
export async function buildRawTransaction(
  args: { aptosConfig: AptosConfig } & InputGenerateTransactionData,
  payload: AnyTransactionPayloadInstance,
): Promise<AnyRawTransaction> {
  const { aptosConfig, sender, options } = args;

  let feePayerAddress;
  if (isFeePayerTransactionInput(args)) {
    feePayerAddress = AccountAddress.ZERO.toString();
  }

  if (isMultiAgentTransactionInput(args)) {
    const { secondarySignerAddresses } = args;
    return buildTransaction({
      aptosConfig,
      sender,
      payload,
      options,
      secondarySignerAddresses,
      feePayerAddress,
    });
  }

  return buildTransaction({
    aptosConfig,
    sender,
    payload,
    options,
    feePayerAddress,
  });
}

/**
 * Determine if the transaction input includes a fee payer.
 *
 * @param data - The input data for generating a transaction.
 * @param data.withFeePayer - Indicates whether a fee payer is included in the transaction input.
 * @returns A boolean value indicating if the transaction input has a fee payer.
 * @group Implementation
 */
function isFeePayerTransactionInput(data: InputGenerateTransactionData): boolean {
  return data.withFeePayer === true;
}

/**
 * Determines whether the provided transaction input data includes multiple agent signatures.
 *
 * @param data - The transaction input data to evaluate.
 * @param data.secondarySignerAddresses - An array of secondary signer addresses, indicating multiple agents.
 * @group Implementation
 */
function isMultiAgentTransactionInput(
  data: InputGenerateTransactionData,
): data is InputGenerateMultiAgentRawTransactionData {
  return "secondarySignerAddresses" in data;
}

/**
 * Builds a signing message that can be signed by external signers.
 *
 * Note: Please prefer using `signTransaction` unless signing outside the SDK.
 *
 * @param args - The arguments for generating the signing message.
 * @param args.transaction - AnyRawTransaction, as generated by `generateTransaction()`.
 *
 * @returns The message to be signed.
 * @group Implementation
 */
export function getSigningMessage(args: { transaction: AnyRawTransaction }): Uint8Array {
  const { transaction } = args;
  return generateSigningMessageForTransaction(transaction);
}

/**
 * Sign a transaction that can later be submitted to the chain.
 *
 * @param args The arguments for signing the transaction.
 * @param args.signer The signer account to sign the transaction.
 * @param args.transaction An instance of a RawTransaction, plus optional secondary/fee payer addresses.
 *
 * @return The signer AccountAuthenticator.
 * @group Implementation
 */
export function signTransaction(args: { signer: Account; transaction: AnyRawTransaction }): AccountAuthenticator {
  const { signer, transaction } = args;
  return signer.signTransactionWithAuthenticator(transaction);
}

export function signAsFeePayer(args: { signer: Account; transaction: AnyRawTransaction }): AccountAuthenticator {
  const { signer, transaction } = args;

  // if transaction doesn't hold a "feePayerAddress" prop it means
  // this is not a fee payer transaction
  if (!transaction.feePayerAddress) {
    throw new Error(`Transaction ${transaction} is not a Fee Payer transaction`);
  }

  // Set the feePayerAddress to the signer account address
  transaction.feePayerAddress = signer.accountAddress;

  return signTransaction({
    signer,
    transaction,
  });
}

/**
 * Error thrown when {@link simulateTransaction} is called with an encrypted payload.
 * Simulation is not supported for encrypted transactions; build the same entry function without
 * `encrypted: true`, simulate that transaction, then build with encryption for submit.
 * @group Implementation
 */
export const ENCRYPTED_TRANSACTION_SIMULATION_NOT_SUPPORTED_MESSAGE =
  "Transaction simulation is not supported for encrypted payloads. Build a plaintext transaction with the same entry function and simulate it, then build with options.encrypted true for submission.";

/**
 * Throws if the transaction cannot be simulated because its payload is encrypted.
 * @group Implementation
 */
export function assertSimulatableTransaction(transaction: AnyRawTransaction): void {
  if (transaction.rawTransaction.payload instanceof TransactionPayloadEncryptedPayload) {
    throw new Error(ENCRYPTED_TRANSACTION_SIMULATION_NOT_SUPPORTED_MESSAGE);
  }
}

/**
 * Simulates a transaction before signing it to evaluate its potential outcome.
 *
 * @param args The arguments for simulating the transaction.
 * @param args.aptosConfig The configuration for the Aptos network.
 * @param args.transaction The raw transaction to simulate. Must not use an encrypted payload
 *   (`options.encrypted` when building); otherwise this function throws before contacting the node.
 * @param args.signerPublicKey Optional. The signer public key.
 * @param args.secondarySignersPublicKeys Optional. For when the transaction involves multiple signers.
 * @param args.feePayerPublicKey Optional. For when the transaction is sponsored by a fee payer.
 * @param args.options Optional. A configuration object to customize the simulation process.
 * @param args.options.estimateGasUnitPrice Optional. Indicates whether to estimate the gas unit price.
 * @param args.options.estimateMaxGasAmount Optional. Indicates whether to estimate the maximum gas amount.
 * @param args.options.estimatePrioritizedGasUnitPrice Optional. Indicates whether to estimate the prioritized gas unit price.
 *
 * @throws Error if `transaction` has an encrypted payload (see {@link assertSimulatableTransaction}).
 * @group Implementation
 */
export async function simulateTransaction(
  args: { aptosConfig: AptosConfig } & InputSimulateTransactionData,
): Promise<Array<UserTransactionResponse>> {
  const { aptosConfig, transaction, signerPublicKey, secondarySignersPublicKeys, feePayerPublicKey, options } = args;

  assertSimulatableTransaction(transaction);

  const signedTransaction = await generateSignedTransactionForSimulation({
    transaction,
    signerPublicKey,
    secondarySignersPublicKeys,
    feePayerPublicKey,
    options,
  });

  const { data } = await postAptosFullNode<Uint8Array, Array<UserTransactionResponse>>({
    aptosConfig,
    body: signedTransaction,
    path: "transactions/simulate",
    params: {
      estimate_gas_unit_price: args.options?.estimateGasUnitPrice ?? false,
      estimate_max_gas_amount: args.options?.estimateMaxGasAmount ?? false,
      estimate_prioritized_gas_unit_price: args.options?.estimatePrioritizedGasUnitPrice ?? false,
    },
    originMethod: "simulateTransaction",
    contentType: MimeType.BCS_SIGNED_TRANSACTION,
  });

  // If the server's gas estimation produced a value below the network minimum,
  // retry without estimation so the transaction's original max_gas_amount is used.
  if (
    args.options?.estimateMaxGasAmount &&
    data.length > 0 &&
    data[0].vm_status === "MAX_GAS_UNITS_BELOW_MIN_TRANSACTION_GAS_UNITS"
  ) {
    const { data: retryData } = await postAptosFullNode<Uint8Array, Array<UserTransactionResponse>>({
      aptosConfig,
      body: signedTransaction,
      path: "transactions/simulate",
      params: {
        estimate_gas_unit_price: args.options?.estimateGasUnitPrice ?? false,
        estimate_max_gas_amount: false,
        estimate_prioritized_gas_unit_price: args.options?.estimatePrioritizedGasUnitPrice ?? false,
      },
      originMethod: "simulateTransaction",
      contentType: MimeType.BCS_SIGNED_TRANSACTION,
    });
    return retryData;
  }

  return data;
}

/**
 * Submit a transaction to the Aptos blockchain.
 *
 * @param args - The arguments for submitting the transaction.
 * @param args.aptosConfig - The configuration for connecting to the Aptos network.
 * @param args.transaction - The Aptos transaction data to be submitted.
 * @param args.senderAuthenticator - The account authenticator of the transaction sender.
 * @param args.secondarySignerAuthenticators - Optional. Authenticators for additional signers in a multi-signer transaction.
 *
 * @returns PendingTransactionResponse - The response containing the status of the submitted transaction.
 * @group Implementation
 */
export async function submitTransaction(
  args: {
    aptosConfig: AptosConfig;
  } & InputSubmitTransactionData,
): Promise<PendingTransactionResponse> {
  const { aptosConfig, transactionSubmitter } = args;
  const maybeTransactionSubmitter =
    transactionSubmitter === undefined ? aptosConfig.getTransactionSubmitter() : transactionSubmitter;
  if (maybeTransactionSubmitter) {
    return maybeTransactionSubmitter.submitTransaction(args);
  }
  const signedTransaction = generateSignedTransaction({ ...args });
  try {
    const { data } = await postAptosFullNode<Uint8Array, PendingTransactionResponse>({
      aptosConfig,
      body: signedTransaction,
      path: "transactions",
      originMethod: "submitTransaction",
      contentType: MimeType.BCS_SIGNED_TRANSACTION,
    });
    return data;
  } catch (e) {
    // Best-effort diagnostic: if this was a keyless submission, refresh the
    // JWK so the next attempt can succeed. Any failure here (deserialization,
    // unregistered variant, network error while fetching JWKs, etc.) must
    // NOT mask the original submission error `e`.
    try {
      const signedTxn = SignedTransaction.deserialize(new Deserializer(signedTransaction));
      if (signedTxn.authenticator.isSingleSender() && signedTxn.authenticator.sender.isSingleKey()) {
        const { variant } = signedTxn.authenticator.sender.public_key;
        if (variant === AnyPublicKeyVariant.Keyless || variant === AnyPublicKeyVariant.FederatedKeyless) {
          // Dynamic import to avoid pulling poseidon-lite into the main bundle
          const { AbstractKeylessAccount } = await import("../account/AbstractKeylessAccount.js");
          // Match `AbstractKeylessAccount.fetchJWK`'s `publicKey` type so the
          // `FederatedKeyless` branch is not miscast to `KeylessPublicKey`.
          type KP =
            | import("../core/crypto/keyless.js").KeylessPublicKey
            | import("../core/crypto/federatedKeyless.js").FederatedKeylessPublicKey;
          type KS = import("../core/crypto/keyless.js").KeylessSignature;
          await AbstractKeylessAccount.fetchJWK({
            aptosConfig,
            publicKey: signedTxn.authenticator.sender.public_key.publicKey as KP,
            kid: (signedTxn.authenticator.sender.signature.signature as KS).getJwkKid(),
          });
        }
      }
    } catch {
      // Swallow diagnostic errors so we always rethrow the original submission error.
    }
    throw e;
  }
}

export type FeePayerOrFeePayerAuthenticatorOrNeither =
  | { feePayer: Account; feePayerAuthenticator?: never }
  | { feePayer?: never; feePayerAuthenticator: AccountAuthenticator }
  | { feePayer?: never; feePayerAuthenticator?: never };

export async function signAndSubmitTransaction(
  args: FeePayerOrFeePayerAuthenticatorOrNeither & {
    aptosConfig: AptosConfig;
    signer: Account;
    transaction: AnyRawTransaction;
  } & InputTransactionPluginData,
): Promise<PendingTransactionResponse> {
  const { aptosConfig, signer, feePayer, transaction, ...rest } = args;
  // If the signer contains a KeylessAccount, await proof fetching in case the proof
  // was fetched asynchronously.
  if (isKeylessSigner(signer)) {
    await signer.checkKeylessAccountValidity(aptosConfig);
  }
  if (isKeylessSigner(feePayer)) {
    await feePayer.checkKeylessAccountValidity(aptosConfig);
  }
  if (
    transaction.rawTransaction.payload instanceof TransactionPayloadEncryptedPayload &&
    (isKeylessSigner(signer) || isKeylessSigner(feePayer))
  ) {
    throw new Error(
      "Encrypted transactions are not supported with keyless or federated keyless signers (signature mutability breaks payload binding).",
    );
  }
  const feePayerAuthenticator =
    args.feePayerAuthenticator || (feePayer && signAsFeePayer({ signer: feePayer, transaction }));

  const senderAuthenticator = signTransaction({ signer, transaction });
  return submitTransaction({
    aptosConfig,
    transaction,
    senderAuthenticator,
    feePayerAuthenticator,
    ...rest,
  });
}

export async function signAndSubmitAsFeePayer(
  args: {
    aptosConfig: AptosConfig;
    feePayer: Account;
    senderAuthenticator: AccountAuthenticator;
    transaction: AnyRawTransaction;
  } & InputTransactionPluginData,
): Promise<PendingTransactionResponse> {
  const { aptosConfig, senderAuthenticator, feePayer, transaction, ...rest } = args;

  if (isKeylessSigner(feePayer)) {
    await feePayer.checkKeylessAccountValidity(aptosConfig);
  }

  const feePayerAuthenticator = signAsFeePayer({ signer: feePayer, transaction });

  return submitTransaction({
    aptosConfig,
    transaction,
    senderAuthenticator,
    feePayerAuthenticator,
    ...rest,
  });
}

// Lazy-initialized to avoid circular dependency issues at module evaluation time.
let _packagePublishAbi: EntryFunctionABI | undefined;
function getPackagePublishAbi(): EntryFunctionABI {
  if (!_packagePublishAbi) {
    _packagePublishAbi = {
      typeParameters: [],
      parameters: [TypeTagVector.u8(), new TypeTagVector(TypeTagVector.u8())],
    };
  }
  return _packagePublishAbi;
}

/**
 * Publishes a package transaction to the Aptos blockchain.
 * This function allows you to create and send a transaction that publishes a package with the specified metadata and bytecode.
 *
 * @param args - The arguments for the package transaction.
 * @param args.aptosConfig - The configuration settings for the Aptos client.
 * @param args.account - The address of the account sending the transaction.
 * @param args.metadataBytes - The metadata associated with the package, represented as hexadecimal input.
 * @param args.moduleBytecode - An array of module bytecode, each represented as hexadecimal input.
 * @param args.options - Optional parameters for generating the transaction.
 * @group Implementation
 */
export async function publicPackageTransaction(args: {
  aptosConfig: AptosConfig;
  account: AccountAddressInput;
  metadataBytes: HexInput;
  moduleBytecode: Array<HexInput>;
  options?: InputGenerateTransactionOptions;
}): Promise<SimpleTransaction> {
  const { aptosConfig, account, metadataBytes, moduleBytecode, options } = args;

  const totalByteCode = moduleBytecode.map((bytecode) => MoveVector.U8(bytecode));

  return generateTransaction({
    aptosConfig,
    sender: AccountAddress.from(account),
    data: {
      function: "0x1::code::publish_package_txn",
      functionArguments: [MoveVector.U8(metadataBytes), new MoveVector(totalByteCode)],
      abi: getPackagePublishAbi(),
    },
    options,
  });
}
