import { useMemo, useState, useEffect, useRef, useCallback, RefObject } from "react";
import semver from "semver";
import { intervalToDuration } from "date-fns";

import { Account, AccountLike, AnyMessage, Operation, SignedOperation } from "@ledgerhq/types-live";
import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets";
import { WalletHandlers, ServerConfig, WalletAPIServer } from "@ledgerhq/wallet-api-server";
import { useWalletAPIServer as useWalletAPIServerRaw } from "@ledgerhq/wallet-api-server/lib/react";
import { Transport, Permission } from "@ledgerhq/wallet-api-core";
import { StateDB } from "../hooks/useDBRaw";
import { Observable, firstValueFrom, Subject } from "rxjs";
import { first } from "rxjs/operators";
import {
  accountToWalletAPIAccount,
  currencyToWalletAPICurrency,
  getAccountIdFromWalletAccountId,
} from "./converters";
import { isWalletAPISupportedCurrency } from "./helpers";
import { WalletAPICurrency, AppManifest, WalletAPIAccount, WalletAPICustomHandlers } from "./types";
import { getMainAccount, getParentAccount } from "../account";
import {
  listCurrencies,
  findCryptoCurrencyById,
  findTokenById,
  getCryptoCurrencyById,
} from "../currencies";
import { TrackingAPI } from "./tracking";
import {
  bitcoinFamilyAccountGetXPubLogic,
  broadcastTransactionLogic,
  startExchangeLogic,
  completeExchangeLogic,
  CompleteExchangeRequest,
  CompleteExchangeUiRequest,
  receiveOnAccountLogic,
  signMessageLogic,
  signTransactionLogic,
  bitcoinFamilyAccountGetAddressLogic,
  bitcoinFamilyAccountGetPublicKeyLogic,
  signRawTransactionLogic,
} from "./logic";
import { getAccountBridge } from "../bridge";
import { getEnv } from "@ledgerhq/live-env";
import openTransportAsSubject, { BidirectionalEvent } from "../hw/openTransportAsSubject";
import { AppResult } from "../hw/actions/app";
import { UserRefusedOnDevice } from "@ledgerhq/errors";
import { Transaction } from "../generated/types";
import {
  DISCOVER_INITIAL_CATEGORY,
  INITIAL_PLATFORM_STATE,
  MAX_RECENTLY_USED_LENGTH,
} from "./constants";
import { DiscoverDB } from "./types";
import { LiveAppManifest } from "../platform/types";
import { WalletState } from "@ledgerhq/live-wallet/store";
import { ModularDrawerConfiguration } from "./ModularDrawer/types";

export function safeGetRefValue<T>(ref: RefObject<T>): NonNullable<T> {
  if (!ref.current) {
    throw new Error("Ref objects doesn't have a current value");
  }
  return ref.current;
}

export function useWalletAPIAccounts(
  walletState: WalletState,
  accounts: AccountLike[],
): WalletAPIAccount[] {
  return useMemo(() => {
    return accounts.map(account => {
      const parentAccount = getParentAccount(account, accounts);

      return accountToWalletAPIAccount(walletState, account, parentAccount);
    });
  }, [walletState, accounts]);
}

export function useWalletAPICurrencies(): WalletAPICurrency[] {
  return useMemo(() => {
    return listCurrencies(true).reduce<WalletAPICurrency[]>((filtered, currency) => {
      if (isWalletAPISupportedCurrency(currency)) {
        filtered.push(currencyToWalletAPICurrency(currency));
      }
      return filtered;
    }, []);
  }, []);
}

export function useManifestCurrencies(manifest: AppManifest) {
  return useMemo(() => {
    return (
      manifest.dapp?.networks.map(network => {
        return getCryptoCurrencyById(network.currency);
      }) ?? []
    );
  }, [manifest.dapp?.networks]);
}

export function useGetAccountIds(
  accounts$: Observable<WalletAPIAccount[]> | undefined,
): Map<string, boolean> | undefined {
  const [accounts, setAccounts] = useState<WalletAPIAccount[]>([]);

  useEffect(() => {
    if (!accounts$) {
      return undefined;
    }

    const subscription = accounts$.subscribe(walletAccounts => {
      setAccounts(walletAccounts);
    });

    return () => {
      subscription.unsubscribe();
    };
  }, [accounts$]);

  return useMemo(() => {
    if (!accounts$) {
      return undefined;
    }

    return accounts.reduce((accountIds, account) => {
      accountIds.set(getAccountIdFromWalletAccountId(account.id), true);
      return accountIds;
    }, new Map());
  }, [accounts, accounts$]);
}

