import { sha3_256 } from "@noble/hashes/sha3.js";
import { Serializer } from "../bcs/serializer.js";
import { AccountAddress } from "../core/accountAddress.js";
import { Hex } from "../core/hex.js";
import { AccountAuthenticatorAbstraction } from "../transactions/authenticator/account.js";
import { HexInput, MoveFunctionId } from "../types/index.js";
import { isValidFunctionInfo } from "../utils/helpers.js";
import { AbstractedAccount } from "./AbstractedAccount.js";

type DerivableAbstractedAccountArgs = {
  /**
   * The signer function signs transactions and returns the `authenticator` bytes in the `AbstractionAuthData`.
   *
   * @param digest - The SHA256 hash of the transaction signing message
   * @returns The `authenticator` bytes that can be used to verify the signature.
   */
  signer: (digest: HexInput) => Uint8Array;

  /**
   * The authentication function that will be used to verify the signature.
   *
   * @example
   * ```ts
   * const authenticationFunction = `${accountAddress}::permissioned_delegation::authenticate`;
   * ```
   */
  authenticationFunction: MoveFunctionId;

  /**
   * The abstract public key that is used to identify the account.
   * Depends on the use cases, most of the time it is the public key of the source wallet
   */
  abstractPublicKey: Uint8Array;
};

export class DerivableAbstractedAccount extends AbstractedAccount {
  /**
   * The abstract public key that is used to identify the account.
   * Depends on the use cases, most of the time it is the public key of the source wallet
   */
  readonly abstractPublicKey: Uint8Array;

  /**
   * The domain separator used to calculate the DAA account address.
   */
  static readonly ADDRESS_DOMAIN_SEPERATOR: number = 5;

  constructor({ signer, authenticationFunction, abstractPublicKey }: DerivableAbstractedAccountArgs) {
    const daaAccountAddress = new AccountAddress(
      DerivableAbstractedAccount.computeAccountAddress(authenticationFunction, abstractPublicKey),
    );
    super({
      accountAddress: daaAccountAddress,
      signer,
      authenticationFunction,
    });
    this.abstractPublicKey = abstractPublicKey;
  }

  /**
   * Compute the account address of the DAA
   * The DAA account address is computed by hashing the function info and the account identity
   * and appending the domain separator (5)
   *
   * @param functionInfo - The authentication function
   * @param accountIdentifier - The account identity
   * @returns The account address
   */
  static computeAccountAddress(functionInfo: string, accountIdentifier: Uint8Array): Uint8Array {
    if (!isValidFunctionInfo(functionInfo)) {
      throw new Error(`Invalid authentication function ${functionInfo} passed into DerivableAbstractedAccount`);
    }
    const [moduleAddress, moduleName, functionName] = functionInfo.split("::");

    const hash = sha3_256.create();
    // Serialize and append the function info
    const serializer = new Serializer();
    AccountAddress.fromString(moduleAddress).serialize(serializer);
    serializer.serializeStr(moduleName);
    serializer.serializeStr(functionName);
    hash.update(serializer.toUint8Array());

    // Serialize and append the account identity
    const s2 = new Serializer();
    s2.serializeBytes(accountIdentifier);
    hash.update(s2.toUint8Array());

    // Append the domain separator
    hash.update(new Uint8Array([DerivableAbstractedAccount.ADDRESS_DOMAIN_SEPERATOR]));

    return hash.digest();
  }

  signWithAuthenticator(message: HexInput): AccountAuthenticatorAbstraction {
    const messageBytes = Hex.fromHexInput(message).toUint8Array();
    return new AccountAuthenticatorAbstraction(
      this.authenticationFunction,
      sha3_256(messageBytes),
      this.sign(sha3_256(messageBytes)).value,
      this.abstractPublicKey,
    );
  }
}
