import { AcquisitionManager as Sdk } from "code-push/script/acquisition-sdk";
import { Alert } from "./AlertAdapter";
import requestFetchAdapter from "./request-fetch-adapter";
import { AppState, Platform } from "react-native";
import RestartManager from "./RestartManager";
import log from "./logging";
import hoistStatics from "hoist-non-react-statics";

let NativeCodePush = require("react-native").NativeModules.CodePush;
const PackageMixins = require("./package-mixins")(NativeCodePush);

async function setServerUrl(url) {
    if (!url) return;
    return await NativeCodePush.setServerUrl(url);
}

async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {
    /*
     * Before we ask the server if an update exists, we
     * need to retrieve three pieces of information from the
     * native side: deployment key, app version (e.g. 1.0.1)
     * and the hash of the currently running update (if there is one).
     * This allows the client to only receive updates which are targetted
     * for their specific deployment and version and which are actually
     * different from the CodePush update they have already installed.
     */
    const nativeConfig = await getConfiguration();
    /*
     * If a deployment key was explicitly provided,
     * then let's override the one we retrieved
     * from the native-side of the app. This allows
     * dynamically "redirecting" end-users at different
     * deployments (e.g. an early access deployment for insiders).
     */
    const config = deploymentKey ? { ...nativeConfig, ...{ deploymentKey } } : nativeConfig;
    const sdk = getPromisifiedSdk(requestFetchAdapter, config);

    // Use dynamically overridden getCurrentPackage() during tests.
    const localPackage = await module.exports.getCurrentPackage();

    /*
     * If the app has a previously installed update, and that update
     * was targetted at the same app version that is currently running,
     * then we want to use its package hash to determine whether a new
     * release has been made on the server. Otherwise, we only need
     * to send the app version to the server, since we are interested
     * in any updates for current app store version, regardless of hash.
     */
    let queryPackage;
    if (localPackage) {
        queryPackage = localPackage;
    } else {
        queryPackage = { appVersion: config.appVersion };
        if (Platform.OS === "ios" && config.packageHash) {
            queryPackage.packageHash = config.packageHash;
        }
    }

    const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);

    /*
     * There are four cases where checkForUpdate will resolve to null:
     * ----------------------------------------------------------------
     * 1) The server said there isn't an update. This is the most common case.
     * 2) The server said there is an update but it requires a newer binary version.
     *    This would occur when end-users are running an older app store version than
     *    is available, and CodePush is making sure they don't get an update that
     *    potentially wouldn't be compatible with what they are running.
     * 3) The server said there is an update, but the update's hash is the same as
     *    the currently running update. This should _never_ happen, unless there is a
     *    bug in the server, but we're adding this check just to double-check that the
     *    client app is resilient to a potential issue with the update check.
     * 4) The server said there is an update, but the update's hash is the same as that
     *    of the binary's currently running version. This should only happen in Android -
     *    unlike iOS, we don't attach the binary's hash to the updateCheck request
     *    because we want to avoid having to install diff updates against the binary's
     *    version, which we can't do yet on Android.
     */
    if (
        !update ||
        update.updateAppVersion ||
        (localPackage && update.packageHash === localPackage.packageHash) ||
        ((!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash)
    ) {
        if (update && update.updateAppVersion) {
            log("An update is available but it is not targeting the binary version of your app.");
            if (handleBinaryVersionMismatchCallback && typeof handleBinaryVersionMismatchCallback === "function") {
                handleBinaryVersionMismatchCallback(update);
            }
        }

        return null;
    } else {
        const remotePackage = { ...update, ...PackageMixins.remote(sdk.reportStatusDownload) };
        remotePackage.failedInstall = await NativeCodePush.isFailedUpdate(remotePackage.packageHash);
        remotePackage.deploymentKey = deploymentKey || nativeConfig.deploymentKey;
        return remotePackage;
    }
}

const getConfiguration = (() => {
    let config;
    return async function getConfiguration() {
        if (config) {
            return config;
        } else if (testConfig) {
            return testConfig;
        } else {
            config = await NativeCodePush.getConfiguration();
            return config;
        }
    };
})();

