import type {
  EIP6963AnnounceProviderEvent,
  MetaMaskInpageProvider,
} from '@metamask/providers';

/**
 * Check if the current provider supports snaps by calling `wallet_getSnaps`.
 *
 * @param provider - The provider to use to check for snaps support. Defaults to
 * `window.ethereum`.
 * @returns True if the provider supports snaps, false otherwise.
 */
export async function hasSnapsSupport(
  provider: MetaMaskInpageProvider = window.ethereum,
) {
  try {
    await provider.request({
      method: 'wallet_getSnaps',
    });

    return true;
  } catch {
    return false;
  }
}

/**
 * Get a MetaMask provider using EIP6963. This will return the first provider
 * reporting as MetaMask. If no provider is found after 500ms, this will
 * return null instead.
 *
 * @returns A MetaMask provider if found, otherwise null.
 */
export async function getMetaMaskEIP6963Provider() {
  return new Promise<MetaMaskInpageProvider | null>((resolve) => {
    // Timeout looking for providers after 500ms
    const timeout = setTimeout(() => {
      resolveWithCleanup(null);
    }, 500);

    /**
     * Resolve the promise with a MetaMask provider and clean up.
     *
     * @param provider - A MetaMask provider if found, otherwise null.
     */
    function resolveWithCleanup(provider: MetaMaskInpageProvider | null) {
      window.removeEventListener(
        'eip6963:announceProvider',
        onAnnounceProvider,
      );

      clearTimeout(timeout);
      resolve(provider);
    }

    /**
     * Listener for the EIP6963 announceProvider event.
     *
     * Resolves the promise if a MetaMask provider is found.
     *
     * @param event - The EIP6963 announceProvider event.
     * @param event.detail - The details of the EIP6963 announceProvider event.
     */
    function onAnnounceProvider({ detail }: EIP6963AnnounceProviderEvent) {
      if (!detail) {
        return;
      }

      const { info, provider } = detail;

      if (info.rdns.includes('io.metamask')) {
        resolveWithCleanup(provider);
      }
    }

    window.addEventListener('eip6963:announceProvider', onAnnounceProvider);

    window.dispatchEvent(new Event('eip6963:requestProvider'));
  });
}

/**
 * Check if a provider reports as MetaMask Flask via web3_clientVersion.
 *
 * @param provider
 */
async function isFlaskProvider(
  provider: MetaMaskInpageProvider,
): Promise<boolean> {
  try {
    const clientVersion = (await provider.request({
      method: 'web3_clientVersion',
    })) as string | string[] | undefined;
    const versionStr = Array.isArray(clientVersion)
      ? clientVersion.join(' ')
      : String(clientVersion ?? '');
    return versionStr.toLowerCase().includes('flask');
  } catch {
    return false;
  }
}

/**
 * Get a provider that supports snaps. This will loop through all the detected
 * providers and return the first one that supports snaps. Prefers Flask when
 * multiple snaps-capable providers exist so the UI can show "Connect MetaMask Flask".
 *
 * @returns The provider, or `null` if no provider supports snaps.
 */
export async function getSnapsProvider() {
  if (typeof window === 'undefined') {
    return null;
  }

  if (await hasSnapsSupport()) {
    return window.ethereum;
  }

  if (window.ethereum?.detected) {
    for (const provider of window.ethereum.detected) {
      if (await hasSnapsSupport(provider)) {
        return provider;
      }
    }
  }

  if (window.ethereum?.providers) {
    const snapsProviders: MetaMaskInpageProvider[] = [];
    for (const provider of window.ethereum.providers) {
      if (await hasSnapsSupport(provider)) {
        snapsProviders.push(provider);
      }
    }
    if (snapsProviders.length > 0) {
      for (const provider of snapsProviders) {
        if (await isFlaskProvider(provider)) {
          return provider;
        }
      }
      return snapsProviders[0];
    }
  }

  const eip6963Provider = await getMetaMaskEIP6963Provider();

  if (eip6963Provider && (await hasSnapsSupport(eip6963Provider))) {
    return eip6963Provider;
  }

  // Fallback: if window.ethereum reports Flask, use it (Flask supports snaps;
  // hasSnapsSupport may have failed e.g. if wallet was locked).
  if (window.ethereum) {
    try {
      const clientVersion = (await window.ethereum.request({
        method: 'web3_clientVersion',
      })) as string | string[] | undefined;
      const versionStr = Array.isArray(clientVersion)
        ? clientVersion.join(' ')
        : String(clientVersion ?? '');
      if (versionStr.toLowerCase().includes('flask')) {
        return window.ethereum as MetaMaskInpageProvider;
      }
    } catch {
      // ignore
    }
  }

  return null;
}