export interface UiHook {
  "account.request": (params: {
    accounts$?: Observable<WalletAPIAccount[]>;
    currencies: CryptoOrTokenCurrency[];
    areCurrenciesFiltered?: boolean;
    useCase?: string;
    drawerConfiguration?: ModularDrawerConfiguration;
    onSuccess: (account: AccountLike, parentAccount: Account | undefined) => void;
    onCancel: () => void;
  }) => void;
  "account.receive": (params: {
    account: AccountLike;
    parentAccount: Account | undefined;
    accountAddress: string;
    onSuccess: (address: string) => void;
    onCancel: () => void;
    onError: (error: Error) => void;
  }) => void;
  "message.sign": (params: {
    account: AccountLike;
    message: AnyMessage;
    options: Parameters<WalletHandlers["message.sign"]>[0]["options"];
    onSuccess: (signature: string) => void;
    onError: (error: Error) => void;
    onCancel: () => void;
  }) => void;
  "storage.get": WalletHandlers["storage.get"];
  "storage.set": WalletHandlers["storage.set"];
  "transaction.signRaw": (params: {
    account: AccountLike;
    parentAccount: Account | undefined;
    transaction: string;
    options: Parameters<WalletHandlers["transaction.sign"]>[0]["options"];
    onSuccess: (signedOperation: SignedOperation) => void;
    onError: (error: Error) => void;
  }) => void;
  "transaction.sign": (params: {
    account: AccountLike;
    parentAccount: Account | undefined;
    signFlowInfos: {
      canEditFees: boolean;
      hasFeesProvided: boolean;
      liveTx: Partial<Transaction>;
    };
    options: Parameters<WalletHandlers["transaction.sign"]>[0]["options"];
    onSuccess: (signedOperation: SignedOperation) => void;
    onError: (error: Error) => void;
  }) => void;
  "transaction.broadcast": (
    account: AccountLike,
    parentAccount: Account | undefined,
    mainAccount: Account,
    optimisticOperation: Operation,
  ) => void;
  "device.transport": (params: {
    appName: string | undefined;
    onSuccess: (result: AppResult) => void;
    onCancel: () => void;
  }) => void;
  "device.select": (params: {
    appName: string | undefined;
    onSuccess: (result: AppResult) => void;
    onCancel: () => void;
  }) => void;
  "exchange.start": (params: {
    exchangeType: "SWAP" | "FUND" | "SELL" | "SWAP_NG" | "SELL_NG" | "FUND_NG";
    onSuccess: (nonce: string) => void;
    onCancel: (error: Error) => void;
  }) => void;
  "exchange.complete": (params: {
    exchangeParams: CompleteExchangeUiRequest;
    onSuccess: (hash: string) => void;
    onCancel: (error: Error) => void;
  }) => void;
}

export function usePermission(manifest: AppManifest): Permission {
  return useMemo(
    () => ({
      currencyIds: manifest.currencies === "*" ? ["**"] : manifest.currencies,
      methodIds: manifest.permissions as unknown as string[], // TODO remove when using the correct manifest type
    }),
    [manifest],
  );
}

function useTransport(postMessage: (message: string) => void | undefined): Transport {
  return useMemo(() => {
    return {
      onMessage: undefined,
      send: postMessage,
    };
  }, [postMessage]);
}

export function useConfig({
  appId,
  userId,
  tracking,
  wallet,
  mevProtected,
}: ServerConfig): ServerConfig {
  return useMemo(
    () => ({
      appId,
      userId,
      tracking,
      wallet,
      mevProtected,
    }),
    [appId, tracking, userId, wallet],
  );
}

function useDeviceTransport({ manifest, tracking }) {
  const ref = useRef<Subject<BidirectionalEvent> | undefined>();

  const subscribe = useCallback((deviceId: string) => {
    ref.current = openTransportAsSubject({ deviceId });

    ref.current.subscribe({
      complete: () => {
        ref.current = undefined;
      },
    });
  }, []);

  const close = useCallback(() => {
    ref.current?.complete();
  }, []);

  const exchange = useCallback<WalletHandlers["device.exchange"]>(
    ({ apduHex }) => {
      const subject$ = ref.current;

      return new Promise((resolve, reject) => {
        if (!subject$) {
          reject(new Error("No device transport"));
          return;
        }

        subject$.pipe(first(e => e.type === "device-response" || e.type === "error")).subscribe({
          next: e => {
            if (e.type === "device-response") {
              tracking.deviceExchangeSuccess(manifest);
              resolve(e.data);
              return;
            }
            if (e.type === "error") {
              tracking.deviceExchangeFail(manifest);
              reject(e.error || new Error("deviceExchange: unknown error"));
            }
          },
          error: error => {
            tracking.deviceExchangeFail(manifest);
            reject(error);
          },
        });

        subject$.next({ type: "input-frame", apduHex });
      });
    },
    [manifest, tracking],
  );

  useEffect(() => {
    return () => {
      ref.current?.complete();
    };
  }, []);

  return useMemo(() => ({ ref, subscribe, close, exchange }), [close, exchange, subscribe]);
}

