import _ from 'lodash'
import { callbackify } from 'node:util'
import { callbackifyMultiResult } from '@overleaf/promise-utils'
import PlansLocator from './PlansLocator.mjs'
import SubscriptionLocator from './SubscriptionLocator.mjs'
import SubscriptionHelper from './SubscriptionHelper.mjs'
import UserFeaturesUpdater from './UserFeaturesUpdater.mjs'
import FeaturesHelper from './FeaturesHelper.mjs'
import Settings from '@overleaf/settings'
import logger from '@overleaf/logger'
import ReferalFeatures from '../Referal/ReferalFeatures.mjs'
import V1SubscriptionManager from './V1SubscriptionManager.mjs'
import InstitutionsFeatures from '../Institutions/InstitutionsFeatures.mjs'
import UserGetter from '../User/UserGetter.mjs'
import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
import Queues from '../../infrastructure/Queues.mjs'
import Modules from '../../infrastructure/Modules.mjs'
import SubscriptionViewModelBuilder from './SubscriptionViewModelBuilder.mjs'
import CustomerIoPlanHelpers from './CustomerIoPlanHelpers.mjs'
import { GroupPolicy } from '../../models/GroupPolicy.mjs'
import { AI_ADD_ON_CODE } from './AiHelper.mjs'
import { fetchNothing } from '@overleaf/fetch-utils'
import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'

/**
 * Enqueue a job for refreshing features for the given user
 */
async function scheduleRefreshFeatures(userId, reason) {
  const queue = Queues.getQueue('refresh-features')
  await queue.add({ userId, reason })
}

/* Check if user features refresh if needed, based on the global featuresEpoch setting */
function featuresEpochIsCurrent(user) {
  return Settings.featuresEpoch
    ? user.featuresEpoch === Settings.featuresEpoch
    : true
}

/**
 * Refresh features for the given user
 */
async function refreshFeatures(userId, reason) {
  const user = await UserGetter.promises.getUser(userId, {
    _id: 1,
    features: 1,
    email: 1,
  })
  const oldFeatures = _.clone(user.features)
  const features = await computeFeatures(userId)
  logger.debug({ userId, features, reason }, 'updating user features')

  const matchedFeatureSet = FeaturesHelper.getMatchedFeatureSet(features)
  AnalyticsManager.setUserPropertyForUserInBackground(
    userId,
    'feature-set',
    matchedFeatureSet
  )

  const { features: newFeatures, featuresChanged } =
    await UserFeaturesUpdater.promises.updateFeatures(userId, features)

  _updateCustomerIoSubscriptionProperties(user, features).catch(err => {
    logger.warn(
      { err, userId },
      'Failed to update subscription properties in customer.io'
    )
  })

  if (oldFeatures.dropbox === true && features.dropbox === false) {
    logger.debug({ userId }, '[FeaturesUpdater] must unlink dropbox')
    try {
      await Modules.promises.hooks.fire('removeDropbox', userId, reason)
    } catch (err) {
      logger.error({ err, userId }, 'removeDropbox hook failed')
    }
  }

  if (oldFeatures.github === true && features.github === false) {
    logger.debug({ userId }, '[FeaturesUpdater] must unlink github')
    try {
      await Modules.promises.hooks.fire('removeGithub', userId, reason)
    } catch (err) {
      logger.error({ err, userId }, 'removeGithub hook failed')
    }
  }

  // only update Writefull if the user's features have changed,
  //  skip if they are the reason we are refreshing features (they'd already be up to date)
  if (featuresChanged && reason !== 'writefullEntitlementSynced') {
    try {
      // todo: quota clean-up: simplify once split test isnt needed
      let hasPremiumAiFeatures
      const inQuotaSplitTest =
        await SplitTestHandler.promises.featureFlagEnabledForUser(
          userId,
          'plans-2026-phase-1'
        )
      if (inQuotaSplitTest) {
        hasPremiumAiFeatures =
          newFeatures.aiUsageQuota === Settings.aiFeatures.unlimitedQuota
      } else {
        hasPremiumAiFeatures = Boolean(newFeatures.aiErrorAssistant)
      }
      // update WF with the current feature set for the user
      await fetchNothing(
        `${Settings.writefull.overleafApiUrl}/api/user/status/update-overleaf-status`,
        {
          headers: {
            'x-api-key': Settings.writefull.overleafApiKey,
          },
          json: {
            userOverleafId: userId,
            // todo: quota clean-up: collab with writefull to rename this, and check if still needed
            hasAiAssist: hasPremiumAiFeatures,
            aiUsageQuota: newFeatures.aiUsageQuota,
          },
          method: 'POST',
        }
      )
    } catch (err) {
      // continue with sync even if we cant communicate with wf
      logger.warn(
        { userId, reason },
        'failed to sync entitlement to Writefull after a feature refresh'
      )
    }
  }
  return { features: newFeatures, featuresChanged }
}