async function getCurrentPackage() {
    return await getUpdateMetadata(CodePush.UpdateState.LATEST);
}

async function getUpdateMetadata(updateState) {
    let updateMetadata = await NativeCodePush.getUpdateMetadata(updateState || CodePush.UpdateState.RUNNING);
    if (updateMetadata) {
        updateMetadata = { ...PackageMixins.local, ...updateMetadata };
        updateMetadata.failedInstall = await NativeCodePush.isFailedUpdate(updateMetadata.packageHash);
        updateMetadata.isFirstRun = await NativeCodePush.isFirstRun(updateMetadata.packageHash);
    }
    return updateMetadata;
}

function getPromisifiedSdk(requestFetchAdapter, config) {
    // Use dynamically overridden AcquisitionSdk during tests.
    const sdk = new module.exports.AcquisitionSdk(requestFetchAdapter, config);
    sdk.queryUpdateWithCurrentPackage = queryPackage => {
        return new Promise((resolve, reject) => {
            module.exports.AcquisitionSdk.prototype.queryUpdateWithCurrentPackage.call(sdk, queryPackage, (err, update) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(update);
                }
            });
        });
    };

    sdk.reportStatusDeploy = (deployedPackage, status, previousLabelOrAppVersion, previousDeploymentKey) => {
        return new Promise((resolve, reject) => {
            module.exports.AcquisitionSdk.prototype.reportStatusDeploy.call(
                sdk,
                deployedPackage,
                status,
                previousLabelOrAppVersion,
                previousDeploymentKey,
                err => {
                    if (err) {
                        reject(err);
                    } else {
                        resolve();
                    }
                }
            );
        });
    };

    sdk.reportStatusDownload = downloadedPackage => {
        return new Promise((resolve, reject) => {
            module.exports.AcquisitionSdk.prototype.reportStatusDownload.call(sdk, downloadedPackage, err => {
                if (err) {
                    reject(err);
                } else {
                    resolve();
                }
            });
        });
    };

    return sdk;
}

// This ensures that notifyApplicationReadyInternal is only called once
// in the lifetime of this module instance.
const notifyApplicationReady = (() => {
    let notifyApplicationReadyPromise;
    return () => {
        if (!notifyApplicationReadyPromise) {
            notifyApplicationReadyPromise = notifyApplicationReadyInternal();
        }

        return notifyApplicationReadyPromise;
    };
})();

async function notifyApplicationReadyInternal() {
    await NativeCodePush.notifyApplicationReady();
    const statusReport = await NativeCodePush.getNewStatusReport();
    statusReport && tryReportStatus(statusReport); // Don't wait for this to complete.

    return statusReport;
}

async function tryReportStatus(statusReport, resumeListener) {
    const config = await getConfiguration();
    const previousLabelOrAppVersion = statusReport.previousLabelOrAppVersion;
    const previousDeploymentKey = statusReport.previousDeploymentKey || config.deploymentKey;
    try {
        if (statusReport.appVersion) {
            log(`Reporting binary update (${statusReport.appVersion})`);

            const sdk = getPromisifiedSdk(requestFetchAdapter, config);
            await sdk.reportStatusDeploy(/* deployedPackage */ null, /* status */ null, previousLabelOrAppVersion, previousDeploymentKey);
        } else {
            const label = statusReport.package.label;
            if (statusReport.status === "DeploymentSucceeded") {
                log(`Reporting CodePush update success (${label})`);
            } else {
                log(`Reporting CodePush update rollback (${label})`);
                await NativeCodePush.setLatestRollbackInfo(statusReport.package.packageHash);
            }

            config.deploymentKey = statusReport.package.deploymentKey;
            const sdk = getPromisifiedSdk(requestFetchAdapter, config);
            await sdk.reportStatusDeploy(statusReport.package, statusReport.status, previousLabelOrAppVersion, previousDeploymentKey);
        }

        NativeCodePush.recordStatusReported(statusReport);
        resumeListener && AppState.removeEventListener("change", resumeListener);
    } catch (e) {
        log(`Report status failed: ${JSON.stringify(statusReport)}`);
        NativeCodePush.saveStatusReportForRetry(statusReport);
        // Try again when the app resumes
        if (!resumeListener) {
            resumeListener = async newState => {
                if (newState !== "active") return;
                const refreshedStatusReport = await NativeCodePush.getNewStatusReport();
                if (refreshedStatusReport) {
                    tryReportStatus(refreshedStatusReport, resumeListener);
                } else {
                    AppState.removeEventListener("change", resumeListener);
                }
            };
            AppState.addEventListener("change", resumeListener);
        }
    }
}