export type useWalletAPIServerOptions = {
  walletState: WalletState;
  manifest: AppManifest;
  accounts: AccountLike[];
  tracking: TrackingAPI;
  config: ServerConfig;
  webviewHook: {
    reload: () => void;
    postMessage: (message: string) => void;
  };
  uiHook: Partial<UiHook>;
  customHandlers?: WalletAPICustomHandlers;
};

export function useWalletAPIServer({
  walletState,
  manifest,
  accounts,
  tracking,
  config,
  webviewHook,
  uiHook: {
    "account.request": uiAccountRequest,
    "account.receive": uiAccountReceive,
    "message.sign": uiMessageSign,
    "storage.get": uiStorageGet,
    "storage.set": uiStorageSet,
    "transaction.sign": uiTxSign,
    "transaction.signRaw": uiTxSignRaw,
    "transaction.broadcast": uiTxBroadcast,
    "device.transport": uiDeviceTransport,
    "device.select": uiDeviceSelect,
    "exchange.start": uiExchangeStart,
    "exchange.complete": uiExchangeComplete,
  },
  customHandlers,
}: useWalletAPIServerOptions): {
  onMessage: (event: string) => void;
  server: WalletAPIServer;
  onLoad: () => void;
  onReload: () => void;
  onLoadError: () => void;
  widgetLoaded: boolean;
} {
  const permission = usePermission(manifest);
  const transport = useTransport(webviewHook.postMessage);
  const [widgetLoaded, setWidgetLoaded] = useState(false);

  const walletAPIAccounts = useWalletAPIAccounts(walletState, accounts);
  const walletAPICurrencies = useWalletAPICurrencies();

  const { server, onMessage } = useWalletAPIServerRaw({
    transport,
    config,
    accounts: walletAPIAccounts,
    currencies: walletAPICurrencies,
    permission,
    customHandlers,
  });

  useEffect(() => {
    tracking.load(manifest);
  }, [tracking, manifest]);

  useEffect(() => {
    if (!uiAccountRequest) return;

    server.setHandler(
      "account.request",
      async ({ accounts$, currencies$, drawerConfiguration, areCurrenciesFiltered, useCase }) => {
        tracking.requestAccountRequested(manifest);
        const currencies = await firstValueFrom(currencies$);

        return new Promise((resolve, reject) => {
          // handle no curencies selected case
          const currencyList = currencies.reduce<CryptoOrTokenCurrency[]>((prev, { id }) => {
            const currency = findCryptoCurrencyById(id) || findTokenById(id);
            if (currency) {
              prev.push(currency);
            }
            return prev;
          }, []);

          let done = false;
          uiAccountRequest({
            accounts$,
            currencies: currencyList,
            drawerConfiguration,
            areCurrenciesFiltered,
            useCase,
            onSuccess: (account: AccountLike, parentAccount: Account | undefined) => {
              if (done) return;
              done = true;
              tracking.requestAccountSuccess(manifest);
              resolve(accountToWalletAPIAccount(walletState, account, parentAccount));
            },
            onCancel: () => {
              if (done) return;
              done = true;
              tracking.requestAccountFail(manifest);
              reject(new Error("Canceled by user"));
            },
          });
        });
      },
    );
  }, [walletState, manifest, server, tracking, uiAccountRequest]);

  useEffect(() => {
    if (!uiAccountReceive) return;

    server.setHandler("account.receive", ({ account, tokenCurrency }) =>
      receiveOnAccountLogic(
        walletState,
        { manifest, accounts, tracking },
        account.id,
        (account, parentAccount, accountAddress) =>
          new Promise((resolve, reject) => {
            let done = false;
            return uiAccountReceive({
              account,
              parentAccount,
              accountAddress,
              onSuccess: accountAddress => {
                if (done) return;
                done = true;
                tracking.receiveSuccess(manifest);
                resolve(accountAddress);
              },
              onCancel: () => {
                if (done) return;
                done = true;
                tracking.receiveFail(manifest);
                reject(new Error("User cancelled"));
              },
              onError: error => {
                if (done) return;
                done = true;
                tracking.receiveFail(manifest);
                reject(error);
              },
            });
          }),
        tokenCurrency,
      ),
    );
  }, [walletState, accounts, manifest, server, tracking, uiAccountReceive]);

  useEffect(() => {
    if (!uiMessageSign) return;

    server.setHandler("message.sign", ({ account, message, options }) =>
      signMessageLogic(
        { manifest, accounts, tracking },
        account.id,
        message.toString("hex"),
        (account: AccountLike, message: AnyMessage) =>
          new Promise((resolve, reject) => {
            let done = false;
            return uiMessageSign({
              account,
              message,
              options,
              onSuccess: signature => {
                if (done) return;
                done = true;
                tracking.signMessageSuccess(manifest);
                resolve(
                  signature.startsWith("0x")
                    ? Buffer.from(signature.replace("0x", ""), "hex")
                    : Buffer.from(signature),
                );
              },
              onCancel: () => {
                if (done) return;
                done = true;
                tracking.signMessageFail(manifest);
                reject(new UserRefusedOnDevice());
              },
              onError: error => {
                if (done) return;
                done = true;
                tracking.signMessageFail(manifest);
                reject(error);
              },
            });
          }),
      ),
    );
  }, [accounts, manifest, server, tracking, uiMessageSign]);

  useEffect(() => {
    if (!uiStorageGet) return;

    server.setHandler("storage.get", uiStorageGet);
  }, [server, uiStorageGet]);

  useEffect(() => {
    if (!uiStorageSet) return;

    server.setHandler("storage.set", uiStorageSet);
  }, [server, uiStorageSet]);

  useEffect(() => {
    if (!uiTxSign) return;

    server.setHandler(
      "transaction.sign",
      async ({ account, tokenCurrency, transaction, options }) => {
        const signedOperation = await signTransactionLogic(
          { manifest, accounts, tracking },
          account.id,
          transaction,
          (account, parentAccount, signFlowInfos) =>
            new Promise((resolve, reject) => {
              let done = false;
              return uiTxSign({
                account,
                parentAccount,
                signFlowInfos,
                options,
                onSuccess: signedOperation => {
                  if (done) return;
                  done = true;
                  tracking.signTransactionSuccess(manifest);
                  resolve(signedOperation);
                },
                onError: error => {
                  if (done) return;
                  done = true;
                  tracking.signTransactionFail(manifest);
                  reject(error);
                },
              });
            }),
          tokenCurrency,
        );

        return account.currency === "solana"
          ? Buffer.from(signedOperation.signature, "hex")
          : Buffer.from(signedOperation.signature);
      },
    );
  }, [accounts, manifest, server, tracking, uiTxSign]);

  useEffect(() => {
    if (!uiTxSignRaw) return;

    server.setHandler(
      "transaction.signRaw",
      async ({ account, transaction, broadcast, options }) => {
        const signedOperation = await signRawTransactionLogic(
          { manifest, accounts, tracking },
          account.id,
          transaction,
          (account, parentAccount, tx) =>
            new Promise((resolve, reject) => {
              let done = false;
              return uiTxSignRaw({
                account,
                parentAccount,
                transaction: tx,
                options,
                onSuccess: signedOperation => {
                  if (done) return;
                  done = true;
                  tracking.signRawTransactionSuccess(manifest);
                  resolve(signedOperation);
                },
                onError: error => {
                  if (done) return;
                  done = true;
                  tracking.signRawTransactionFail(manifest);
                  reject(error);
                },
              });
            }),
        );

        let hash: string | undefined;
        if (broadcast) {
          hash = await broadcastTransactionLogic(
            { manifest, accounts, tracking },
            account.id,
            signedOperation,
            async (account, parentAccount, signedOperation) => {
              const bridge = getAccountBridge(account, parentAccount);
              const mainAccount = getMainAccount(account, parentAccount);

              let optimisticOperation: Operation = signedOperation.operation;

              if (!getEnv("DISABLE_TRANSACTION_BROADCAST")) {
                try {
                  optimisticOperation = await bridge.broadcast({
                    account: mainAccount,
                    signedOperation,
                    broadcastConfig: { mevProtected: !!config.mevProtected },
                  });
                  tracking.broadcastSuccess(manifest);
                } catch (error) {
                  tracking.broadcastFail(manifest);
                  throw error;
                }
              }

              uiTxBroadcast &&
                uiTxBroadcast(account, parentAccount, mainAccount, optimisticOperation);

              return optimisticOperation.hash;
            },
          );
        }

        return {
          signedTransactionHex: signedOperation.signature,
          transactionHash: hash,
        };
      },
    );
  }, [accounts, config.mevProtected, manifest, server, tracking, uiTxBroadcast, uiTxSignRaw]);

  useEffect(() => {
    if (!uiTxSign) return;

    server.setHandler(
      "transaction.signAndBroadcast",
      async ({ account, tokenCurrency, transaction, options }) => {
        const signedTransaction = await signTransactionLogic(
          { manifest, accounts, tracking },
          account.id,
          transaction,
          (account, parentAccount, signFlowInfos) =>
            new Promise((resolve, reject) => {
              let done = false;
              return uiTxSign({
                account,
                parentAccount,
                signFlowInfos,
                options,
                onSuccess: signedOperation => {
                  if (done) return;
                  done = true;
                  tracking.signTransactionSuccess(manifest);
                  resolve(signedOperation);
                },
                onError: error => {
                  if (done) return;
                  done = true;
                  tracking.signTransactionFail(manifest);
                  reject(error);
                },
              });
            }),
          tokenCurrency,
        );

        return broadcastTransactionLogic(
          { manifest, accounts, tracking },
          account.id,
          signedTransaction,
          async (account, parentAccount, signedOperation) => {
            const bridge = getAccountBridge(account, parentAccount);
            const mainAccount = getMainAccount(account, parentAccount);

            let optimisticOperation: Operation = signedOperation.operation;

            if (!getEnv("DISABLE_TRANSACTION_BROADCAST")) {
              try {
                optimisticOperation = await bridge.broadcast({
                  account: mainAccount,
                  signedOperation,
                  broadcastConfig: { mevProtected: !!config.mevProtected },
                });
                tracking.broadcastSuccess(manifest);
              } catch (error) {
                tracking.broadcastFail(manifest);
                throw error;
              }
            }

            uiTxBroadcast &&
              uiTxBroadcast(account, parentAccount, mainAccount, optimisticOperation);

            return optimisticOperation.hash;
          },
          tokenCurrency,
        );
      },
    );
  }, [accounts, config.mevProtected, manifest, server, tracking, uiTxBroadcast, uiTxSign]);

  const onLoad = useCallback(() => {
    tracking.loadSuccess(manifest);
    setWidgetLoaded(true);
  }, [manifest, tracking]);

  const onReload = useCallback(() => {
    tracking.reload(manifest);
    setWidgetLoaded(false);

    webviewHook.reload();
  }, [manifest, tracking, webviewHook]);

  const onLoadError = useCallback(() => {
    tracking.loadFail(manifest);
  }, [manifest, tracking]);

  const device = useDeviceTransport({ manifest, tracking });

  useEffect(() => {
    if (!uiDeviceTransport) return;

    server.setHandler(
      "device.transport",
      ({ appName, appVersionRange, devices }) =>
        new Promise((resolve, reject) => {
          if (device.ref.current) {
            return reject(new Error("Device already opened"));
          }

          tracking.deviceTransportRequested(manifest);

          let done = false;
          return uiDeviceTransport({
            appName,
            onSuccess: ({ device: deviceParam, appAndVersion }) => {
              if (done) return;
              done = true;
              tracking.deviceTransportSuccess(manifest);

              if (!deviceParam) {
                reject(new Error("No device"));
                return;
              }
              if (devices && !devices.includes(deviceParam.modelId)) {
                reject(new Error("Device not in the devices list"));
                return;
              }
              if (
                appVersionRange &&
                appAndVersion &&
                semver.satisfies(appAndVersion.version, appVersionRange)
              ) {
                reject(new Error("App version doesn't satisfies the range"));
                return;
              }
              // TODO handle appFirmwareRange & seeded params
              device.subscribe(deviceParam.deviceId);
              resolve("1");
            },
            onCancel: () => {
              if (done) return;
              done = true;
              tracking.deviceTransportFail(manifest);
              reject(new Error("User cancelled"));
            },
          });
        }),
    );
  }, [device, manifest, server, tracking, uiDeviceTransport]);

  useEffect(() => {
    if (!uiDeviceSelect) return;

    server.setHandler(
      "device.select",
      ({ appName, appVersionRange, devices }) =>
        new Promise((resolve, reject) => {
          if (device.ref.current) {
            return reject(new Error("Device already opened"));
          }

          tracking.deviceSelectRequested(manifest);

          let done = false;
          return uiDeviceSelect({
            appName,
            onSuccess: ({ device: deviceParam, appAndVersion }) => {
              if (done) return;
              done = true;
              tracking.deviceSelectSuccess(manifest);

              if (!deviceParam) {
                reject(new Error("No device"));
                return;
              }
              if (devices && !devices.includes(deviceParam.modelId)) {
                reject(new Error("Device not in the devices list"));
                return;
              }
              if (
                appVersionRange &&
                appAndVersion &&
                semver.satisfies(appAndVersion.version, appVersionRange)
              ) {
                reject(new Error("App version doesn't satisfies the range"));
                return;
              }
              resolve(deviceParam.deviceId);
            },
            onCancel: () => {
              if (done) return;
              done = true;
              tracking.deviceSelectFail(manifest);
              reject(new Error("User cancelled"));
            },
          });
        }),
    );
  }, [device.ref, manifest, server, tracking, uiDeviceSelect]);

  useEffect(() => {
    server.setHandler("device.open", params => {
      if (device.ref.current) {
        return Promise.reject(new Error("Device already opened"));
      }

      tracking.deviceOpenRequested(manifest);

      device.subscribe(params.deviceId);
      return "1";
    });
  }, [device, manifest, server, tracking]);

  useEffect(() => {
    server.setHandler("device.exchange", params => {
      if (!device.ref.current) {
        return Promise.reject(new Error("No device opened"));
      }

      tracking.deviceExchangeRequested(manifest);

      return device.exchange(params);
    });
  }, [device, manifest, server, tracking]);

  useEffect(() => {
    server.setHandler("device.close", ({ transportId }) => {
      if (!device.ref.current) {
        return Promise.reject(new Error("No device opened"));
      }

      tracking.deviceCloseRequested(manifest);

      device.close();

      tracking.deviceCloseSuccess(manifest);

      return Promise.resolve(transportId);
    });
  }, [device, manifest, server, tracking]);

  useEffect(() => {
    server.setHandler("bitcoin.getAddress", ({ accountId, derivationPath }) => {
      return bitcoinFamilyAccountGetAddressLogic(
        { manifest, accounts, tracking },
        accountId,
        derivationPath,
      );
    });
  }, [accounts, manifest, server, tracking]);

  useEffect(() => {
    server.setHandler("bitcoin.getPublicKey", ({ accountId, derivationPath }) => {
      return bitcoinFamilyAccountGetPublicKeyLogic(
        { manifest, accounts, tracking },
        accountId,
        derivationPath,
      );
    });
  }, [accounts, manifest, server, tracking]);

  useEffect(() => {
    server.setHandler("bitcoin.getXPub", ({ accountId }) => {
      return bitcoinFamilyAccountGetXPubLogic({ manifest, accounts, tracking }, accountId);
    });
  }, [accounts, manifest, server, tracking]);

  useEffect(() => {
    if (!uiExchangeStart) {
      return;
    }

    server.setHandler("exchange.start", ({ exchangeType }) => {
      return startExchangeLogic(
        { manifest, accounts, tracking },
        exchangeType,
        exchangeType =>
          new Promise((resolve, reject) => {
            let done = false;
            return uiExchangeStart({
              exchangeType,
              onSuccess: (nonce: string) => {
                if (done) return;
                done = true;
                tracking.startExchangeSuccess(manifest);
                resolve(nonce);
              },
              onCancel: error => {
                if (done) return;
                done = true;
                tracking.completeExchangeFail(manifest);
                reject(error);
              },
            });
          }),
      );
    });
  }, [uiExchangeStart, accounts, manifest, server, tracking]);

  useEffect(() => {
    if (!uiExchangeComplete) {
      return;
    }

    server.setHandler("exchange.complete", params => {
      // retrofit of the exchange params to fit the old platform spec
      const request: CompleteExchangeRequest = {
        provider: params.provider,
        fromAccountId: params.fromAccount.id,
        toAccountId: params.exchangeType === "SWAP" ? params.toAccount.id : undefined,
        transaction: params.transaction,
        binaryPayload: params.binaryPayload.toString("hex"),
        signature: params.signature.toString("hex"),
        feesStrategy: params.feeStrategy,
        exchangeType: ExchangeType[params.exchangeType],
        swapId: params.exchangeType === "SWAP" ? params.swapId : undefined,
        rate: params.exchangeType === "SWAP" ? params.rate : undefined,
        tokenCurrency: params.exchangeType !== "SELL" ? params.tokenCurrency : undefined,
      };

      return completeExchangeLogic(
        { manifest, accounts, tracking },
        request,
        request =>
          new Promise((resolve, reject) => {
            let done = false;
            return uiExchangeComplete({
              exchangeParams: request,
              onSuccess: (hash: string) => {
                if (done) return;
                done = true;
                tracking.completeExchangeSuccess(manifest);
                resolve(hash);
              },
              onCancel: error => {
                if (done) return;
                done = true;
                tracking.completeExchangeFail(manifest);
                reject(error);
              },
            });
          }),
      );
    });
  }, [uiExchangeComplete, accounts, manifest, server, tracking]);

  return {
    widgetLoaded,
    onMessage,
    onLoad,
    onReload,
    onLoadError,
    server,
  };
}

