import { Subject } from "rxjs";
import flatMap from "lodash/flatMap";
import semver from "semver";
import { getDeviceModel } from "@ledgerhq/devices";
import { SkipReason } from "./types";
import { findCryptoCurrency, findCryptoCurrencyById, isCurrencySupported } from "../currencies";
import { NoSuchAppOnProvider } from "../errors";
import { getEnv } from "@ledgerhq/live-env";
import { LatestFirmwareVersionRequired } from "@ledgerhq/errors";
export const initState = ({ deviceModelId, appsListNames, installed, appByName, ...listAppsResult }, appsToRestore) => {
    let state = {
        ...listAppsResult,
        installed,
        appByName,
        apps: appsListNames.map(name => appByName[name]).filter(Boolean),
        deviceModel: getDeviceModel(deviceModelId),
        recentlyInstalledApps: [],
        installQueue: [],
        uninstallQueue: [],
        updateAllQueue: [],
        currentProgressSubject: new Subject(),
        currentError: null,
        currentAppOp: null,
        skippedAppOps: [],
        skipAppDataBackup: false,
    };
    if (appsToRestore) {
        state = appsToRestore
            .filter(name => appByName[name] && !installed.some(a => a.name === name))
            .map(name => ({
            type: "install",
            name,
        }))
            .reduce(reducer, state);
    }
    return state;
};
// ^TODO move this to legacyDependencies.js
// we should have dependency as part of the data!
const reorderInstallQueue = (appByName, apps) => {
    const list = [];
    apps.forEach(app => {
        if (list.includes(app))
            return;
        if (app in appByName) {
            const deps = appByName[app].dependencies;
            deps.forEach(dep => {
                if (apps.includes(dep) && !list.includes(dep)) {
                    list.push(dep);
                }
            });
        }
        list.push(app);
    });
    return list;
};
const reorderUninstallQueue = (appByName, apps) => reorderInstallQueue(appByName, apps.slice(0).reverse()).reverse();
const findDependents = (appByName, name) => {
    const all = [];
    for (const k in appByName) {
        const app = appByName[k];
        if (app.dependencies.includes(name)) {
            all.push(app.name);
        }
    }
    return all;
};
export const reducer = (state, action) => {
    switch (action.type) {
        case "reset":
            return action.initialState;
        case "onRunnerEvent": {
            // an app operation was correctly prefered. update state accordingly
            const { event } = action;
            const { appOp } = event;
            if (event.type === "runStart") {
                return {
                    ...state,
                    currentAppOp: appOp,
                    currentProgressSubject: new Subject(),
                };
            }
            else if (event.type === "runSuccess") {
                if (state.currentProgressSubject) {
                    state.currentProgressSubject.complete();
                }
                let nextState;
                if (appOp.type === "install") {
                    const app = state.apps.find(a => a.name === appOp.name);
                    nextState = {
                        ...state,
                        currentAppOp: null,
                        currentProgressSubject: null,
                        currentError: null,
                        recentlyInstalledApps: state.recentlyInstalledApps.concat(appOp.name),
                        // append the app to known installed apps
                        installed: state.installed
                            .filter(o => o.name !== appOp.name)
                            .concat({
                            name: appOp.name,
                            updated: true,
                            hash: app ? app.hash : "",
                            blocks: app && app.bytes ? Math.ceil(app.bytes / getBlockSize(state)) : 0,
                            version: app ? app.version : "",
                            availableVersion: app ? app.version : "",
                        }),
                        // remove the install action
                        installQueue: state.installQueue.filter(name => appOp.name !== name),
                    };
                }
                else {
                    nextState = {
                        ...state,
                        currentAppOp: null,
                        currentProgressSubject: null,
                        currentError: null,
                        // remove apps to known installed apps
                        installed: state.installed.filter(i => appOp.name !== i.name),
                        // remove the uninstall action
                        uninstallQueue: state.uninstallQueue.filter(name => appOp.name !== name),
                    };
                }
                if (nextState.installQueue.length + nextState.uninstallQueue.length === 0) {
                    nextState.updateAllQueue = [];
                }
                return nextState;
            }
            else if (event.type === "runError") {
                // TO BE CONTINUED LL-2138
                // to handle recovering from error. however we are not correctly using it at the moment.
                /*
                  const error = event.error;
                  if (error instanceof ManagerDeviceLockedError) {
                    return {
                      ...state,
                      currentError: {
                        appOp: appOp,
                        error: event.error
                      }
                    };
                  }
                  */
                if (state.currentProgressSubject) {
                    state.currentProgressSubject.complete();
                }
                // any other error stops everything
                return {
                    ...state,
                    uninstallQueue: [],
                    installQueue: [],
                    updateAllQueue: [],
                    currentAppOp: null,
                    currentProgressSubject: null,
                    currentError: {
                        appOp: appOp,
                        error: event.error,
                    },
                };
            }
            else if (event.type === "runProgress") {
                // we just emit on the subject
                if (state.currentProgressSubject) {
                    state.currentProgressSubject.next(event.progress);
                }
                return state; // identity state will not re-render the UI
            }
            return state;
        }
        case "recover":
            return { ...state, currentError: null };
        case "wiped":
            // Reconciliate an already achieved wipe via single uninstall apdu
            return {
                ...state,
                installed: [],
                skipAppDataBackup: false,
            };
        case "wipe":
            return {
                ...state,
                currentError: null,
                installQueue: [],
                uninstallQueue: reorderUninstallQueue(state.appByName, state.installed.map(({ name }) => name)),
                skipAppDataBackup: true,
            };
        case "updateAll": {
            let installList = state.installQueue.slice(0);
            let uninstallList = state.uninstallQueue.slice(0);
            state.installed
                .filter(({ updated, name }) => (getEnv("MOCK_APP_UPDATE") || !updated) && state.appByName[name])
                .forEach(app => {
                const dependents = state.installed
                    .filter(a => {
                    const depApp = state.appByName[a.name];
                    return depApp && depApp.dependencies.includes(app.name);
                })
                    .map(a => a.name);
                uninstallList = uninstallList.concat([app.name, ...dependents]);
                installList = installList.concat([app.name, ...dependents]);
            });
            const installQueue = reorderInstallQueue(state.appByName, installList);
            const uninstallQueue = reorderUninstallQueue(state.appByName, uninstallList);
            /** since install queue === uninstall queue in this case we can map the update queue to either one */
            const updateAllQueue = installQueue;
            return {
                ...state,
                currentError: null,
                installQueue,
                uninstallQueue,
                updateAllQueue,
            };
        }
        case "install": {
            const { name, allowPartialDependencies } = action;
            if (state.installQueue.includes(name)) {
                // already queued for install
                return state;
            }
            const existing = state.installed.find(app => app.name === name);
            const skippedAppOps = [...state.skippedAppOps];
            if (existing && existing.updated && state.installedAvailable) {
                // already installed and up to date
                skippedAppOps.push({
                    reason: SkipReason.AppAlreadyInstalled,
                    appOp: action,
                });
                return { ...state, skippedAppOps };
            }
            const appToInstall = state.appByName[name];
            // The target application was not found in the specified provider BUT
            // we can update the firmware instead, and perhaps in the new FW it is available.
            if (!appToInstall &&
                state.firmware?.updateAvailable?.final?.version &&
                semver.lt(state.deviceInfo.version, state.firmware?.updateAvailable?.final?.version || "")) {
                throw new LatestFirmwareVersionRequired("LatestFirmwareVersionRequired", {
                    current: state.deviceInfo.version,
                    latest: state.firmware?.updateAvailable?.final?.version,
                });
            }
            // The target application was not found in the specified provider.
            if (!appToInstall) {
                if (allowPartialDependencies) {
                    // Some flows are resillient to missing operations.
                    skippedAppOps.push({
                        reason: SkipReason.NoSuchAppOnProvider,
                        appOp: action,
                    });
                    return { ...state, skippedAppOps };
                }
                else {
                    // Some flows are not, and we want to stop with an error.
                    throw new NoSuchAppOnProvider("", { appName: name });
                }
            }
            const dependencies = appToInstall.dependencies;
            const dependentsOfDep = flatMap(dependencies, dep => findDependents(state.appByName, dep));
            const depsInstalledOutdated = state.installed.filter(a => dependencies.includes(a.name) && !a.updated);
            let installList = state.installQueue;
            // installing an app will remove if planned for uninstalling
            let uninstallList = state.uninstallQueue.filter(u => name !== u && !dependencies.includes(u));
            if (state.uninstallQueue.length !== uninstallList.length) {
                // app was asked for uninstall so it means we need to just cancel.
                // TODO cover this in tests...
            }
            else {
                // if app is already installed but outdated, we'll need to update related deps
                if ((existing && !existing.updated) || depsInstalledOutdated.length) {
                    // if app has installed direct dependent apps, we'll need to update them too
                    const directDependents = findDependents(state.appByName, name).filter(d => state.installed.some(a => a.name === d));
                    const outdated = state.installed
                        .filter(a => !a.updated &&
                        [name, ...dependencies, ...directDependents, ...dependentsOfDep].includes(a.name))
                        .map(a => a.name);
                    uninstallList = uninstallList.concat(outdated);
                    installList = installList.concat(outdated);
                }
                installList = installList.concat([
                    ...dependencies.filter(d => !state.installed.some(a => a.name === d)),
                    name,
                ]);
            }
            const installQueue = reorderInstallQueue(state.appByName, installList);
            const uninstallQueue = reorderUninstallQueue(state.appByName, uninstallList);
            return { ...state, currentError: null, installQueue, uninstallQueue };
        }
        case "setCustomImage": {
            const { lastSeenCustomImage } = action;
            const { size } = lastSeenCustomImage;
            return {
                ...state,
                customImageBlocks: Math.ceil(size / getBlockSize(state)),
            };
        }
        case "uninstall": {
            const { name } = action;
            if (state.uninstallQueue.includes(name)) {
                // already queued
                return state;
            }
            // uninstalling an app will remove from installQueue as well as direct deps
            const installQueue = state.installQueue.filter(u => name !== u);
            let uninstallQueue = state.uninstallQueue;
            if (state.installed.some(a => a.name === name || a.name === "") ||
                action.force || // if installed unavailable and it was not a cancellation
                // TODO cover this in tests...
                (!state.installedAvailable && !state.installQueue.includes(name))) {
                uninstallQueue = reorderUninstallQueue(state.appByName, uninstallQueue.concat([
                    ...findDependents(state.appByName, name).filter(d => state.installed.some(a => a.name === d)),
                    name,
                ]));
            }
            return {
                ...state,
                currentError: null,
                installQueue,
                uninstallQueue,
                skipAppDataBackup: true,
            };
        }
    }
};
const defaultConfig = {
    warnMemoryRatio: 0.1,
    sortApps: false,
};
// calculate all size information useful for display
export const distribute = (state, config) => {
    const { customImageBlocks } = state;
    const { warnMemoryRatio, sortApps } = { ...defaultConfig, ...config };
    const blockSize = getBlockSize(state);
    const totalBytes = state.deviceModel.memorySize;
    const totalBlocks = Math.floor(totalBytes / blockSize);
    const osBytes = (state.firmware && state.firmware.bytes) || 0;
    const osBlocks = Math.ceil(osBytes / blockSize);
    const languagePackBytes = state.installedLanguagePack?.bytes || 0;
    const languagePackBlocks = Math.ceil(languagePackBytes / blockSize);
    const appsSpaceBlocks = totalBlocks - osBlocks - customImageBlocks - languagePackBlocks;
    const appsSpaceBytes = appsSpaceBlocks * blockSize;
    let totalAppsBlocks = 0;
    const apps = state.installed.map(app => {
        const { name, blocks } = app;
        totalAppsBlocks += blocks;
        const currency = 
        // try to find the "official" currency when possible (2 currencies can have the same manager app and ticker)
        findCryptoCurrency(c => c.name === name) ||
            // Else take the first one with that manager app
            findCryptoCurrency(c => c.managerAppName === name);
        return {
            currency,
            name,
            blocks,
            bytes: blocks * blockSize,
        };
    });
    if (sortApps) {
        apps.sort((a, b) => b.blocks - a.blocks);
    }
    const totalAppsBytes = totalAppsBlocks * blockSize;
    const freeSpaceBlocks = appsSpaceBlocks - totalAppsBlocks;
    const freeSpaceBytes = freeSpaceBlocks * blockSize;
    const shouldWarnMemory = freeSpaceBlocks / appsSpaceBlocks < warnMemoryRatio;
    return {
        totalBlocks,
        totalBytes,
        osBlocks,
        osBytes,
        apps,
        appsSpaceBlocks,
        appsSpaceBytes,
        totalAppsBlocks,
        customImageBlocks,
        languagePackBlocks,
        totalAppsBytes,
        freeSpaceBlocks,
        freeSpaceBytes,
        shouldWarnMemory,
    };
};
export function getBlockSize(state) {
    return state.deviceModel.getBlockSize(state.deviceInfo.version);
}
// tells if the state is "incomplete" to implement the Manager v2 feature
// this happens when some apps are unrecognized
export const isIncompleteState = (state) => state.installed.some(a => !a.name);
// calculate if a given state (typically a predicted one) is out of memory (meaning impossible to reach with a device)
export const isOutOfMemoryState = (state) => {
    const { totalAppsBlocks, appsSpaceBlocks } = distribute(state);
    return totalAppsBlocks > appsSpaceBlocks;
};
export const isLiveSupportedApp = (app) => {
    const currency = app?.currencyId ? findCryptoCurrencyById(app.currencyId) : null;
    return currency ? isCurrencySupported(currency) : false;
};
export const updateAllProgress = (state) => {
    const total = state.updateAllQueue.length;
    /** each uninstall and install comes in a pair and have a weight of 0.5 in the progress */
    const current = (state.uninstallQueue.length + state.installQueue.length) / 2;
    if (total === 0 || current === 0)
        return 1;
    return Math.max(0, Math.min((total - current) / total, 1));
};
// a series of operation to perform on the device for current state
export const getActionPlan = (state) => state.uninstallQueue
    .map(name => ({
    type: "uninstall",
    name,
}))
    .concat(state.installQueue.map(name => ({
    type: "install",
    name,
})));
// get next operation to perform
export const getNextAppOp = (state) => {
    if (state.uninstallQueue.length) {
        return {
            type: "uninstall",
            name: state.uninstallQueue[0],
        };
    }
    else if (state.installQueue.length) {
        return {
            type: "install",
            name: state.installQueue[0],
        };
    }
};
// resolve the State to predict when all queued ops are done
export const predictOptimisticState = (state) => {
    const s = { ...state, currentProgressSubject: null };
    return getActionPlan(s)
        .map(appOp => ({
        type: "onRunnerEvent",
        event: {
            type: "runSuccess",
            appOp,
        },
    }))
        .reduce(reducer, s);
};
//# sourceMappingURL=logic.js.map