async function shouldUpdateBeIgnored(remotePackage, syncOptions) {
    let { rollbackRetryOptions } = syncOptions;

    const isFailedPackage = remotePackage && remotePackage.failedInstall;
    if (!isFailedPackage || !syncOptions.ignoreFailedUpdates) {
        return false;
    }

    if (!rollbackRetryOptions) {
        return true;
    }

    if (typeof rollbackRetryOptions !== "object") {
        rollbackRetryOptions = CodePush.DEFAULT_ROLLBACK_RETRY_OPTIONS;
    } else {
        rollbackRetryOptions = { ...CodePush.DEFAULT_ROLLBACK_RETRY_OPTIONS, ...rollbackRetryOptions };
    }

    if (!validateRollbackRetryOptions(rollbackRetryOptions)) {
        return true;
    }

    const latestRollbackInfo = await NativeCodePush.getLatestRollbackInfo();
    if (!validateLatestRollbackInfo(latestRollbackInfo, remotePackage.packageHash)) {
        log("The latest rollback info is not valid.");
        return true;
    }

    const { delayInHours, maxRetryAttempts } = rollbackRetryOptions;
    const hoursSinceLatestRollback = (Date.now() - latestRollbackInfo.time) / (1000 * 60 * 60);
    if (hoursSinceLatestRollback >= delayInHours && maxRetryAttempts >= latestRollbackInfo.count) {
        log("Previous rollback should be ignored due to rollback retry options.");
        return false;
    }

    return true;
}

function validateLatestRollbackInfo(latestRollbackInfo, packageHash) {
    return (
        latestRollbackInfo &&
        latestRollbackInfo.time &&
        latestRollbackInfo.count &&
        latestRollbackInfo.packageHash &&
        latestRollbackInfo.packageHash === packageHash
    );
}

function validateRollbackRetryOptions(rollbackRetryOptions) {
    if (typeof rollbackRetryOptions.delayInHours !== "number") {
        log("The 'delayInHours' rollback retry parameter must be a number.");
        return false;
    }

    if (typeof rollbackRetryOptions.maxRetryAttempts !== "number") {
        log("The 'maxRetryAttempts' rollback retry parameter must be a number.");
        return false;
    }

    if (rollbackRetryOptions.maxRetryAttempts < 1) {
        log("The 'maxRetryAttempts' rollback retry parameter cannot be less then 1.");
        return false;
    }

    return true;
}

var testConfig;

// This function is only used for tests. Replaces the default SDK, configuration and native bridge
function setUpTestDependencies(testSdk, providedTestConfig, testNativeBridge) {
    if (testSdk) module.exports.AcquisitionSdk = testSdk;
    if (providedTestConfig) testConfig = providedTestConfig;
    if (testNativeBridge) NativeCodePush = testNativeBridge;
}

// This function allows only one syncInternal operation to proceed at any given time.
// Parallel calls to sync() while one is ongoing yields CodePush.SyncStatus.SYNC_IN_PROGRESS.
const sync = (() => {
    let syncInProgress = false;
    const setSyncCompleted = () => {
        syncInProgress = false;
    };

    return (options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) => {
        let syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch;
        if (typeof syncStatusChangeCallback === "function") {
            syncStatusCallbackWithTryCatch = (...args) => {
                try {
                    syncStatusChangeCallback(...args);
                } catch (error) {
                    log(`An error has occurred : ${error.stack}`);
                }
            };
        }

        if (typeof downloadProgressCallback === "function") {
            downloadProgressCallbackWithTryCatch = (...args) => {
                try {
                    downloadProgressCallback(...args);
                } catch (error) {
                    log(`An error has occurred: ${error.stack}`);
                }
            };
        }

        if (syncInProgress) {
            typeof syncStatusCallbackWithTryCatch === "function"
                ? syncStatusCallbackWithTryCatch(CodePush.SyncStatus.SYNC_IN_PROGRESS)
                : log("Sync already in progress.");
            return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
        }

        syncInProgress = true;
        const syncPromise = syncInternal(
            options,
            syncStatusCallbackWithTryCatch,
            downloadProgressCallbackWithTryCatch,
            handleBinaryVersionMismatchCallback
        );
        syncPromise.then(setSyncCompleted).catch(setSyncCompleted);

        return syncPromise;
    };
})();

