import { AcquisitionManager as Sdk } from "code-push/script/acquisition-sdk";

import { Alert } from "./AlertAdapter";

import requestFetchAdapter from "./request-fetch-adapter";

import { AppState, Platform, DeviceEventEmitter } from "react-native";

import log from "./logging";

import hoistStatics from 'hoist-non-react-statics';

import NativeCodePush from './src/NativeCodePush';



const PackageMixins = require("./package-mixins")(NativeCodePush);

async function checkForUpdate(deploymentKey = null, handleBinaryVersionMismatchCallback = null) {

  console.log('checkForUpdate-entry');

  const nativeConfig = await getConfiguration();

  const config = deploymentKey ? { ...nativeConfig, ...{ deploymentKey } } : nativeConfig;

  console.log('checkForUpdate-config='+JSON.stringify(config));

  const sdk = getPromisifiedSdk(requestFetchAdapter, config);

  const localPackage = await module.exports.getCurrentPackage();

  console.log('checkForUpdate-localPackage='+JSON.stringify(localPackage));

  let queryPackage;

  if (localPackage) {

    queryPackage = localPackage;

  } else {

    queryPackage = { appVersion: config.appVersion };

    if (Platform.OS === "ios" && config.packageHash) {

      queryPackage.packageHash = config.packageHash;

    }

  }

  console.log('checkForUpdate-queryPackage='+JSON.stringify(queryPackage));

  const update = await sdk.queryUpdateWithCurrentPackage(queryPackage);

  console.log('checkForUpdate-update='+JSON.stringify(update));

  if (!update || update.updateAppVersion ||

    localPackage && (update.packageHash === localPackage.packageHash) ||

    (!localPackage || localPackage._isDebugOnly) && config.packageHash === update.packageHash) {

    console.log('checkForUpdate-进!update判断');

    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;

}



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, retryOnAppResume) {

  const config = await getConfiguration();

  const previousLabelOrAppVersion = statusReport.previousLabelOrAppVersion;

  const previousDeploymentKey = statusReport.previousDeploymentKey || config.deploymentKey;

  try {

    if (statusReport.appVersion) {

      log(`Reporting binary update (${statusReport.appVersion})`);



      if (!config.deploymentKey) {

        throw new Error("Deployment key is missed");

      }



      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);

    retryOnAppResume && retryOnAppResume.remove();

  } catch (e) {

    log(`Report status failed: ${JSON.stringify(statusReport)}`);

    NativeCodePush.saveStatusReportForRetry(statusReport);

    // Try again when the app resumes

    if (!retryOnAppResume) {

      const resumeListener = AppState.addEventListener("change", async (newState) => {

        if (newState !== "active") return;

        const refreshedStatusReport = await NativeCodePush.getNewStatusReport();

        if (refreshedStatusReport) {

          tryReportStatus(refreshedStatusReport, resumeListener);

        } else {

          resumeListener && resumeListener.remove();

        }

      });

    }

  }

}



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;

}



async function restartApp(onlyIfUpdateIsPending = false) {

  NativeCodePush.restartApp(onlyIfUpdateIsPending);

}



// 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") {

      DeviceEventEmitter.addListener('CodePushDownloadProgress', (date) => {

        downloadProgressCallback(date)

      })

      downloadProgressCallbackWithTryCatch = (...args) => {

        DeviceEventEmitter.addListener('CodePushDownloadProgress', (date) => {

        })

      }

    }



    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;

  };

})();



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 {

    syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE);

    const remotePackage = await checkForUpdate(syncOptions.deploymentKey, handleBinaryVersionMismatchCallback);

    console.log('checkForUpdate-remotePackage='+JSON.stringify(remotePackage) );

    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) {

      console.log('checkForUpdate-!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) {

      console.log('checkForUpdate-syncOptions.updateDialog= 去下载更新');

      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`

    );

  }



  const decorator = (RootComponent) => {

    class CodePushComponent extends React.Component {

      constructor(props) {

        super(props);

        this.rootComponentRef = React.createRef();

      }



      componentDidMount() {

        if (options.checkFrequency === CodePush.CheckFrequency.MANUAL) {

          CodePush.notifyAppReady();

        } else {

          const rootComponentInstance = this.rootComponentRef.current;



          let syncStatusCallback;

          if (rootComponentInstance && rootComponentInstance.codePushStatusDidChange) {

            syncStatusCallback = rootComponentInstance.codePushStatusDidChange.bind(rootComponentInstance);

          }



          let downloadProgressCallback;

          if (rootComponentInstance && rootComponentInstance.codePushDownloadDidProgress) {

            downloadProgressCallback = rootComponentInstance.codePushDownloadDidProgress.bind(rootComponentInstance);

          }



          let handleBinaryVersionMismatchCallback;

          if (rootComponentInstance && rootComponentInstance.codePushOnBinaryVersionMismatch) {

            handleBinaryVersionMismatchCallback = rootComponentInstance.codePushOnBinaryVersionMismatch.bind(rootComponentInstance);

          }



          CodePush.sync(options, syncStatusCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback);



          if (options.checkFrequency === CodePush.CheckFrequency.ON_APP_RESUME) {

            ReactNative.AppState.addEventListener("change", (newState) => {

              if (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 && RootComponent.prototype.render) {

          props.ref = this.rootComponentRef;

        }



        return <RootComponent {...props} />

      }

    }



    return hoistStatics(CodePushComponent, 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,

    log,

    notifyAppReady: notifyApplicationReady,

    notifyApplicationReady,

    restartApp,

    setUpTestDependencies,

    sync,

    disallowRestart: NativeCodePush.disallow,

    allowRestart: NativeCodePush.allow,

    clearUpdates: NativeCodePush.clearUpdates,

    InstallMode: {

      IMMEDIATE: 0, // Restart the app immediately

      ON_NEXT_RESTART: 1, // Don't artificially restart the app. Allow the update to be "picked up" on the next app restart

      ON_NEXT_RESUME: 2, // Restart the app the next time it is resumed from the background

      ON_NEXT_SUSPEND: 3 // 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: 0,//NativeCodePush.codePushUpdateStateRunning

      PENDING: 1,//NativeCodePush.codePushUpdateStatePending

      LATEST: 2 // 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;