async function _updateCustomerIoSubscriptionProperties(user, features) {
  const userId = user._id
  const {
    bestSubscription,
    individualSubscription,
    memberGroupSubscriptions,
    managedGroupSubscriptions,
    currentInstitutionsWithLicence,
  } = await SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails({
    _id: userId,
  })

  const userIsMemberOfGroupSubscription =
    memberGroupSubscriptions.length > 0 || managedGroupSubscriptions.length > 0
  const hasCommons = (currentInstitutionsWithLicence?.length ?? 0) > 0

  let individualPaymentRecord = null
  if (individualSubscription && !individualSubscription.groupPlan) {
    try {
      ;[individualPaymentRecord] = await Modules.promises.hooks.fire(
        'getPaymentFromRecordPromise',
        individualSubscription
      )
    } catch (error) {
      logger.warn(
        { err: error, userId },
        'Failed to load payment record for customer.io subscription properties'
      )
    }
  }

  let writefullData = null
  try {
    writefullData = await UserGetter.promises.getWritefullData(userId)
  } catch (error) {
    logger.warn(
      { err: error, userId },
      'Failed to load writefull data for customer.io subscription properties'
    )
  }

  const aiBlockedByPolicyId = await _loadAiBlockedByPolicyId([
    ...memberGroupSubscriptions,
    ...managedGroupSubscriptions,
  ])

  const planProperties = CustomerIoPlanHelpers.getPlanProperties({
    bestSubscription,
    individualSubscription,
    individualPaymentRecord,
    memberGroupSubscriptions,
    managedGroupSubscriptions,
    userIsMemberOfGroupSubscription,
    hasCommons,
    writefullData,
    aiBlockedByPolicyId,
    userId,
  })

  await Modules.promises.hooks.fire('setUserProperties', userId, {
    ...planProperties,
    features,
    overleaf_id: userId,
    ...(user.email && { email: user.email }),
  })
}

async function _loadAiBlockedByPolicyId(groupSubscriptions) {
  const policyIds = [
    ...new Set(
      groupSubscriptions.map(sub => sub.groupPolicy?.toString()).filter(Boolean)
    ),
  ]

  if (policyIds.length === 0) {
    return new Map()
  }

  const policies = await GroupPolicy.find(
    { _id: { $in: policyIds } },
    { _id: 1, userCannotUseAIFeatures: 1 }
  ).exec()
  return new Map(
    policies.map(policy => [
      policy._id.toString(),
      Boolean(policy.userCannotUseAIFeatures),
    ])
  )
}

/**
 * Return the features that the given user should have.
 */
async function computeFeatures(userId) {
  const individualFeatures = await _getIndividualFeatures(userId)
  const groupFeatureSets = await _getGroupFeatureSets(userId)
  const institutionFeatures =
    await InstitutionsFeatures.promises.getInstitutionsFeatures(userId)
  const user = await UserGetter.promises.getUser(userId, {
    featuresOverrides: 1,
    'overleaf.id': 1,
  })
  const v1Features = await _getV1Features(user)
  const bonusFeatures = await ReferalFeatures.promises.getBonusFeatures(userId)
  const featuresOverrides = await _getFeaturesOverrides(user)
  logger.debug(
    {
      userId,
      individualFeatures,
      groupFeatureSets,
      institutionFeatures,
      v1Features,
      bonusFeatures,
      featuresOverrides,
    },
    'merging user features'
  )
  const featureSets = groupFeatureSets.concat([
    individualFeatures,
    institutionFeatures,
    v1Features,
    bonusFeatures,
    featuresOverrides,
  ])
  return _.reduce(
    featureSets,
    FeaturesHelper.mergeFeatures,
    Settings.defaultFeatures
  )
}