/*
 * The syncInternal method provides a simple, one-line experience for
 * incorporating the check, download and installation of an update.
 *
 * It simply composes the existing API methods together and adds additional
 * support for respecting mandatory updates, ignoring previously failed
 * releases, and displaying a standard confirmation UI to the end-user
 * when an update is available.
 */
async function syncInternal(options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) {
    let resolvedInstallMode;
    const syncOptions = {
        deploymentKey: null,
        ignoreFailedUpdates: true,
        rollbackRetryOptions: null,
        installMode: CodePush.InstallMode.ON_NEXT_RESTART,
        mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
        minimumBackgroundDuration: 0,
        updateDialog: null,
        ...options
    };

    syncStatusChangeCallback =
        typeof syncStatusChangeCallback === "function"
            ? syncStatusChangeCallback
            : syncStatus => {
                  switch (syncStatus) {
                      case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
                          log("Checking for update.");
                          break;
                      case CodePush.SyncStatus.AWAITING_USER_ACTION:
                          log("Awaiting user action.");
                          break;
                      case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
                          log("Downloading package.");
                          break;
                      case CodePush.SyncStatus.INSTALLING_UPDATE:
                          log("Installing update.");
                          break;
                      case CodePush.SyncStatus.UP_TO_DATE:
                          log("App is up to date.");
                          break;
                      case CodePush.SyncStatus.UPDATE_IGNORED:
                          log("User cancelled the update.");
                          break;
                      case CodePush.SyncStatus.UPDATE_INSTALLED:
                          if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) {
                              log("Update is installed and will be run on the next app restart.");
                          } else if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESUME) {
                              if (syncOptions.minimumBackgroundDuration > 0) {
                                  log(
                                      `Update is installed and will be run after the app has been in the background for at least ${
                                          syncOptions.minimumBackgroundDuration
                                      } seconds.`
                                  );
                              } else {
                                  log("Update is installed and will be run when the app next resumes.");
                              }
                          }
                          break;
                      case CodePush.SyncStatus.UNKNOWN_ERROR:
                          log("An unknown error occurred.");
                          break;
                  }
              };

    try {
        await CodePush.notifyApplicationReady();

        syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);
        const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);

        const doDownloadAndInstall = async () => {
            syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
            const localPackage = await remotePackage.download(downloadProgressCallback);

            // Determine the correct install mode based on whether the update is mandatory or not.
            resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;

            syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
            await localPackage.install(resolvedInstallMode, syncOptions.minimumBackgroundDuration, () => {
                syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
            });

            return CodePush.SyncStatus.UPDATE_INSTALLED;
        };

        const updateShouldBeIgnored = await shouldUpdateBeIgnored(remotePackage, syncOptions);

        if (!remotePackage || updateShouldBeIgnored) {
            if (updateShouldBeIgnored) {
                log("An update is available, but it is being ignored due to having been previously rolled back.");
            }

            const currentPackage = await CodePush.getCurrentPackage();
            if (currentPackage && currentPackage.isPending) {
                syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
                return CodePush.SyncStatus.UPDATE_INSTALLED;
            } else {
                syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE);
                return CodePush.SyncStatus.UP_TO_DATE;
            }
        } else if (syncOptions.updateDialog) {
            // updateDialog supports any truthy value (e.g. true, "goo", 12),
            // but we should treat a non-object value as just the default dialog
            if (typeof syncOptions.updateDialog !== "object") {
                syncOptions.updateDialog = CodePush.DEFAULT_UPDATE_DIALOG;
            } else {
                syncOptions.updateDialog = { ...CodePush.DEFAULT_UPDATE_DIALOG, ...syncOptions.updateDialog };
            }

            return await new Promise((resolve, reject) => {
                let message = null;
                let installButtonText = null;

                const dialogButtons = [];

                if (remotePackage.isMandatory) {
                    message = syncOptions.updateDialog.mandatoryUpdateMessage;
                    installButtonText = syncOptions.updateDialog.mandatoryContinueButtonLabel;
                } else {
                    message = syncOptions.updateDialog.optionalUpdateMessage;
                    installButtonText = syncOptions.updateDialog.optionalInstallButtonLabel;
                    // Since this is an optional update, add a button
                    // to allow the end-user to ignore it
                    dialogButtons.push({
                        text: syncOptions.updateDialog.optionalIgnoreButtonLabel,
                        onPress: () => {
                            syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_IGNORED);
                            resolve(CodePush.SyncStatus.UPDATE_IGNORED);
                        }
                    });
                }

                // Since the install button should be placed to the
                // right of any other button, add it last
                dialogButtons.push({
                    text: installButtonText,
                    onPress: () => {
                        doDownloadAndInstall().then(resolve, reject);
                    }
                });

                // If the update has a description, and the developer
                // explicitly chose to display it, then set that as the message
                if (syncOptions.updateDialog.appendReleaseDescription && remotePackage.description) {
                    message += `${syncOptions.updateDialog.descriptionPrefix} ${remotePackage.description}`;
                }

                syncStatusChangeCallback(CodePush.SyncStatus.AWAITING_USER_ACTION);
                Alert.alert(syncOptions.updateDialog.title, message, dialogButtons);
            });
        } else {
            return await doDownloadAndInstall();
        }
    } catch (error) {
        syncStatusChangeCallback(CodePush.SyncStatus.UNKNOWN_ERROR);
        log(error.message);
        throw error;
    }
}

