import { useCallback, useEffect, useMemo } from "react";
import { combineLatest, firstValueFrom, interval, of, } from "rxjs";
import { map, take, switchMap } from "rxjs/operators";
import { useObservable, useObsState, useSubject } from "../../hooks/rxjs";
import { createSimpleContext } from "../simple";
import { getSpecTypes } from "@polkadot/types-known";
import { runOnClientSide } from "../../utils/device";
const win = globalThis;
const selectExtension = (name) => {
    const ext = win.injectedWeb3[name];
    delete win.injectedWeb3[name];
    const extensions = { ...win.injectedWeb3 };
    win.injectedWeb3 = { [name]: ext };
    return extensions;
};
const restoreExtensions = (exts) => {
    win.injectedWeb3 = { ...win.injectedWeb3, ...exts };
};
export const getExtensions = () => {
    return Object.keys(window.injectedWeb3 ?? {});
};
export const getVersion = (extName = "polkadot-js") => window.injectedWeb3?.[extName]?.version;
const withPub = (account, ss58, { enc, dec }) => {
    if (!enc || !dec) {
        return;
    }
    let address = account.address;
    let error = false;
    const pubRaw = dec(account.address);
    const pub = `0x${Buffer.from(pubRaw).toString("hex")}`;
    try {
        address = enc(pubRaw, ss58);
    }
    catch (e) {
        // FIXME now suppresses error, TODO handle other account types
        // console.warn(e);
        error = true;
    }
    return { ...account, pub, address, error };
};
const saType = [
    "Bailsman",
    "Borrower",
];
const defaultCompatState = {
    installed: false,
    account: undefined,
    accounts: undefined,
    signer: undefined,
    _debug: false,
};
// todo remove "SUBSTRATE_WALLET" "POLKADOT_SESSION" after deprecate polkadot/connect context
const Context = createSimpleContext("eq-connect", {
    name: "",
    ...defaultCompatState,
    // @ts-ignore
    state$: of({ ...defaultCompatState, subAccounts: undefined }),
    connect: () => Promise.reject(new Error("not implemented")),
    disconnect: () => Promise.reject(new Error("not implemented")),
    updateMetadata: () => Promise.reject(new Error("not implemented")),
    select: () => Promise.reject(new Error("not implemented")),
}, ({ name, useNet, useBase, useSession, useApi, useStillNotifications, }) => {
    const intervalObservable = useMemo(() => interval(1000), []);
    const api = useApi?.();
    const { data$: session$, set: setSession, remove: removeSession, } = useSession();
    const { addNotification, deleteNotification } = useStillNotifications?.() ?? {};
    const { network } = useNet();
    const network$ = useObservable(network);
    const { extensionDApp, utilCrypto } = useBase();
    const extensions$ = useSubject();
    const accounts$ = useSubject();
    const signer$ = useSubject();
    useEffect(() => {
        const sub = signer$.subscribe((signer) => {
            if (signer && api) {
                api.setSigner(signer);
            }
        });
        return () => {
            sub.unsubscribe();
        };
    }, [signer$, api]);
    const versions$ = useSubject();
    const { decodeAddress, encodeAddress } = useMemo(() => ({
        decodeAddress: utilCrypto?.decodeAddress,
        encodeAddress: utilCrypto?.encodeAddress,
    }), [utilCrypto]);
    const { web3AccountsSubscribe, web3Enable, web3Accounts } = useMemo(() => ({
        web3AccountsSubscribe: extensionDApp?.web3AccountsSubscribe,
        web3Enable: extensionDApp?.web3Enable,
        web3Accounts: extensionDApp?.web3Accounts,
    }), [extensionDApp]);
    useEffect(() => {
        let unsub = false;
        let sub;
        sub = intervalObservable.subscribe(() => {
            const extensionNames = getExtensions();
            if (!extensionNames.length) {
                return;
            }
            const versions = extensionNames.reduce((prev, name) => ({
                ...prev,
                [name]: getVersion(name),
            }), {});
            versions$.next(versions);
            unsub = true;
            sub?.unsubscribe();
        });
        return () => {
            if (!unsub) {
                sub?.unsubscribe();
            }
        };
    }, [intervalObservable, versions$]);
    useEffect(() => {
        if (web3AccountsSubscribe) {
            let unsubAccounts;
            let timeout;
            let unsub = false;
            let sub;
            sub = extensions$.subscribe((exts) => {
                if (exts) {
                    timeout = setTimeout(async () => {
                        unsubAccounts = await web3AccountsSubscribe((accs) => accounts$.next(accs));
                    }, 50);
                    sub?.unsubscribe();
                    unsub = true;
                }
            });
            return () => {
                if (!unsub) {
                    sub?.unsubscribe();
                }
                if (timeout) {
                    clearTimeout(timeout);
                }
                unsubAccounts?.();
            };
        }
    }, [accounts$, extensions$, web3AccountsSubscribe]);
    const base64Encode = useMemo(() => utilCrypto?.base64Encode, [utilCrypto?.base64Encode]);
    const checkExtensionsForUpdate = useCallback(async () => {
        if (!api)
            return false;
        const extensions = await firstValueFrom(extensions$);
        if (!extensions || !extensions.length)
            return false;
        extensions.forEach(async (ext) => {
            const current = (await ext.metadata?.get())?.find(({ genesisHash }) => api?._api.genesisHash.eq(genesisHash));
            const updateAvailable = !current ||
                api?._api.runtimeVersion.specVersion.gtn(current.specVersion);
            updateAvailable
                ? addNotification?.({ type: "metadataUpdate" })
                : deleteNotification?.("metadataUpdate");
        });
    }, [addNotification, api, deleteNotification, extensions$]);
    const updateMetadata = useCallback(async (debug) => {
        if (!api || !base64Encode) {
            return;
        }
        const extensions = await firstValueFrom(extensions$);
        if (!extensions || !extensions.length)
            return;
        const chain = await firstValueFrom(api?._api.rpc.system.chain());
        const chainInfo = {
            chain: chain.toString(),
            chainType: "substrate",
            color: "",
            genesisHash: api._api.genesisHash.toHex(),
            icon: "substrate",
            metaCalls: base64Encode(api._api.runtimeMetadata.asCallsOnly.toU8a()),
            specVersion: debug ? 0 : Number(api._api.runtimeVersion.specVersion),
            ss58Format: api._api.registry.chainSS58 || 42,
            tokenDecimals: api._api.registry.chainDecimals[0],
            tokenSymbol: api._api.registry.chainTokens[0],
            types: getSpecTypes(api?._api.registry, chain, api._api.runtimeVersion.specName, api._api.runtimeVersion.specVersion),
        };
        extensions.forEach((e) => e.metadata
            ?.provide(chainInfo)
            .then((isOk) => isOk && checkExtensionsForUpdate()));
    }, [api, base64Encode, extensions$, checkExtensionsForUpdate]);
    runOnClientSide(() => {
        // For debug purpose. Allows to clear extension metada from console
        window.clearMetadata = () => updateMetadata(true);
    });
    useEffect(() => {
        const sub = extensions$.subscribe(() => {
            checkExtensionsForUpdate();
        });
        return () => {
            sub.unsubscribe();
        };
    }, [extensions$, checkExtensionsForUpdate]);
    const metaDataShouldBeUpdated = useCallback(async () => {
        const extensions = await firstValueFrom(extensions$);
        if (!api || !extensions || !extensions.length) {
            deleteNotification?.("metadataUpdate");
            return false;
        }
        const updateAvailable = extensions.some(async (ext) => {
            const current = (await ext.metadata?.get())?.find(({ genesisHash }) => api?._api.genesisHash.eq(genesisHash));
            return (!current ||
                api?._api.runtimeVersion.specVersion.gtn(current.specVersion));
        });
        updateAvailable
            ? addNotification?.({ type: "metadataUpdate" })
            : deleteNotification?.("metadataUpdate");
    }, [api, extensions$, addNotification, deleteNotification]);
    useEffect(() => {
        const sub = extensions$.subscribe(() => {
            metaDataShouldBeUpdated();
        });
        return () => {
            sub.unsubscribe();
        };
    }, [metaDataShouldBeUpdated, extensions$]);
    const connect = useCallback(async (extName) => {
        if (!web3Accounts || !web3Enable)
            return;
        let extensionName;
        if (extName) {
            extensionName = extName;
        }
        else {
            const { connected: connectedExtension } = await firstValueFrom(session$);
            extensionName = connectedExtension;
        }
        if (!extensionName)
            return;
        let cut;
        try {
            cut = selectExtension(extensionName);
            const extensions = await web3Enable(name);
            extensions$.next(extensions);
            const signer = extensions?.[0].signer;
            signer$.next(signer);
            setSession("connected", extensionName);
            globalThis.localStorage?.setItem("SUBSTRATE_WALLET", extensionName);
            const accounts = await web3Accounts();
            accounts$.next(accounts);
            return accounts;
        }
        finally {
            cut && restoreExtensions(cut);
        }
    }, [
        name,
        session$,
        web3Enable,
        web3Accounts,
        extensions$,
        setSession,
        accounts$,
        signer$
    ]);
    const disconnect = useCallback(() => {
        removeSession();
        extensions$.next(undefined);
        accounts$.next(undefined);
        signer$.next(undefined);
        globalThis.localStorage?.removeItem("SUBSTRATE_WALLET");
        globalThis.localStorage?.removeItem("POLKADOT_SESSION");
    }, [accounts$, extensions$, removeSession, signer$]);
    const autoConnect$ = useMemo(() => 
    // only get first value
    session$.pipe(map((s) => s.connected), take(1)), [session$]);
    useEffect(() => {
        let sub;
        sub = autoConnect$.subscribe(async (val) => {
            if (!val)
                return;
            await connect(val);
        });
        return () => {
            sub?.unsubscribe();
        };
    }, [autoConnect$, connect]);
    const select = useCallback(async (accountOrAddressOrIndex) => {
        if (!accountOrAddressOrIndex &&
            typeof accountOrAddressOrIndex !== "number") {
            removeSession();
            globalThis.localStorage?.removeItem("SUBSTRATE_WALLET");
            globalThis.localStorage?.removeItem("POLKADOT_SESSION");
            signer$.next(undefined);
            return;
        }
        let sub;
        sub = combineLatest([accounts$, network$]).subscribe(([accs, net]) => {
            const address = typeof accountOrAddressOrIndex === "string"
                ? accountOrAddressOrIndex
                : typeof accountOrAddressOrIndex === "number"
                    ? accs?.[accountOrAddressOrIndex]?.address
                    : accountOrAddressOrIndex.address;
            const i = accs?.findIndex((acc) => {
                const account = withPub(acc, net?.ss58 ?? 0, {
                    dec: decodeAddress,
                    enc: encodeAddress,
                });
                if (account?.error) {
                    return false;
                }
                return account?.address === address;
            }) ?? -1;
            if (i >= 0) {
                setSession("accountIndex", i);
                address &&
                    globalThis.localStorage?.setItem("POLKADOT_SESSION", address);
            }
            sub?.unsubscribe();
        });
    }, [
        accounts$,
        network$,
        setSession,
        removeSession,
        signer$,
        encodeAddress,
        decodeAddress,
    ]);
    const state$ = useMemo(() => combineLatest([
        combineLatest([network$, session$, versions$, accounts$]).pipe(map(([network, session, versions, _accounts]) => {
            const installed = !!versions;
            if (!installed) {
                return {
                    installed,
                    account: undefined,
                    accounts: undefined,
                    _debug: false,
                };
            }
            const accounts = _accounts
                ?.map((acc) => withPub(acc, network?.ss58 ?? 0, {
                dec: decodeAddress,
                enc: encodeAddress,
            }))
                .filter((acc) => !acc?.error);
            const account = accounts?.[session.accountIndex] ?? accounts?.[0];
            if (session.accountDebugOverride && account) {
                const pub = decodeAddress?.(session.accountDebugOverride);
                const address = pub && encodeAddress?.(pub, network?.ss58);
                if (pub && address) {
                    account.pub = `0x${Buffer.from(pub).toString("hex")}`;
                    account.address = address;
                }
            }
            return {
                installed,
                account,
                accounts,
                _debug: !!session.accountDebugOverride,
            };
        }), switchMap(({ account, ...rest }) => {
            if (!account || !api)
                return combineLatest([of({ account, ...rest }), of(undefined)]);
            const subAccs$ = api.query.getAddress
                .multi(saType.map((t) => [account.address, t]))
                .pipe(map((res) => {
                return saType.reduce((total, t, i) => {
                    const accountId = res[i].unwrapOr(undefined)?.toString();
                    return { ...total, [t]: accountId };
                }, {
                    Bailsman: undefined,
                    Borrower: undefined,
                });
            }));
            return combineLatest([of({ account, ...rest }), subAccs$]);
        }), map(([state, subAccounts]) => {
            return { ...state, subAccounts };
        })),
        signer$,
    ]).pipe(map(([state, signer]) => ({ ...state, signer }))), [
        network$,
        session$,
        versions$,
        accounts$,
        signer$,
        encodeAddress,
        decodeAddress,
        api,
    ]);
    /** @deprecated */
    const state = useObsState(state$);
    return useMemo(() => {
        const compatState = state ?? defaultCompatState;
        return {
            name,
            /** @deprecated */
            installed: compatState.installed,
            /** @deprecated */
            account: compatState.account,
            /** @deprecated */
            accounts: compatState.accounts,
            /** @deprecated */
            signer: compatState.signer,
            /** @deprecated */
            _debug: compatState._debug,
            state$,
            connect,
            disconnect,
            updateMetadata,
            select,
        };
    }, [state, name, state$, connect, disconnect, updateMetadata, select]);
});
export const ConnectProvider = Context.Provider;
export const useConnect = Context.useContext;
//# sourceMappingURL=connect.js.map