async function _getIndividualFeatures(userId) {
  const subscription =
    await SubscriptionLocator.promises.getUsersSubscription(userId)
  if (
    subscription == null ||
    SubscriptionHelper.getPaidSubscriptionState(subscription) === 'paused' ||
    subscription.userFeaturesDisabled
  ) {
    return {}
  }

  const featureSets = []

  // The plan doesn't apply to the group admin when the subscription
  // is a group subscription
  if (!subscription.groupPlan) {
    featureSets.push(_subscriptionToFeatures(subscription))
  }

  // todo: quota clean-up - remove
  // if they are in the quota split test, we no longer look at the add-on, since every plan will now have the same quota
  // standalone plan will receive correct state since their plan will provide the correct quota
  featureSets.push(_aiAddOnFeatures(subscription))
  return _.reduce(featureSets, FeaturesHelper.mergeFeatures, {})
}

async function _getGroupFeatureSets(userId) {
  const subs =
    await SubscriptionLocator.promises.getGroupSubscriptionsMemberOf(userId)
  return (subs || [])
    .filter(sub => sub.userFeaturesDisabled !== true)
    .map(_subscriptionToFeatures)
}

async function _getFeaturesOverrides(user) {
  if (!user || !user.featuresOverrides || user.featuresOverrides.length === 0) {
    return {}
  }
  const activeFeaturesOverrides = []
  for (const featuresOverride of user.featuresOverrides) {
    if (
      !featuresOverride.expiresAt ||
      featuresOverride.expiresAt > new Date()
    ) {
      activeFeaturesOverrides.push(featuresOverride.features)
    }
  }
  return _.reduce(activeFeaturesOverrides, FeaturesHelper.mergeFeatures, {})
}

async function _getV1Features(user) {
  const v1Id = user?.overleaf?.id
  return V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) || {}
}

function _subscriptionToFeatures(subscription) {
  if (!subscription?.planCode) {
    return {}
  }
  const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
  if (!plan) {
    return {}
  } else {
    return plan.features
  }
}

// todo: quota clean-up: remove post split test
function _aiAddOnFeatures(subscription) {
  if (subscription?.addOns?.some(addOn => addOn.addOnCode === AI_ADD_ON_CODE)) {
    return {
      // allow both naming systems to work
      aiErrorAssistant: true,
      aiUsageQuota: Settings.aiFeatures.unlimitedQuota,
    }
  } else {
    return {}
  }
}

async function doSyncFromV1(v1UserId) {
  logger.debug({ v1UserId }, '[AccountSync] starting account sync')
  const user = await UserGetter.promises.getUser(
    { 'overleaf.id': v1UserId },
    { _id: 1 }
  )
  if (user == null) {
    logger.warn({ v1UserId }, '[AccountSync] no user found for v1 id')
    return
  }
  logger.debug(
    { v1UserId, userId: user._id },
    '[AccountSync] updating user subscription and features'
  )
  return refreshFeatures(user._id, 'sync-v1')
}

export default {
  featuresEpochIsCurrent,
  computeFeatures: callbackify(computeFeatures),
  refreshFeatures: callbackifyMultiResult(refreshFeatures, [
    'features',
    'featuresChanged',
  ]),
  doSyncFromV1: callbackifyMultiResult(doSyncFromV1, [
    'features',
    'featuresChanged',
  ]),
  scheduleRefreshFeatures: callbackify(scheduleRefreshFeatures),
  promises: {
    computeFeatures,
    refreshFeatures,
    scheduleRefreshFeatures,
    doSyncFromV1,
  },
}