let CodePush;

function codePushify(options = {}) {
    let React;
    let ReactNative = require("react-native");

    try {
        React = require("react");
    } catch (e) {}
    if (!React) {
        try {
            React = ReactNative.React;
        } catch (e) {}
        if (!React) {
            throw new Error("Unable to find the 'React' module.");
        }
    }

    if (!React.Component) {
        throw new Error(
            `Unable to find the "Component" class, please either:
1. Upgrade to a newer version of React Native that supports it, or
2. Call the codePush.sync API in your component instead of using the @codePush decorator`
        );
    }

    var decorator = RootComponent => {
        const extended = class CodePushComponent extends React.Component {
            componentDidMount() {
                if (options.checkFrequency === CodePush.CheckFrequency.MANUAL) {
                    CodePush.notifyAppReady();
                } else {
                    let rootComponentInstance = this.refs.rootComponent;

                    let syncStatusCallback;
                    if (rootComponentInstance && rootComponentInstance.codePushStatusDidChange) {
                        syncStatusCallback = rootComponentInstance.codePushStatusDidChange;
                        if (rootComponentInstance instanceof React.Component) {
                            syncStatusCallback = syncStatusCallback.bind(rootComponentInstance);
                        }
                    }

                    let downloadProgressCallback;
                    if (rootComponentInstance && rootComponentInstance.codePushDownloadDidProgress) {
                        downloadProgressCallback = rootComponentInstance.codePushDownloadDidProgress;
                        if (rootComponentInstance instanceof React.Component) {
                            downloadProgressCallback = downloadProgressCallback.bind(rootComponentInstance);
                        }
                    }

                    let handleBinaryVersionMismatchCallback;
                    if (rootComponentInstance && rootComponentInstance.codePushOnBinaryVersionMismatch) {
                        handleBinaryVersionMismatchCallback = rootComponentInstance.codePushOnBinaryVersionMismatch;
                        if (rootComponentInstance instanceof React.Component) {
                            handleBinaryVersionMismatchCallback = handleBinaryVersionMismatchCallback.bind(rootComponentInstance);
                        }
                    }

                    CodePush.sync(options, syncStatusCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback);
                    if (options.checkFrequency === CodePush.CheckFrequency.ON_APP_RESUME) {
                        ReactNative.AppState.addEventListener("change", newState => {
                            newState === "active" && CodePush.sync(options, syncStatusCallback, downloadProgressCallback);
                        });
                    }
                }
            }

            render() {
                const props = { ...this.props };

                // we can set ref property on class components only (not stateless)
                // check it by render method
                if (RootComponent.prototype.render) {
                    props.ref = "rootComponent";
                }

                return <RootComponent {...props} />;
            }
        };

        return hoistStatics(extended, RootComponent);
    };

    if (typeof options === "function") {
        // Infer that the root component was directly passed to us.
        return decorator(options);
    } else {
        return decorator;
    }
}