export enum ExchangeType {
  SWAP = 0x00,
  SELL = 0x01,
  FUND = 0x02,
  SWAP_NG = 0x03,
  SELL_NG = 0x04,
  FUND_NG = 0x05,
}

export interface Categories {
  categories: string[];
  manifestsByCategories: Map<string, AppManifest[]>;
  selected: string;
  setSelected: (val: string) => void;
  reset: () => void;
}

/** e.g. "all", "restaking", "services", etc */
export type CategoryId = Categories["selected"];

export function useCategories(manifests, initialCategory?: CategoryId | null): Categories {
  const [selected, setSelected] = useState(initialCategory || DISCOVER_INITIAL_CATEGORY);

  const reset = useCallback(() => {
    setSelected(DISCOVER_INITIAL_CATEGORY);
  }, []);

  const manifestsByCategories = useMemo(() => {
    const res = manifests.reduce(
      (res, manifest) => {
        manifest.categories.forEach(category => {
          const list = res.has(category) ? [...res.get(category), manifest] : [manifest];
          res.set(category, list);
        });

        return res;
      },
      new Map().set("all", manifests),
    );

    return res;
  }, [manifests]);

  const categories = useMemo(() => [...manifestsByCategories.keys()], [manifestsByCategories]);

  return useMemo(
    () => ({
      categories,
      manifestsByCategories,
      selected,
      setSelected,
      reset,
    }),
    [categories, manifestsByCategories, selected, reset],
  );
}

