import invariant from 'tiny-invariant';
import { d1 as FEATURE_PERMISSIONS, H as getRoleHash, aM as resolveAddress, o as MissingRoleError } from './index-01ae44ae.browser.esm.js';
import { h as hasFunction } from './contract-appuri-b0b011b6.browser.esm.js';
import { f as buildTransactionFunction, T as Transaction } from './transactions-7ca93634.browser.esm.js';
import { C as ContractEncoder } from './fetchCurrencyValue-16a41a05.browser.esm.js';

/**
 * Handle contract permissions
 * @remarks Configure roles and permissions for a contract, to restrict certain actions.
 * @example
 * ```javascript
 * const contract = await sdk.getContract("{{contract_address}}");
 * const rolesAndMembers = await contract.roles.getAll();
 * await contract.roles.grantRole("admin", "0x...");
 * ```
 * @public
 */
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- TO BE REMOVED IN V4
class ContractRoles {
  featureName = FEATURE_PERMISSIONS.name;

  /**
   * @internal
   * @remarks This is used for typing inside react hooks which is why it has to be public.
   */

  constructor(contractWrapper, roles) {
    this.contractWrapper = contractWrapper;
    this.roles = roles;
  }

  /** **************************
   * READ FUNCTIONS
   ****************************/

  /**
   * Get all members of all roles
   * @remarks See {@link ContractRoles.get} to get a list of addresses that are members of a specific role.
   * @example
   * ```javascript
   * const rolesAndMembers = await contract.roles.getAll();
   * ```
   * @returns A record of {@link Role}s to lists of addresses that are members of the given role.
   * @throws If the contract does not support roles this will throw an error.
   *
   * @public
   * @twfeature PermissionsEnumerable
   */
  async getAll() {
    invariant(this.roles.length, "this contract has no support for roles");
    const roles = {};
    const entries = Object.entries(this.roles);
    (await Promise.all(entries.map(_ref => {
      let [, role] = _ref;
      return this.get(role);
    }))).forEach((item, index) => roles[entries[index][1]] = item);
    return roles;
  }

  /**
   * Get all members of a specific role
   * @remarks See {@link ContractRoles.getAll} to get get a list of addresses for all supported roles on the contract.
   * @param role - The Role to to get a memberlist for.
   * @returns The list of addresses that are members of the specific role.
   * @throws If you are requesting a role that does not exist on the contract this will throw an error.
   *
   * @example Say you want to get the list of addresses that are members of the minter role.
   * ```javascript
   * const minterAddresses = await contract.roles.get("minter");
   * ```
   *
   * @public
   * @twfeature Permissions
   */
  async get(role) {
    invariant(this.roles.includes(role), `this contract does not support the "${role}" role`);
    const wrapper = this.contractWrapper;
    if (hasFunction("getRoleMemberCount", wrapper) && hasFunction("getRoleMember", wrapper)) {
      const roleHash = getRoleHash(role);
      const count = (await wrapper.read("getRoleMemberCount", [roleHash])).toNumber();
      return await Promise.all(Array.from(Array(count).keys()).map(i => wrapper.read("getRoleMember", [roleHash, i])));
    }
    throw new Error("Contract does not support enumerating roles. Please implement IPermissionsEnumerable to unlock this functionality.");
  }

