// Script to add feature overrides
//
// A feature override is appended to the user's featuresOverride list if they do
// not already have the feature. The features are refreshed after adding the
// override.
//
// If the script detects that the user would have the feature just by refreshing
// then it skips adding the override and just refreshes the users features --
// this is to minimise the creation of unnecessary overrides.
//
// Usage:
//
// $ node scripts/add_feature_override.mjs --commit --note 'text description' --expires 2022-01-01 --override JSONFILE --ids IDFILE
//
// --commit   do the update, remove this option for dry-run testing
// --note     text description [optional]
// --expires  expiry date for override [optional]
// --skip-existing   don't create the override for users who already have the feature (e.g. via a subscription)
//
// IDFILE: file containing list of user ids, one per line
// JSONFILE:  file containing JSON of the desired feature overrides e.g. {"symbolPalette": true}
//
// The feature override is specified with JSON to allow types to be set as string/number/boolean.
// It is contained in a file to avoid any issues with shell quoting.

import minimist from 'minimist'

import fs from 'node:fs'
import { ObjectId } from '../app/src/infrastructure/mongodb.js'
import pLimit from 'p-limit'
import FeaturesUpdater from '../app/src/Features/Subscription/FeaturesUpdater.js'
import FeaturesHelper from '../app/src/Features/Subscription/FeaturesHelper.js'
import UserFeaturesUpdater from '../app/src/Features/Subscription/UserFeaturesUpdater.js'
import UserGetter from '../app/src/Features/User/UserGetter.js'

const processLogger = {
  failed: [],
  success: [],
  skipped: [],
  printSummary: () => {
    console.log(
      {
        success: processLogger.success,
        failed: processLogger.failed,
        skipped: processLogger.skipped,
      },
      `\nDONE. ${processLogger.success.length} successful. ${processLogger.skipped.length} skipped. ${processLogger.failed.length} failed to update.`
    )
  },
}

function _validateUserIdList(userIds) {
  userIds.forEach(userId => {
    if (!ObjectId.isValid(userId))
      throw new Error(`user ID not valid: ${userId}`)
  })
}

async function _handleUser(userId) {
  console.log('updating user', userId)
  const user = await UserGetter.promises.getUser(userId, {
    features: 1,
    featuresOverrides: 1,
  })
  if (!user) {
    console.log(userId, 'does not exist, failed')
    processLogger.failed.push(userId)
    return
  }
  const desiredFeatures = OVERRIDE.features
  // Does the user have the requested features already?
  if (
    SKIP_EXISTING &&
    FeaturesHelper.isFeatureSetBetter(user.features, desiredFeatures)
  ) {
    console.log(
      userId,
      `already has ${JSON.stringify(desiredFeatures)}, skipping`
    )
    processLogger.skipped.push(userId)
    return
  }
  // Would the user have the requested feature if the features were refreshed?
  const freshFeatures = await FeaturesUpdater.promises.computeFeatures(userId)
  if (
    SKIP_EXISTING &&
    FeaturesHelper.isFeatureSetBetter(freshFeatures, desiredFeatures)
  ) {
    console.log(
      userId,
      `would have ${JSON.stringify(
        desiredFeatures
      )} if refreshed, skipping override`
    )
  } else {
    // create the override (if not in dry-run mode)
    if (COMMIT) {
      await UserFeaturesUpdater.promises.createFeaturesOverride(
        userId,
        OVERRIDE
      )
    }
  }
  if (!COMMIT) {
    // not saving features; nothing else to do
    return
  }
  const refreshResult = await FeaturesUpdater.promises.refreshFeatures(
    userId,
    'add-feature-override-script'
  )
  const featureSetIncludesNewFeatures = FeaturesHelper.isFeatureSetBetter(
    refreshResult.features,
    desiredFeatures
  )
  if (featureSetIncludesNewFeatures) {
    // features added successfully
    processLogger.success.push(userId)
  } else {
    console.log('FEATURE NOT ADDED', refreshResult)
    processLogger.failed.push(userId)
  }
}

const argv = minimist(process.argv.slice(2))
const CONCURRENCY = argv.async ? argv.async : 10
const overridesFilename = argv.override
const expires = argv.expires
const note = argv.note
const SKIP_EXISTING = argv['skip-existing'] || false
const COMMIT = argv.commit !== undefined
if (!COMMIT) {
  console.warn('Doing dry run without --commit')
}

const idsFilename = argv.ids
if (!idsFilename) throw new Error('missing ids list filename')

const usersFile = fs.readFileSync(idsFilename, 'utf8')
const userIds = usersFile
  .trim()
  .split('\n')
  .map(id => id.trim())

const overridesFile = fs.readFileSync(overridesFilename, 'utf8')
const features = JSON.parse(overridesFile)
const OVERRIDE = { features }
if (note) {
  OVERRIDE.note = note
}
if (expires) {
  OVERRIDE.expiresAt = new Date(expires)
}

async function processUsers(userIds) {
  console.log('---Starting add feature override script---')

  console.log('Will update users to have', OVERRIDE)
  console.log(
    SKIP_EXISTING
      ? 'Users with this feature already will be skipped'
      : 'Every user in file will get feature override'
  )

  _validateUserIdList(userIds)
  console.log(`---Starting to process ${userIds.length} users---`)

  const limit = pLimit(CONCURRENCY)
  const results = await Promise.allSettled(
    userIds.map(userId => limit(() => _handleUser(new ObjectId(userId))))
  )
  results.forEach((result, idx) => {
    if (result.status !== 'fulfilled') {
      console.log(userIds[idx], 'failed', result.reason)
      processLogger.failed.push(userIds[idx])
    }
  })
  processLogger.printSummary()
  process.exit()
}

await processUsers(userIds)