export type RecentlyUsedDB = StateDB<DiscoverDB, DiscoverDB["recentlyUsed"]>;
export type CacheBustedLiveAppsdDB = StateDB<DiscoverDB, DiscoverDB["cacheBustedLiveApps"]>;
export type LocalLiveAppDB = StateDB<DiscoverDB, DiscoverDB["localLiveApp"]>;
export type CurrentAccountHistDB = StateDB<DiscoverDB, DiscoverDB["currentAccountHist"]>;

export interface LocalLiveApp {
  state: LiveAppManifest[];
  addLocalManifest: (LiveAppManifest) => void;
  removeLocalManifestById: (string) => void;
  getLocalLiveAppManifestById: (string) => LiveAppManifest | undefined;
}

export function useLocalLiveApp([LocalLiveAppDb, setState]: LocalLiveAppDB): LocalLiveApp {
  useEffect(() => {
    if (LocalLiveAppDb === undefined) {
      setState(discoverDB => {
        return { ...discoverDB, localLiveApp: INITIAL_PLATFORM_STATE.localLiveApp };
      });
    }
  }, [LocalLiveAppDb, setState]);

  const addLocalManifest = useCallback(
    (newLocalManifest: LiveAppManifest) => {
      setState(discoverDB => {
        const newLocalLiveAppList = discoverDB.localLiveApp?.filter(
          manifest => manifest.id !== newLocalManifest.id,
        );

        newLocalLiveAppList.push(newLocalManifest);
        return { ...discoverDB, localLiveApp: newLocalLiveAppList };
      });
    },
    [setState],
  );

  const removeLocalManifestById = useCallback(
    (manifestId: string) => {
      setState(discoverDB => {
        const newLocalLiveAppList = discoverDB.localLiveApp.filter(
          manifest => manifest.id !== manifestId,
        );

        return { ...discoverDB, localLiveApp: newLocalLiveAppList };
      });
    },
    [setState],
  );

  const getLocalLiveAppManifestById = useCallback(
    (manifestId: string): LiveAppManifest | undefined => {
      return LocalLiveAppDb.find(manifest => manifest.id === manifestId);
    },
    [LocalLiveAppDb],
  );

  return {
    state: LocalLiveAppDb,
    addLocalManifest,
    removeLocalManifestById,
    getLocalLiveAppManifestById,
  };
}