  /**
   * Overwrite the list of members for specific roles
   *
   * @remarks Every role in the list will be overwritten with the new list of addresses provided with them.
   * If you want to add or remove addresses for a single address use {@link ContractRoles.grant} and {@link ContractRoles.revoke} respectively instead.
   * @param rolesWithAddresses - A record of {@link Role}s to lists of addresses that should be members of the given role.
   * @throws If you are requesting a role that does not exist on the contract this will throw an error.
   * @example Say you want to overwrite the list of addresses that are members of the minter role.
   * ```javascript
   * const minterAddresses = await contract.roles.get("minter");
   * await contract.roles.setAll({
   *  minter: []
   * });
   * console.log(await contract.roles.get("minter")); // No matter what members had the role before, the new list will be set to []
   * ```
   * @public
   * @twfeature Permissions
   *
   * */
  setAll = /* @__PURE__ */buildTransactionFunction(async (rolesWithAddresses, actingAddress) => {
    // if we are removing multiple roles, we need to allways remove the connected wallet address *last*
    // this is so we don't renounce (i.e.)  admin role first and then try to revoke someone else's (i.e.) admin role after (which will revert the entire txn because we are no longer an admin)
    // if it is explicitly passed in (i.e. for estimation) we use that value, otherwise we get it from the connected signer
    const connectedWalletAddress = actingAddress || (await this.contractWrapper.getSignerAddress());
    const contractEncoder = new ContractEncoder(this.contractWrapper);
    const roles = Object.keys(rolesWithAddresses);
    invariant(roles.length, "you must provide at least one role to set");
    invariant(roles.every(role => this.roles.includes(role)), "this contract does not support the given role");
    const currentRoles = await this.getAll();
    const encoded = [];
    // add / remove admin role at the end so we don't revoke admin then grant
    const sortedRoles = roles.sort(role => role === "admin" ? 1 : -1);
    for (let i = 0; i < sortedRoles.length; i++) {
      const role = sortedRoles[i];
      const [addresses, currentAddresses] = await Promise.all([Promise.all(rolesWithAddresses[role]?.map(addressOrEns => resolveAddress(addressOrEns)) || []), Promise.all(currentRoles[role]?.map(addressOrEns => resolveAddress(addressOrEns)) || [])]);
      const toAdd = addresses.filter(address => !currentAddresses.includes(address));
      const toRemove = currentAddresses.filter(address => !addresses.includes(address));

      // if we're removing more than one address we have to make sure we always remove the *connected* (acting) wallet address first
      // otherwise we'll revoke the connected wallet address and then try to revoke someone else's address which will revert the entire txn
      if (toRemove.length > 1) {
        const index = toRemove.indexOf(connectedWalletAddress);
        if (index > -1) {
          toRemove.splice(index, 1);
          toRemove.push(connectedWalletAddress);
        }
      }
      if (toAdd.length) {
        toAdd.forEach(address => {
          encoded.push(contractEncoder.encode("grantRole", [getRoleHash(role), address]));
        });
      }
      if (toRemove.length) {
        const revokeFunctionNames = await Promise.all(toRemove.map(address => this.getRevokeRoleFunctionName(address)));
        revokeFunctionNames.forEach((revokeFunctionName, index) => encoded.push(contractEncoder.encode(revokeFunctionName, [getRoleHash(role), toRemove[index]])));
      }
    }
    return Transaction.fromContractWrapper({
      contractWrapper: this.contractWrapper,
      method: "multicall",
      args: [encoded]
    });
  });

  /**
   * Throws an error if an address is missing the roles specified.
   *
   * @param roles - The roles to check
   * @param address - The address to check
   *
   * @internal
   */
  async verify(roles, address) {
    await Promise.all(roles.map(async role => {
      const [members, resolvedAddress] = await Promise.all([this.get(role), resolveAddress(address)]);
      if (!members.map(a => a.toLowerCase()).includes(resolvedAddress.toLowerCase())) {
        throw new MissingRoleError(resolvedAddress, role);
      }
    }));
  }

  /** **************************
   * WRITE FUNCTIONS
   ****************************/

  /**
   * Grant a role to a specific address
   *
   * @remarks Make sure you are sure you want to grant the role to the address.
   *
   * @example
   * ```javascript
   * await contract.roles.grant("minter", "{{wallet_address}}");
   * ```
   *
   * @param role - The {@link Role} to grant to the address
   * @param address - The address to grant the role to
   * @returns The transaction receipt
   * @throws If you are trying to grant does not exist on the contract this will throw an error.
   *
   * @public
   * @twfeature Permissions
   */
  grant = /* @__PURE__ */buildTransactionFunction(async (role, address) => {
    invariant(this.roles.includes(role), `this contract does not support the "${role}" role`);
    const resolvedAddress = await resolveAddress(address);
    return Transaction.fromContractWrapper({
      contractWrapper: this.contractWrapper,
      method: "grantRole",
      args: [getRoleHash(role), resolvedAddress]
    });
  });

  /**
   * Revoke a role from a specific address
   *
   * @remarks
   *
   * -- Caution --
   *
   * This will let you remove yourself from the role, too.
   * If you remove yourself from the admin role, you will no longer be able to administer the contract.
   * There is no way to recover from this.
   *
   * @example
   * ```javascript
   * await contract.roles.revoke("minter", "{{wallet_address}}");
   * ```
   *
   * @param role - The {@link Role} to revoke
   * @param address - The address to revoke the role from
   * @returns The transaction receipt
   * @throws If you are trying to revoke does not exist on the module this will throw an error.
   *
   * @public
   * @twfeature Permissions
   */
  revoke = /* @__PURE__ */buildTransactionFunction(async (role, address) => {
    invariant(this.roles.includes(role), `this contract does not support the "${role}" role`);
    const resolvedAddress = await resolveAddress(address);
    const revokeFunctionName = await this.getRevokeRoleFunctionName(resolvedAddress);
    return Transaction.fromContractWrapper({
      contractWrapper: this.contractWrapper,
      method: revokeFunctionName,
      args: [getRoleHash(role), resolvedAddress]
    });
  });

  /** **************************
   * PRIVATE FUNCTIONS
   ****************************/

  async getRevokeRoleFunctionName(address) {
    const [resolvedAddress, signerAddress] = await Promise.all([resolveAddress(address), this.contractWrapper.getSignerAddress()]);
    if (signerAddress.toLowerCase() === resolvedAddress.toLowerCase()) {
      return "renounceRole";
    }
    return "revokeRole";
  }
}

export { ContractRoles as C };
