import { AccountAddress, AccountAddressInput } from "../../core/index.js";
import {
  addAuthenticationFunctionTransaction,
  removeAuthenticationFunctionTransaction,
  removeDispatchableAuthenticatorTransaction,
} from "../../internal/abstraction.js";
import { view } from "../../internal/view.js";
import { InputGenerateTransactionOptions, TypeTagAddress } from "../../transactions/index.js";
import { MoveFunctionId } from "../../types/index.js";
import { getFunctionParts } from "../../utils/helpers.js";
import { AptosConfig } from "../aptosConfig.js";

export class AccountAbstraction {
  constructor(readonly config: AptosConfig) {}

  /**
   * Adds a dispatchable authentication function to the account.
   *
   * @example
   * ```ts
   * const txn = await aptos.abstraction.addAuthenticationFunctionTransaction({
   *   accountAddress: alice.accountAddress,
   *   authenticationFunction: `${alice.accountAddress}::any_authenticator::authenticate`,
   * });
   *
   * const txn =  await aptos.signAndSubmitTransaction({ signer: alice, transaction});
   * await aptos.waitForTransaction({ transactionHash: txn.hash });
   * ```
   *
   * @param args.accountAddress - The account to add the authentication function to.
   * @param args.authenticationFunction - The authentication function info to add.
   * @param args.options - The options for the transaction.
   * @returns A transaction to add the authentication function to the account.
   */
  public async addAuthenticationFunctionTransaction(args: {
    accountAddress: AccountAddressInput;
    authenticationFunction: MoveFunctionId;
    options?: InputGenerateTransactionOptions;
  }) {
    const { accountAddress, authenticationFunction, options } = args;
    return addAuthenticationFunctionTransaction({
      aptosConfig: this.config,
      authenticationFunction,
      sender: accountAddress,
      options,
    });
  }

  /**
   * Removes a dispatchable authentication function from the account.
   *
   * @example
   * ```ts
   * const txn = await aptos.abstraction.removeAuthenticationFunctionTransaction({
   *   accountAddress: alice.accountAddress,
   *   authenticationFunction: `${alice.accountAddress}::any_authenticator::authenticate`,
   * });
   *
   * const txn = await aptos.signAndSubmitTransaction({ signer: alice, transaction: txn });
   * await aptos.waitForTransaction({ transactionHash: txn.hash });
   * ```
   *
   * @param args.accountAddress - The account to remove the authentication function from.
   * @param args.authenticationFunction - The authentication function info to remove.
   * @param args.options - The options for the transaction.
   * @returns A transaction to remove the authentication function from the account.
   */
  public async removeAuthenticationFunctionTransaction(args: {
    accountAddress: AccountAddressInput;
    authenticationFunction: MoveFunctionId;
    options?: InputGenerateTransactionOptions;
  }) {
    const { accountAddress, authenticationFunction, options } = args;
    return removeAuthenticationFunctionTransaction({
      aptosConfig: this.config,
      sender: accountAddress,
      authenticationFunction,
      options,
    });
  }

  /**
   * Removes a dispatchable authenticator from the account.
   *
   * @example
   * ```ts
   * const txn = await aptos.abstraction.removeDispatchableAuthenticatorTransaction({
   *   accountAddress: alice.accountAddress,
   * });
   *
   * const txn = await aptos.signAndSubmitTransaction({ signer: alice, transaction: txn });
   * await aptos.waitForTransaction({ transactionHash: txn.hash });
   * ```
   *
   * @param args.accountAddress - The account to remove the authenticator from.
   * @param args.options - The options for the transaction.
   * @returns A transaction to remove the authenticator from the account.
   */
  public async removeDispatchableAuthenticatorTransaction(args: {
    accountAddress: AccountAddressInput;
    options?: InputGenerateTransactionOptions;
  }) {
    const { accountAddress, options } = args;
    return removeDispatchableAuthenticatorTransaction({ aptosConfig: this.config, sender: accountAddress, options });
  }