export interface RecentlyUsed {
  data: RecentlyUsedManifest[];
  append: (manifest: AppManifest) => void;
  clear: () => void;
}

export type RecentlyUsedManifest = AppManifest & { usedAt: UsedAt };
export type UsedAt = {
  unit?: Intl.RelativeTimeFormatUnit;
  diff: number;
};

function calculateTimeDiff(usedAt: string): UsedAt {
  const start = new Date();
  const end = new Date(usedAt);
  const interval = intervalToDuration({ start, end });
  const units: Intl.RelativeTimeFormatUnit[] = [
    "years",
    "months",
    "weeks",
    "days",
    "hours",
    "minutes",
    "seconds",
  ];
  let timeDiff: UsedAt = { unit: undefined, diff: 0 };

  for (const unit of units) {
    if (interval[unit] > 0) {
      timeDiff = { unit, diff: interval[unit] };
      break;
    }
  }

  return timeDiff;
}
export function useCacheBustedLiveApps([cacheBustedLiveAppsDb, setState]: CacheBustedLiveAppsdDB) {
  const getLatest = useCallback(
    (manifestId: string) => {
      return cacheBustedLiveAppsDb?.[manifestId];
    },
    [cacheBustedLiveAppsDb],
  );
  const edit = useCallback(
    (manifestId: string, cacheBustingId: number) => {
      const _cacheBustedLiveAppsDb = {
        ...cacheBustedLiveAppsDb,
        [manifestId]: cacheBustingId,
        init: 1,
      };
      setState(state => {
        const newstate = { ...state, cacheBustedLiveApps: _cacheBustedLiveAppsDb };
        return newstate;
      });
    },
    [setState, cacheBustedLiveAppsDb],
  );
  return { getLatest, edit };
}

