import { useMemo, useEffect, useRef, useCallback, useState } from "react";
import { atom, useAtom } from "jotai";
import { getMainAccount, getParentAccount } from "../account";
import { getAccountBridge } from "../bridge";
import { getEnv } from "@ledgerhq/live-env";
import network from "@ledgerhq/live-network/network";
import { getWalletAPITransactionSignFlowInfos } from "./converters";
import { prepareMessageToSign } from "../hw/signMessage/index";
import BigNumber from "bignumber.js";
import { safeEncodeEIP55 } from "@ledgerhq/coin-evm/utils";
import { SmartWebsocket } from "./SmartWebsocket";
import { stripHexPrefix } from "./helpers";
import { getTxType } from "./utils/txTrackingHelper";
import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state";
const errors = {
    ParseError: -32700,
    InvalidRequest: -32600,
    MethodNotFound: -32601,
    InvalidParams: -32602,
    InternalError: -32603,
    UserRejected: 4001,
    Unauthorized: 4100,
    UnsupportedMethod: 4200,
    Disconnected: 4900,
    ChainDisconnected: 4901,
};
const rejectedError = (code, message, data = {}) => ({
    code,
    message,
    data: {
        code,
        message,
        ...data,
    },
});
// TODO remove any usage
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function convertEthToLiveTX(ethTX) {
    return {
        family: "ethereum",
        amount: ethTX.value !== undefined
            ? new BigNumber(ethTX.value.replace("0x", ""), 16)
            : new BigNumber(0),
        recipient: safeEncodeEIP55(ethTX.to),
        gasPrice: ethTX.gasPrice !== undefined
            ? new BigNumber(ethTX.gasPrice.replace("0x", ""), 16)
            : undefined,
        gasLimit: ethTX.gas !== undefined ? new BigNumber(ethTX.gas.replace("0x", ""), 16) : undefined,
        data: ethTX.data ? Buffer.from(ethTX.data.replace("0x", ""), "hex") : undefined,
    };
}
export const currentAccountAtom = atom(null);
export function useDappCurrentAccount(currentAccountHistDb) {
    const [currentAccount, setCurrentAccount] = useAtom(currentAccountAtom);
    // prefer using this setter when the user manually sets a current account
    const setCurrentAccountHist = useCallback((manifestId, account) => {
        if (!currentAccountHistDb)
            return;
        const [_, _setCurrentAccountHist] = currentAccountHistDb;
        _setCurrentAccountHist(state => {
            const newState = {
                ...state,
                currentAccountHist: {
                    ...state.currentAccountHist,
                    [manifestId]: account.id,
                },
            };
            return newState;
        });
    }, [currentAccountHistDb]);
    return { currentAccount, setCurrentAccount, setCurrentAccountHist };
}
const globCurrencies = ["**"];
function useDappAccountLogic({ manifest, accounts, currentAccountHistDb, initialAccountId, }) {
    const [initialAccountSelected, setInitialAccountSelected] = useState(false);
    const currencyIds = manifest.currencies === "*" ? globCurrencies : manifest.currencies;
    const { currentAccount, setCurrentAccount, setCurrentAccountHist } = useDappCurrentAccount(currentAccountHistDb);
    const currentParentAccount = useMemo(() => {
        if (currentAccount) {
            return getParentAccount(currentAccount, accounts);
        }
    }, [currentAccount, accounts]);
    const firstAccountAvailable = useMemo(() => {
        // Return an account for manifests with wildcard currencyIds
        if (currencyIds.includes("**") && accounts.length)
            return getParentAccount(accounts[0], accounts);
        const account = accounts.find(account => {
            if (account.type === "Account" && currencyIds.includes(account.currency.id)) {
                return account;
            }
            if (account.type === "TokenAccount" && currencyIds.includes(account.token.id)) {
                return getParentAccount(account, accounts);
            }
        });
        // might not even need to set parent here
        if (account) {
            return getParentAccount(account, accounts);
        }
    }, [accounts, currencyIds]);
    const storedCurrentAccountIsPermitted = useCallback(() => {
        if (!currentAccount)
            return false;
        return accounts.some(account => account.type === "Account" &&
            currencyIds.includes(account.currency.id) &&
            account.id === currentAccount.id);
    }, [currentAccount, accounts, currencyIds]);
    const currentAccountIdFromHist = useMemo(() => {
        if (manifest && currentAccountHistDb) {
            return currentAccountHistDb[0]?.[manifest.id];
        }
        return null;
    }, [manifest, currentAccountHistDb]);
    const currentAccountFromHist = useMemo(() => {
        return accounts.find(account => account.id === currentAccountIdFromHist);
    }, [accounts, currentAccountIdFromHist]);
    const initialAccount = useMemo(() => {
        if (!initialAccountId)
            return;
        return accounts.find(account => account.id === initialAccountId);
    }, [accounts, initialAccountId]);
    useEffect(() => {
        if (initialAccountSelected) {
            return;
        }
        if (initialAccount && !initialAccountSelected) {
            setCurrentAccount(initialAccount);
            setCurrentAccountHist(manifest.id, initialAccount);
            setInitialAccountSelected(true);
            return;
        }
        if (currentAccountFromHist) {
            setCurrentAccount(currentAccountFromHist);
            return;
        }
        if (!currentAccount || !(currentAccount && storedCurrentAccountIsPermitted())) {
            /** if there is no current account OR if there is a current account but it is not in the manifest currencies then fall back to the first permitted account */
            setCurrentAccount(firstAccountAvailable ?? null);
        }
    }, [
        currentAccount,
        currentAccountFromHist,
        firstAccountAvailable,
        initialAccount,
        initialAccountSelected,
        manifest.id,
        setCurrentAccount,
        setCurrentAccountHist,
        storedCurrentAccountIsPermitted,
    ]);
    return {
        currentAccount,
        setCurrentAccount,
        currentParentAccount,
        setCurrentAccountHist,
    };
}
// Type guard function to make typescript happy
function isParentAccountPresent(account, parentAccount) {
    if (account.type === "TokenAccount") {
        return !!parentAccount;
    }
    return true;
}
export function useDappLogic({ manifest, accounts, postMessage, uiHook, tracking, currentAccountHistDb, initialAccountId, mevProtected, }) {
    const nanoApp = manifest.dapp?.nanoApp;
    const dependencies = manifest.dapp?.dependencies;
    const ws = useRef();
    const { currentAccount, currentParentAccount, setCurrentAccount, setCurrentAccountHist } = useDappAccountLogic({
        manifest,
        accounts,
        currentAccountHistDb,
        initialAccountId,
    });
    /** Current network is needed for recognising the current chain id.
     * If a token account is selected, this depends on the parent currency. */
    const currentNetwork = useMemo(() => {
        if (!currentAccount) {
            return undefined;
        }
        // If the current account is a token account, and the chain id is not specified for that specific token, we can also use the network of the parent currency to determine the correct chain id.
        return manifest.dapp?.networks.find(network => {
            const accountCurrencyId = currentAccount.type === "TokenAccount"
                ? currentAccount.token.id
                : currentAccount.currency.id;
            const accountNetworkCurrency = currentAccount.type === "TokenAccount"
                ? currentAccount.token.parentCurrency.id
                : currentAccount.currency.id;
            return network.currency === accountCurrencyId || network.currency === accountNetworkCurrency;
        });
    }, [currentAccount, manifest.dapp?.networks]);
    const currentAddress = useMemo(() => {
        return currentAccount?.type === "Account"
            ? currentAccount.freshAddress
            : currentParentAccount?.freshAddress;
    }, [currentAccount, currentParentAccount?.freshAddress]);
    useEffect(() => {
        if (!currentAddress) {
            return;
        }
        postMessage(JSON.stringify({
            jsonrpc: "2.0",
            method: "accountsChanged",
            params: [[currentAddress]],
        }));
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [currentAddress]);
    useEffect(() => {
        if (!currentNetwork) {
            return;
        }
        postMessage(JSON.stringify({
            jsonrpc: "2.0",
            method: "chainChanged",
            params: [`0x${currentNetwork.chainID.toString(16)}`],
        }));
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [currentNetwork?.chainID]);
    useEffect(() => {
        if (currentNetwork?.nodeURL) {
            const rpcURL = new URL(currentNetwork.nodeURL);
            if (rpcURL.protocol === "wss:") {
                const websocket = new SmartWebsocket(rpcURL.toString(), {
                    reconnect: true,
                    reconnectMaxAttempts: Infinity,
                });
                websocket.on("message", message => {
                    postMessage(JSON.stringify(message));
                });
                websocket.connect();
                ws.current = websocket;
                return () => {
                    websocket.close();
                    ws.current = undefined;
                };
            }
        }
    }, [currentNetwork?.nodeURL, postMessage]);
    const onDappMessage = useCallback(async (data) => {
        if (data.jsonrpc !== "2.0") {
            console.error("Request is not a jsonrpc 2.0: ", data);
            return;
        }
        if (!currentNetwork) {
            console.error("No network selected: ", data);
            postMessage(JSON.stringify({
                id: data.id,
                jsonrpc: "2.0",
                error: rejectedError(errors.InternalError, "No network selected"),
            }));
            return;
        }
        if (!currentAccount) {
            console.error("No account selected: ", data);
            postMessage(JSON.stringify({
                id: data.id,
                jsonrpc: "2.0",
                error: rejectedError(errors.InternalError, "No account selected"),
            }));
            return;
        }
        if (!isParentAccountPresent(currentAccount, currentParentAccount)) {
            console.error("No parent account found for the currentAccount: ", currentAccount, data);
            postMessage(JSON.stringify({
                id: data.id,
                jsonrpc: "2.0",
                error: rejectedError(errors.InternalError, "No parent account found"),
            }));
            return;
        }
        switch (data.method) {
            // https://eips.ethereum.org/EIPS/eip-695
            case "eth_chainId": {
                postMessage(JSON.stringify({
                    id: data.id,
                    jsonrpc: "2.0",
                    result: `0x${currentNetwork.chainID.toString(16)}`,
                }));
                break;
            }
            // https://eips.ethereum.org/EIPS/eip-1102
            // https://docs.metamask.io/guide/rpc-api.html#eth-requestaccounts
            case "eth_requestAccounts":
            // legacy method, cf. https://docs.metamask.io/guide/ethereum-provider.html#legacy-methods
            // eslint-disable-next-line no-fallthrough
            case "enable":
            // https://eips.ethereum.org/EIPS/eip-1474#eth_accounts
            // https://eth.wiki/json-rpc/API#eth_accounts
            // eslint-disable-next-line no-fallthrough
            case "eth_accounts": {
                const address = currentAccount.type === "Account"
                    ? currentAccount.freshAddress
                    : currentParentAccount.freshAddress;
                postMessage(JSON.stringify({
                    id: data.id,
                    jsonrpc: "2.0",
                    result: [address],
                }));
                break;
            }
            // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3326.md
            case "wallet_switchEthereumChain": {
                const { chainId } = data.params[0];
                // Check chanId is valid hex string
                const decimalChainId = parseInt(chainId, 16);
                if (isNaN(decimalChainId)) {
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        error: rejectedError(errors.InvalidParams, "Invalid chainId"),
                    }));
                    break;
                }
                // Check chain ID is known to the wallet
                const requestedCurrency = manifest.dapp?.networks.find(network => network.chainID === decimalChainId);
                if (!requestedCurrency) {
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        error: rejectedError(errors.InvalidParams, `Chain ID ${chainId} is not supported`),
                    }));
                    break;
                }
                try {
                    await new Promise((resolve, reject) => uiHook["account.request"]({
                        currencyIds: [requestedCurrency.currency],
                        areCurrenciesFiltered: true,
                        onSuccess: account => {
                            setCurrentAccountHist(manifest.id, account);
                            setCurrentAccount(account);
                            resolve();
                        },
                        onCancel: () => {
                            reject("User canceled");
                        },
                    }));
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        result: null,
                    }));
                }
                catch (error) {
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        error: rejectedError(errors.UserRejected, `error switching chain: ${error}`),
                    }));
                }
                break;
            }
            // https://eth.wiki/json-rpc/API#eth_sendtransaction
            case "eth_sendTransaction": {
                const ethTX = data.params[0];
                const tx = convertEthToLiveTX(ethTX);
                const address = currentAccount.type === "Account"
                    ? currentAccount.freshAddress
                    : currentParentAccount.freshAddress;
                if (address.toLowerCase() === ethTX.from.toLowerCase()) {
                    let trackingData;
                    try {
                        const signFlowInfos = getWalletAPITransactionSignFlowInfos({
                            walletApiTransaction: tx,
                            account: currentAccount,
                        });
                        const transactionType = getTxType(signFlowInfos.liveTx);
                        const accountCurrencyName = currentAccount.type === "TokenAccount"
                            ? currentAccount.token.name
                            : currentAccount.currency.name;
                        const accountNetwork = currentAccount.type === "TokenAccount"
                            ? currentAccount.token.parentCurrency.id
                            : currentAccount.currency.id;
                        const token = await getCryptoAssetsStore().findTokenByAddressInCurrency(tx.recipient, accountNetwork);
                        trackingData = {
                            type: transactionType,
                            currency: token ? token.name : accountCurrencyName,
                            network: token ? token.parentCurrency.id : accountNetwork,
                        };
                        const options = nanoApp
                            ? { hwAppId: nanoApp, dependencies: dependencies }
                            : undefined;
                        tracking.dappSendTransactionRequested(manifest, trackingData);
                        const signedTransaction = await new Promise((resolve, reject) => uiHook["transaction.sign"]({
                            account: currentAccount,
                            parentAccount: undefined,
                            signFlowInfos,
                            options,
                            onSuccess: signedOperation => {
                                resolve(signedOperation);
                            },
                            onError: error => {
                                reject(error);
                            },
                        }));
                        const bridge = getAccountBridge(currentAccount, undefined);
                        const mainAccount = getMainAccount(currentAccount, undefined);
                        let optimisticOperation = signedTransaction.operation;
                        if (!getEnv("DISABLE_TRANSACTION_BROADCAST")) {
                            optimisticOperation = await bridge.broadcast({
                                account: mainAccount,
                                signedOperation: signedTransaction,
                                broadcastConfig: { mevProtected: !!mevProtected },
                            });
                        }
                        uiHook["transaction.broadcast"](currentAccount, undefined, mainAccount, optimisticOperation);
                        tracking.dappSendTransactionSuccess(manifest, trackingData);
                        postMessage(JSON.stringify({
                            id: data.id,
                            jsonrpc: "2.0",
                            result: optimisticOperation.hash,
                        }));
                    }
                    catch {
                        tracking.dappSendTransactionFail(manifest, trackingData);
                        postMessage(JSON.stringify({
                            id: data.id,
                            jsonrpc: "2.0",
                            error: rejectedError(errors.UserRejected, "Transaction declined"),
                        }));
                    }
                }
                break;
            }
            // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-191.md
            // https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sign
            // https://docs.walletconnect.com/json-rpc-api-methods/ethereum
            // Discussion about the diff between eth_sign and personal_sign:
            // https://github.com/WalletConnect/walletconnect-docs/issues/32#issuecomment-644697172
            case "personal_sign": {
                try {
                    /**
                     * The message is received as a prefixed hex string.
                     * We need to strip the "0x" prefix.
                     */
                    const message = stripHexPrefix(data.params[0]);
                    tracking.dappPersonalSignRequested(manifest);
                    const formattedMessage = prepareMessageToSign(currentAccount.type === "Account" ? currentAccount : currentParentAccount, message);
                    const options = nanoApp ? { hwAppId: nanoApp, dependencies: dependencies } : undefined;
                    const signedMessage = await new Promise((resolve, reject) => uiHook["message.sign"]({
                        account: currentAccount,
                        message: formattedMessage,
                        options,
                        onSuccess: resolve,
                        onError: reject,
                        onCancel: () => {
                            reject("Canceled by user");
                        },
                    }));
                    tracking.dappPersonalSignSuccess(manifest);
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        result: signedMessage,
                    }));
                }
                catch {
                    tracking.dappPersonalSignFail(manifest);
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        error: rejectedError(errors.UserRejected, "Personal message signed declined"),
                    }));
                }
                break;
            }
            // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md
            case data.method.match(/eth_signTypedData(_v.)?$/)?.input: {
                try {
                    const message = data.params[1];
                    tracking.dappSignTypedDataRequested(manifest);
                    const formattedMessage = prepareMessageToSign(currentAccount.type === "Account" ? currentAccount : currentParentAccount, Buffer.from(message).toString("hex"));
                    const options = nanoApp ? { hwAppId: nanoApp, dependencies: dependencies } : undefined;
                    const signedMessage = await new Promise((resolve, reject) => uiHook["message.sign"]({
                        account: currentAccount,
                        message: formattedMessage,
                        options,
                        onSuccess: resolve,
                        onError: reject,
                        onCancel: () => {
                            reject("Canceled by user");
                        },
                    }));
                    tracking.dappSignTypedDataSuccess(manifest);
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        result: signedMessage,
                    }));
                }
                catch {
                    tracking.dappSignTypedDataFail(manifest);
                    postMessage(JSON.stringify({
                        id: data.id,
                        jsonrpc: "2.0",
                        error: rejectedError(errors.UserRejected, "Typed Data message signed declined"),
                    }));
                }
                break;
            }
            default: {
                if (ws.current) {
                    ws.current.send(data);
                }
                else if (currentNetwork.nodeURL?.startsWith("https:")) {
                    void network({
                        method: "POST",
                        url: currentNetwork.nodeURL,
                        data,
                    }).then(res => {
                        postMessage(JSON.stringify(res.data));
                    });
                }
                break;
            }
        }
    }, [
        currentAccount,
        currentNetwork,
        currentParentAccount,
        dependencies,
        manifest,
        mevProtected,
        nanoApp,
        postMessage,
        setCurrentAccount,
        setCurrentAccountHist,
        tracking,
        uiHook,
    ]);
    return { onDappMessage, noAccounts: !currentAccount };
}
//# sourceMappingURL=useDappLogic.js.map