  /**
   * Gets the dispatchable authentication function for the account.
   *
   * @example
   * ```ts
   * const functionInfos = await aptos.abstraction.getAuthenticationFunction({
   *   accountAddress: alice.accountAddress,
   * });
   *
   * if (functionInfos) {
   *   console.log(`Account ${alice.accountAddress.toString()} is using account abstraction!`);
   * } else {
   *   console.log(`Account ${alice.accountAddress.toString()} is not using account abstraction.`);
   * }
   * ```
   *
   * @param args.accountAddress - The account to get the dispatchable authentication function for.
   * @returns The dispatchable authentication function for the account.
   */
  public async getAuthenticationFunction(args: { accountAddress: AccountAddressInput }) {
    const { accountAddress } = args;
    const [{ vec: functionInfoOption }] = await view<
      [{ vec: { function_name: string; module_name: string; module_address: string }[][] }]
    >({
      aptosConfig: this.config,
      payload: {
        function: "0x1::account_abstraction::dispatchable_authenticator",
        functionArguments: [AccountAddress.from(accountAddress)],
        abi: { typeParameters: [], parameters: [new TypeTagAddress()], returnTypes: [] },
      },
    });

    if (functionInfoOption.length === 0) return undefined;

    return functionInfoOption[0].map((functionInfo) => ({
      moduleAddress: AccountAddress.fromString(functionInfo.module_address),
      moduleName: functionInfo.module_name,
      functionName: functionInfo.function_name,
    }));
  }

  /**
   * Will return true if the account is abstracted, otherwise false.
   *
   * @example
   * ```ts
   * const isAccountAbstractionEnabled = await aptos.abstraction.isAccountAbstractionEnabled({
   *   accountAddress: alice.accountAddress,
   *   authenticationFunction: `${alice.accountAddress}::any_authenticator::authenticate`,
   * });
   * if (isAccountAbstractionEnabled) {
   *   console.log(`Account ${alice.accountAddress.toString()} is using account abstraction!`);
   * } else {
   *   console.log(`Account ${alice.accountAddress.toString()} is not using account abstraction.`);
   * }
   * ```
   *
   * @param args.accountAddress - The account to check.
   * @returns Whether the account is abstracted.
   */
  public isAccountAbstractionEnabled = async (args: {
    accountAddress: AccountAddressInput;
    authenticationFunction: MoveFunctionId;
  }) => {
    const functionInfos = await this.getAuthenticationFunction(args);
    const { moduleAddress, moduleName, functionName } = getFunctionParts(args.authenticationFunction);
    return (
      functionInfos?.some(
        (functionInfo) =>
          AccountAddress.fromString(moduleAddress).equals(functionInfo.moduleAddress) &&
          moduleName === functionInfo.moduleName &&
          functionName === functionInfo.functionName,
      ) ?? false
    );
  };

  /**
   * Creates a transaction to enable account abstraction with the given authentication function.
   *
   * @example
   * ```ts
   * const txn = await aptos.abstraction.enableAccountAbstractionTransaction({
   *   accountAddress: alice.accountAddress,
   *   authenticationFunction: `{alice.accountAddress}::any_authenticator::authenticate`,
   * });
   *
   * const txn = await aptos.signAndSubmitTransaction({ signer: alice, transaction: txn });
   * await aptos.waitForTransaction({ transactionHash: txn.hash });
   * ```
   *
   * @param args.accountAddress - The account to enable account abstraction for.
   * @param args.authenticationFunction - The authentication function info to use.
   * @param args.options - The options for the transaction.
   * @returns A transaction to enable account abstraction for the account.
   */
  public enableAccountAbstractionTransaction = this.addAuthenticationFunctionTransaction;

  /**
   * Creates a transaction to disable account abstraction. If an authentication function is provided, it will specify to
   * remove the authentication function.
   *
   * @example
   * ```ts
   * const txn = await aptos.abstraction.disableAccountAbstractionTransaction({
   *   accountAddress: alice.accountAddress,
   *   authenticationFunction: `${alice.accountAddress}::any_authenticator::authenticate`,
   * });
   *
   * const txn = await aptos.signAndSubmitTransaction({ signer: alice, transaction: txn });
   * await aptos.waitForTransaction({ transactionHash: txn.hash });
   * ```
   *
   * @param args.accountAddress - The account to disable account abstraction for.
   * @param args.authenticationFunction - The authentication function info to remove.
   * @param args.options - The options for the transaction.
   * @returns A transaction to disable account abstraction for the account.
   */
  public disableAccountAbstractionTransaction = async (args: {
    accountAddress: AccountAddressInput;
    authenticationFunction?: MoveFunctionId;
    options?: InputGenerateTransactionOptions;
  }) => {
    const { accountAddress, authenticationFunction, options } = args;
    if (authenticationFunction) {
      return this.removeAuthenticationFunctionTransaction({
        accountAddress,
        authenticationFunction,
        options,
      });
    }
    return this.removeDispatchableAuthenticatorTransaction({ accountAddress, options });
  };
}