export function useRecentlyUsed(
  manifests: AppManifest[],
  [recentlyUsedManifestsDb, setState]: RecentlyUsedDB,
): RecentlyUsed {
  const data = useMemo(
    () =>
      recentlyUsedManifestsDb
        .map(recentlyUsed => {
          const res = manifests.find(manifest => manifest.id === recentlyUsed.id);
          return res
            ? {
                ...res,
                usedAt: calculateTimeDiff(recentlyUsed.usedAt),
              }
            : res;
        })
        .filter(manifest => manifest !== undefined) as RecentlyUsedManifest[],
    [recentlyUsedManifestsDb, manifests],
  );
  const append = useCallback(
    (manifest: AppManifest) => {
      setState(state => {
        const index = state.recentlyUsed.findIndex(({ id }) => id === manifest.id);

        // Manifest already in first position
        if (index === 0) {
          return {
            ...state,
            recentlyUsed: [
              { ...state.recentlyUsed[0], usedAt: new Date().toISOString() },
              ...state.recentlyUsed.slice(1),
            ],
          };
        }

        // Manifest present we move it to the first position
        // No need to check for MAX_LENGTH as we only move it
        if (index !== -1) {
          return {
            ...state,
            recentlyUsed: [
              { id: manifest.id, usedAt: new Date().toISOString() },
              ...state.recentlyUsed.slice(0, index),
              ...state.recentlyUsed.slice(index + 1),
            ],
          };
        }

        // Manifest not preset we simply append and check for the length
        return {
          ...state,
          recentlyUsed:
            state.recentlyUsed.length >= MAX_RECENTLY_USED_LENGTH
              ? [
                  { id: manifest.id, usedAt: new Date().toISOString() },
                  ...state.recentlyUsed.slice(0, -1),
                ]
              : [{ id: manifest.id, usedAt: new Date().toISOString() }, ...state.recentlyUsed],
        };
      });
    },
    [setState],
  );

  const clear = useCallback(() => {
    setState(state => ({ ...state, recentlyUsed: [] }));
  }, [setState]);

  return { data, append, clear };
}