// If the "NativeCodePush" variable isn't defined, then
// the app didn't properly install the native module,
// and therefore, it doesn't make sense initializing
// the JS interface when it wouldn't work anyways.
if (NativeCodePush) {
    CodePush = codePushify;
    Object.assign(CodePush, {
        AcquisitionSdk: Sdk,
        checkForUpdate,
        getConfiguration,
        getCurrentPackage,
        getUpdateMetadata,
        setServerUrl,
        log,
        notifyAppReady: notifyApplicationReady,
        notifyApplicationReady,
        restartApp: RestartManager.restartApp,
        setUpTestDependencies,
        sync,
        disallowRestart: RestartManager.disallow,
        allowRestart: RestartManager.allow,
        clearUpdates: NativeCodePush.clearUpdates,
        InstallMode: {
            IMMEDIATE: NativeCodePush.codePushInstallModeImmediate, // Restart the app immediately
            ON_NEXT_RESTART: NativeCodePush.codePushInstallModeOnNextRestart, // Don't artificially restart the app. Allow the update to be "picked up" on the next app restart
            ON_NEXT_RESUME: NativeCodePush.codePushInstallModeOnNextResume, // Restart the app the next time it is resumed from the background
            ON_NEXT_SUSPEND: NativeCodePush.codePushInstallModeOnNextSuspend // Restart the app _while_ it is in the background,
            // but only after it has been in the background for "minimumBackgroundDuration" seconds (0 by default),
            // so that user context isn't lost unless the app suspension is long enough to not matter
        },
        SyncStatus: {
            UP_TO_DATE: 0, // The running app is up-to-date
            UPDATE_INSTALLED: 1, // The app had an optional/mandatory update that was successfully downloaded and is about to be installed.
            UPDATE_IGNORED: 2, // The app had an optional update and the end-user chose to ignore it
            UNKNOWN_ERROR: 3,
            SYNC_IN_PROGRESS: 4, // There is an ongoing "sync" operation in progress.
            CHECKING_FOR_UPDATE: 5,
            AWAITING_USER_ACTION: 6,
            DOWNLOADING_PACKAGE: 7,
            INSTALLING_UPDATE: 8
        },
        CheckFrequency: {
            ON_APP_START: 0,
            ON_APP_RESUME: 1,
            MANUAL: 2
        },
        UpdateState: {
            RUNNING: NativeCodePush.codePushUpdateStateRunning,
            PENDING: NativeCodePush.codePushUpdateStatePending,
            LATEST: NativeCodePush.codePushUpdateStateLatest
        },
        DeploymentStatus: {
            FAILED: "DeploymentFailed",
            SUCCEEDED: "DeploymentSucceeded"
        },
        DEFAULT_UPDATE_DIALOG: {
            appendReleaseDescription: false,
            descriptionPrefix: " Description: ",
            mandatoryContinueButtonLabel: "Continue",
            mandatoryUpdateMessage: "An update is available that must be installed.",
            optionalIgnoreButtonLabel: "Ignore",
            optionalInstallButtonLabel: "Install",
            optionalUpdateMessage: "An update is available. Would you like to install it?",
            title: "Update available"
        },
        DEFAULT_ROLLBACK_RETRY_OPTIONS: {
            delayInHours: 24,
            maxRetryAttempts: 1
        }
    });
} else {
    log("The CodePush module doesn't appear to be properly installed. Please double-check that everything is setup correctly.");
}

module.exports = CodePush;
