import { log } from "@ledgerhq/logs";
import shuffle from "lodash/shuffle";
import priorityQueue from "async/priorityQueue";
import { concat, from } from "rxjs";
import { ignoreElements } from "rxjs/operators";
import React, { useEffect, useCallback, useState, useRef, useMemo } from "react";
import { getVotesCount, isUpToDateAccount } from "../../account";
import { getAccountBridge } from "..";
import { getAccountCurrency } from "../../account";
import { getEnv } from "@ledgerhq/live-env";
import { BridgeSyncContext, BridgeSyncStateContext } from "./context";
import { createSyncSessionManager } from "../syncSessionManager";
export const BridgeSync = ({ children, accounts, updateAccountWithUpdater, recoverError, trackAnalytics, prepareCurrency, hydrateCurrency, blacklistedTokenIds, }) => {
    useHydrate({
        accounts,
        hydrateCurrency,
    });
    const sessionManager = useRef(createSyncSessionManager(trackAnalytics)).current;
    const [syncQueue, syncState] = useSyncQueue({
        accounts,
        prepareCurrency,
        recoverError,
        trackAnalytics,
        updateAccountWithUpdater,
        blacklistedTokenIds,
        sessionManager,
    });
    const sync = useSync({
        syncQueue,
        accounts,
        sessionManager,
    });
    useSyncBackground({
        sync,
    });
    useSyncContinouslyPendingOperations({
        sync,
        accounts,
    });
    return (React.createElement(BridgeSyncStateContext.Provider, { value: syncState },
        React.createElement(BridgeSyncContext.Provider, { value: sync }, children)));
};
// utility internal hooks for <BridgeSync>
// useHydrate: bridge.hydrate once for each currency
function useHydrate({ accounts, hydrateCurrency }) {
    const hydratedCurrencies = useRef({});
    useEffect(() => {
        const hydrated = hydratedCurrencies.current;
        for (const account of accounts) {
            const { currency } = account;
            if (!hydrated[currency.id]) {
                hydrated[currency.id] = true;
                hydrateCurrency(currency);
            }
        }
    }, [accounts, hydrateCurrency]);
}
let lastTimeAnalyticsTrackPerAccountId = {};
let nothingState = {
    pending: false,
    error: null,
};
// Only used for tests
export function resetStates() {
    lastTimeAnalyticsTrackPerAccountId = {};
    nothingState = {
        pending: false,
        error: null,
    };
}
// useHydrate: returns a sync queue and bridge sync state
function useSyncQueue({ accounts, prepareCurrency, recoverError, trackAnalytics, updateAccountWithUpdater, blacklistedTokenIds, sessionManager, }) {
    const [bridgeSyncState, setBridgeSyncState] = useState({});
    const setAccountSyncState = useCallback((accountId, s) => {
        setBridgeSyncState(state => ({ ...state, [accountId]: s }));
    }, []);
    const synchronize = useCallback(({ accountId, reason }, next) => {
        const state = bridgeSyncState[accountId] || nothingState;
        if (state.pending) {
            next();
            return;
        }
        const account = accounts.find(a => a.id === accountId);
        if (!account) {
            next();
            return;
        }
        // FIXME if we want to stop syncs for specific currency (e.g. api down) we would do it here
        try {
            const bridge = getAccountBridge(account);
            setAccountSyncState(accountId, {
                pending: true,
                error: null,
            });
            const startSyncTime = Date.now();
            const trackedRecently = lastTimeAnalyticsTrackPerAccountId[accountId] &&
                startSyncTime - lastTimeAnalyticsTrackPerAccountId[accountId] < 90 * 1000;
            if (!trackedRecently) {
                lastTimeAnalyticsTrackPerAccountId[accountId] = startSyncTime;
            }
            const trackSyncSuccessEnd = () => {
                if (trackedRecently)
                    return;
                const account = accounts.find(a => a.id === accountId);
                if (!account)
                    return;
                const subAccounts = account.subAccounts || [];
                if (reason === "background") {
                    // don't track background syncs
                    return;
                }
                trackAnalytics("SyncSuccess", {
                    duration: (Date.now() - startSyncTime) / 1000,
                    currencyName: account.currency.name,
                    derivationMode: account.derivationMode,
                    freshAddressPath: account.freshAddressPath,
                    operationsLength: account.operationsCount,
                    accountsCountForCurrency: accounts.filter(a => a.currency === account.currency).length,
                    tokensLength: subAccounts.length,
                    tokens: subAccounts.map(a => ({
                        tokenId: a.type === "TokenAccount" ? getAccountCurrency(a).id : account.currency.name,
                        tokenTicker: getAccountCurrency(a).ticker,
                        operationsLength: a.operationsCount,
                        parentCurrencyName: account.currency.name,
                        parentDerivationMode: account.derivationMode,
                        votesCount: getVotesCount(a, account),
                    })),
                    votesCount: getVotesCount(account),
                    reason,
                });
            };
            const syncConfig = {
                paginationConfig: {},
                blacklistedTokenIds,
            };
            concat(from(prepareCurrency(account.currency)).pipe(ignoreElements()), bridge.sync(account, syncConfig)).subscribe({
                next: accountUpdater => {
                    updateAccountWithUpdater(accountId, accountUpdater);
                },
                complete: () => {
                    trackSyncSuccessEnd();
                    setAccountSyncState(accountId, {
                        pending: false,
                        error: null,
                    });
                    sessionManager.onAccountSyncDone(accountId, accounts);
                    next();
                },
                error: (raw) => {
                    const error = recoverError(raw);
                    if (!error) {
                        // This error is normal because the thread was recently killed. we silent it for the user.
                        setAccountSyncState(accountId, {
                            pending: false,
                            error: null,
                        });
                        sessionManager.onAccountSyncDone(accountId, accounts);
                        next();
                        return;
                    }
                    setAccountSyncState(accountId, {
                        pending: false,
                        error,
                    });
                    sessionManager.onAccountSyncDone(accountId, accounts, true);
                    next();
                },
            });
        }
        catch (error) {
            setAccountSyncState(accountId, {
                pending: false,
                error,
            });
            sessionManager.onAccountSyncDone(accountId, accounts, true);
            next();
        }
    }, [
        accounts,
        bridgeSyncState,
        prepareCurrency,
        recoverError,
        setAccountSyncState,
        trackAnalytics,
        updateAccountWithUpdater,
        blacklistedTokenIds,
        sessionManager,
    ]);
    const synchronizeRef = useRef(synchronize);
    useEffect(() => {
        synchronizeRef.current = synchronize;
    }, [synchronize]);
    const [syncQueue] = useState(() => priorityQueue((job, next) => synchronizeRef.current(job, next), getEnv("SYNC_MAX_CONCURRENT")));
    return [syncQueue, bridgeSyncState];
}
// useSync: returns a sync function with the syncQueue
function useSync({ syncQueue, accounts, sessionManager }) {
    const skipUnderPriority = useRef(-1);
    const sync = useMemo(() => {
        const schedule = (ids, priority, reason) => {
            if (priority < skipUnderPriority.current)
                return;
            // by convention we remove concurrent tasks with same priority
            // FIXME this is somehow a hack. ideally we should just dedup the account ids in the pending queue...
            syncQueue.remove(o => priority === o.priority);
            // start a global session only if initial + all accounts
            if (reason === "initial" && ids.length === accounts.length) {
                sessionManager.start(ids, reason);
            }
            log("bridge", "schedule " + ids.join(", "));
            syncQueue.push(ids.map(accountId => ({
                accountId,
                reason,
            })), -priority);
        };
        // don't always sync in the same order to avoid potential "account never reached"
        const shuffledAccountIds = () => shuffle(accounts.map(a => a.id));
        const handlers = {
            BACKGROUND_TICK: ({ reason }) => {
                if (syncQueue.idle()) {
                    schedule(shuffledAccountIds(), -1, reason);
                }
            },
            SET_SKIP_UNDER_PRIORITY: ({ priority }) => {
                if (priority === skipUnderPriority.current)
                    return;
                skipUnderPriority.current = priority;
                syncQueue.remove(({ priority }) => priority < skipUnderPriority.current);
                if (priority === -1 && !accounts.every(isUpToDateAccount)) {
                    // going back to -1 priority => retriggering a background sync if it is "Paused"
                    schedule(shuffledAccountIds(), -1, "outdated");
                }
            },
            SYNC_ALL_ACCOUNTS: ({ priority, reason }) => {
                schedule(shuffledAccountIds(), priority, reason);
            },
            SYNC_ONE_ACCOUNT: ({ accountId, priority, reason, }) => {
                schedule([accountId], priority, reason);
            },
            SYNC_SOME_ACCOUNTS: ({ accountIds, priority, reason, }) => {
                schedule(accountIds, priority, reason);
            },
        };
        return (action) => {
            const handler = handlers[action.type];
            if (handler) {
                log("bridge", `action ${action.type}`, {
                    action,
                    type: "syncQueue",
                });
                handler(action);
            }
            else {
                log("warn", "BridgeSyncContext unsupported action", {
                    action,
                    type: "syncQueue",
                });
            }
        };
    }, [accounts, syncQueue, sessionManager]);
    const ref = useRef(sync);
    useEffect(() => {
        ref.current = sync;
    }, [sync]);
    const syncFn = useCallback(action => ref.current(action), [ref]);
    return syncFn;
}
// useSyncBackground: continuously synchronize accounts in background
function useSyncBackground({ sync }) {
    useEffect(() => {
        let syncTimeout;
        const syncLoop = async (reason) => {
            sync({
                type: "BACKGROUND_TICK",
                reason,
            });
            syncTimeout = setTimeout(syncLoop, getEnv("SYNC_ALL_INTERVAL"), "background");
        };
        syncTimeout = setTimeout(syncLoop, getEnv("SYNC_BOOT_DELAY"), "initial");
        return () => clearTimeout(syncTimeout);
    }, [sync]);
}
// useSyncContinouslyPendingOperations: continously sync accounts with pending operations
function useSyncContinouslyPendingOperations({ sync, accounts }) {
    const ids = useMemo(() => accounts.filter(a => a.pendingOperations.length > 0).map(a => a.id), [accounts]);
    const refIds = useRef(ids);
    useEffect(() => {
        refIds.current = ids;
    }, [ids]);
    useEffect(() => {
        let timeout;
        const update = () => {
            timeout = setTimeout(update, getEnv("SYNC_PENDING_INTERVAL"));
            if (!refIds.current.length)
                return;
            sync({
                type: "SYNC_SOME_ACCOUNTS",
                accountIds: refIds.current,
                priority: 20,
                reason: "pending-operations",
            });
        };
        timeout = setTimeout(update, getEnv("SYNC_PENDING_INTERVAL"));
        return () => clearTimeout(timeout);
    }, [sync]);
}
//# sourceMappingURL=BridgeSync.js.map