const async = require('async')
const OError = require('@overleaf/o-error')
const PlansLocator = require('./PlansLocator')
const _ = require('underscore')
const SubscriptionLocator = require('./SubscriptionLocator')
const UserFeaturesUpdater = require('./UserFeaturesUpdater')
const Settings = require('settings-sharelatex')
const logger = require('logger-sharelatex')
const ReferalFeatures = require('../Referal/ReferalFeatures')
const V1SubscriptionManager = require('./V1SubscriptionManager')
const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures')
const UserGetter = require('../User/UserGetter')

const FeaturesUpdater = {
  refreshFeatures(userId, callback = () => {}) {
    UserGetter.getUser(userId, { _id: 1, features: 1 }, (err, user) => {
      if (err) {
        return callback(err)
      }
      const oldFeatures = _.clone(user.features)
      FeaturesUpdater._computeFeatures(userId, (error, features) => {
        if (error) {
          return callback(error)
        }
        logger.log({ userId, features }, 'updating user features')
        UserFeaturesUpdater.updateFeatures(
          userId,
          features,
          (err, newFeatures, featuresChanged) => {
            if (err) {
              return callback(err)
            }
            if (oldFeatures.dropbox === true && features.dropbox === false) {
              logger.log({ userId }, '[FeaturesUpdater] must unlink dropbox')
              const Modules = require('../../infrastructure/Modules')
              Modules.hooks.fire('removeDropbox', userId, err => {
                if (err) {
                  logger.error(err)
                }
              })
            }
            return callback(null, newFeatures, featuresChanged)
          }
        )
      })
    })
  },

  _computeFeatures(userId, callback) {
    const jobs = {
      individualFeatures(cb) {
        FeaturesUpdater._getIndividualFeatures(userId, cb)
      },
      groupFeatureSets(cb) {
        FeaturesUpdater._getGroupFeatureSets(userId, cb)
      },
      institutionFeatures(cb) {
        InstitutionsFeatures.getInstitutionsFeatures(userId, cb)
      },
      v1Features(cb) {
        FeaturesUpdater._getV1Features(userId, cb)
      },
      bonusFeatures(cb) {
        ReferalFeatures.getBonusFeatures(userId, cb)
      },
      featuresOverrides(cb) {
        FeaturesUpdater._getFeaturesOverrides(userId, cb)
      }
    }
    async.series(jobs, function (err, results) {
      if (err) {
        OError.tag(
          err,
          'error getting subscription or group for refreshFeatures',
          {
            userId
          }
        )
        return callback(err)
      }

      const {
        individualFeatures,
        groupFeatureSets,
        institutionFeatures,
        v1Features,
        bonusFeatures,
        featuresOverrides
      } = results
      logger.log(
        {
          userId,
          individualFeatures,
          groupFeatureSets,
          institutionFeatures,
          v1Features,
          bonusFeatures,
          featuresOverrides
        },
        'merging user features'
      )
      const featureSets = groupFeatureSets.concat([
        individualFeatures,
        institutionFeatures,
        v1Features,
        bonusFeatures,
        featuresOverrides
      ])
      const features = _.reduce(
        featureSets,
        FeaturesUpdater._mergeFeatures,
        Settings.defaultFeatures
      )
      callback(null, features)
    })
  },

  _getIndividualFeatures(userId, callback) {
    SubscriptionLocator.getUserIndividualSubscription(userId, (err, sub) =>
      callback(err, FeaturesUpdater._subscriptionToFeatures(sub))
    )
  },

  _getGroupFeatureSets(userId, callback) {
    SubscriptionLocator.getGroupSubscriptionsMemberOf(userId, (err, subs) =>
      callback(err, (subs || []).map(FeaturesUpdater._subscriptionToFeatures))
    )
  },

  _getFeaturesOverrides(userId, callback) {
    UserGetter.getUser(userId, { featuresOverrides: 1 }, (error, user) => {
      if (error) {
        return callback(error)
      }
      if (
        !user ||
        !user.featuresOverrides ||
        user.featuresOverrides.length === 0
      ) {
        return callback(null, {})
      }
      let activeFeaturesOverrides = []
      for (let featuresOverride of user.featuresOverrides) {
        if (
          !featuresOverride.expiresAt ||
          featuresOverride.expiresAt > new Date()
        ) {
          activeFeaturesOverrides.push(featuresOverride.features)
        }
      }
      const features = _.reduce(
        activeFeaturesOverrides,
        FeaturesUpdater._mergeFeatures,
        {}
      )
      callback(null, features)
    })
  },

  _getV1Features(userId, callback) {
    V1SubscriptionManager.getPlanCodeFromV1(
      userId,
      function (err, planCode, v1Id) {
        if (err) {
          if ((err ? err.name : undefined) === 'NotFoundError') {
            return callback(null, [])
          }
          return callback(err)
        }

        callback(
          err,
          FeaturesUpdater._mergeFeatures(
            V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) || {},
            FeaturesUpdater._planCodeToFeatures(planCode)
          )
        )
      }
    )
  },

  _mergeFeatures(featuresA, featuresB) {
    const features = Object.assign({}, featuresA)
    for (let key in featuresB) {
      // Special merging logic for non-boolean features
      if (key === 'compileGroup') {
        if (
          features.compileGroup === 'priority' ||
          featuresB.compileGroup === 'priority'
        ) {
          features.compileGroup = 'priority'
        } else {
          features.compileGroup = 'standard'
        }
      } else if (key === 'collaborators') {
        if (features.collaborators === -1 || featuresB.collaborators === -1) {
          features.collaborators = -1
        } else {
          features.collaborators = Math.max(
            features.collaborators || 0,
            featuresB.collaborators || 0
          )
        }
      } else if (key === 'compileTimeout') {
        features.compileTimeout = Math.max(
          features.compileTimeout || 0,
          featuresB.compileTimeout || 0
        )
      } else {
        // Boolean keys, true is better
        features[key] = features[key] || featuresB[key]
      }
    }
    return features
  },

  _subscriptionToFeatures(subscription) {
    return FeaturesUpdater._planCodeToFeatures(
      subscription ? subscription.planCode : undefined
    )
  },

  _planCodeToFeatures(planCode) {
    if (!planCode) {
      return {}
    }
    const plan = PlansLocator.findLocalPlanInSettings(planCode)
    if (!plan) {
      return {}
    } else {
      return plan.features
    }
  },

  compareFeatures(currentFeatures, expectedFeatures) {
    currentFeatures = _.clone(currentFeatures)
    expectedFeatures = _.clone(expectedFeatures)
    if (_.isEqual(currentFeatures, expectedFeatures)) {
      return {}
    }

    let mismatchReasons = {}
    const featureKeys = [
      ...new Set([
        ...Object.keys(currentFeatures),
        ...Object.keys(expectedFeatures)
      ])
    ]
    featureKeys.sort().forEach(key => {
      if (expectedFeatures[key] !== currentFeatures[key]) {
        mismatchReasons[key] = expectedFeatures[key]
      }
    })

    if (mismatchReasons.compileTimeout) {
      // store the compile timeout difference instead of the new compile timeout
      mismatchReasons.compileTimeout =
        expectedFeatures.compileTimeout - currentFeatures.compileTimeout
    }

    if (mismatchReasons.collaborators) {
      // store the collaborators difference instead of the new number only
      // replace -1 by 100 to make it clearer
      if (expectedFeatures.collaborators === -1) {
        expectedFeatures.collaborators = 100
      }
      if (currentFeatures.collaborators === -1) {
        currentFeatures.collaborators = 100
      }
      mismatchReasons.collaborators =
        expectedFeatures.collaborators - currentFeatures.collaborators
    }

    return mismatchReasons
  },

  doSyncFromV1(v1UserId, callback) {
    logger.log({ v1UserId }, '[AccountSync] starting account sync')
    return UserGetter.getUser(
      { 'overleaf.id': v1UserId },
      { _id: 1 },
      function (err, user) {
        if (err != null) {
          OError.tag(err, '[AccountSync] error getting user', {
            v1UserId
          })
          return callback(err)
        }
        if ((user != null ? user._id : undefined) == null) {
          logger.warn({ v1UserId }, '[AccountSync] no user found for v1 id')
          return callback(null)
        }
        logger.log(
          { v1UserId, userId: user._id },
          '[AccountSync] updating user subscription and features'
        )
        return FeaturesUpdater.refreshFeatures(user._id, callback)
      }
    )
  }
}

const refreshFeaturesPromise = userId =>
  new Promise(function (resolve, reject) {
    FeaturesUpdater.refreshFeatures(
      userId,
      (error, features, featuresChanged) => {
        if (error) {
          reject(error)
        } else {
          resolve({ features, featuresChanged })
        }
      }
    )
  })

FeaturesUpdater.promises = {
  refreshFeatures: refreshFeaturesPromise
}

module.exports = FeaturesUpdater