export interface DisclaimerRaw {
  onConfirm: (manifest: AppManifest, isChecked: boolean) => void;
  onSelect: (manifest: AppManifest) => void;
}

interface DisclaimerUiHook {
  prompt: (
    manifest: AppManifest,
    onContinue: (manifest: AppManifest, isChecked: boolean) => void,
  ) => void;
  dismiss: () => void;
  openApp: (manifest: AppManifest) => void;
  close: () => void;
}

export function useDisclaimerRaw({
  isReadOnly = false,
  isDismissed,
  uiHook,
  appendRecentlyUsed,
}: {
  // used only on mobile for now
  isReadOnly?: boolean;
  isDismissed: boolean;
  appendRecentlyUsed: (manifest: AppManifest) => void;
  uiHook: DisclaimerUiHook;
}): DisclaimerRaw {
  const onConfirm = useCallback(
    (manifest: AppManifest, isChecked: boolean) => {
      if (!manifest) return;

      if (isChecked) {
        uiHook.dismiss();
      }

      uiHook.close();
      appendRecentlyUsed(manifest);
      uiHook.openApp(manifest);
    },
    [uiHook, appendRecentlyUsed],
  );

  const onSelect = useCallback(
    (manifest: AppManifest) => {
      if (manifest.branch === "soon") {
        return;
      }

      if (!isDismissed && !isReadOnly && manifest.author !== "ledger") {
        uiHook.prompt(manifest, onConfirm);
      } else {
        appendRecentlyUsed(manifest);
        uiHook.openApp(manifest);
      }
    },
    [isReadOnly, isDismissed, uiHook, appendRecentlyUsed, onConfirm],
  );

  return {
    onSelect,
    onConfirm,
  };
}
