mongoose = require 'mongoose'
jsonschema = require '../../app/schemas/models/earned_achievement'
util = require '../../app/core/utils'
log = require 'winston'
co = require 'co'
errors = require '../commons/errors'

EarnedAchievementSchema = new mongoose.Schema({
  notified:
    type: Boolean
    default: false
}, {strict:false})

EarnedAchievementSchema.pre 'save', (next) ->
  @set('changed', new Date())
  next()

EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'})
EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '})


EarnedAchievementSchema.statics.upsertFor = co.wrap (achievement, trigger, earned, user) ->
  
  if achievement.get('proportionalTo') and earned
    earnedAchievementDoc = yield @createForAchievement(achievement, trigger, {previouslyEarnedAchievement: earned})
    return earnedAchievementDoc or earned

  else if earned
    achievementEarned = achievement.get('rewards')
    actuallyEarned = earned.get('earnedRewards')
    oldPoints = earned.get('earnedPoints') ? 0
    newPoints = achievement.get('worth') ? 10
    if (not _.isEqual(achievementEarned, actuallyEarned)) or (oldPoints isnt newPoints)
        earned.set('earnedRewards', achievementEarned)
        earned.set('earnedPoints', newPoints)
        yield earned.save()

    # make sure user has all the levels, heroes, gems, items and points they should have
    update = {$inc: { points: newPoints - oldPoints }}

    for rewardType, rewards of achievement.get('rewards') ? {}
      if rewardType is 'gems'
        update.$inc['earned.gems'] = rewards - (actuallyEarned?.gems ? 0)
      else if rewards.length
        update.$addToSet ?= {}
        update.$addToSet["earned.#{rewardType}"] = { $each: rewards }
    yield user.update(update)
    return earned

  else
    earned = yield @createForAchievement(achievement, trigger)
    if not earned
      console.error "Couldn't create achievement", achievement, trigger
      throw new errors.NotFound("Couldn't create achievement")
    return earned
    
    
EarnedAchievementSchema.statics.createForAchievement = co.wrap (achievement, doc, options={}) ->
  { previouslyEarnedAchievement, originalDocObj } = options
  
  User = require('./User')
  userObjectID = doc.get(achievement.get('userField'))
  userID = if _.isObject userObjectID then userObjectID.toHexString() else userObjectID # TODO: Migrate to ObjectIds

  earnedAttrs = {
    user: userID
    achievement: achievement._id.toHexString()
    achievementName: achievement.get 'name'
    earnedRewards: achievement.get 'rewards'
  }

  pointWorth = achievement.get('worth') ? 10
  gemWorth = achievement.get('rewards')?.gems ? 0
  earnedPoints = 0
  earnedGems = 0
  earnedDoc = null

  isRepeatable = achievement.get('proportionalTo')?

  if isRepeatable
    proportionalTo = achievement.get('proportionalTo')
    docObj = doc.toObject()
    newAmount = util.getByPath(docObj, proportionalTo) or 0

    ignoreDocObj = false
    if proportionalTo is 'simulatedBy' and newAmount > 0 and not previouslyEarnedAchievement and Math.random() < 0.1
      # Because things like simulatedBy get updated with $inc and not the post-save plugin hook,
      # we (infrequently) fetch the previously earned achievement so we can really update.
      previouslyEarnedAchievement = yield EarnedAchievement.findOne({user: earnedAttrs.user, achievement: earnedAttrs.achievement})
      ignoreDocObj = true # don't use the originalDocObj if the achievement wasn't made yet, for whatever reason 

    if previouslyEarnedAchievement
      originalAmount = previouslyEarnedAchievement.get('achievedAmount') or 0
    else if originalDocObj and not ignoreDocObj  # This branch could get buggy if unchangedCopy tracking isn't working.
      originalAmount = util.getByPath(originalDocObj, proportionalTo) or 0
    else
      originalAmount = 0

    if originalAmount isnt newAmount
      expFunction = achievement.getExpFunction()
      earnedAttrs.notified = false
      earnedAttrs.achievedAmount = newAmount
      earnedPoints = earnedAttrs.earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * pointWorth
      earnedGems = earnedAttrs.earnedGems = (expFunction(newAmount) - expFunction(originalAmount)) * gemWorth ? 0
      earnedAttrs.previouslyAchievedAmount = originalAmount
      yield EarnedAchievement.update({achievement: earnedAttrs.achievement, user: earnedAttrs.user}, earnedAttrs, {upsert: true})
      earnedDoc = new EarnedAchievement(earnedAttrs)

  else # not alreadyAchieved
    earnedAttrs.earnedPoints = pointWorth
    earnedAttrs.earnedGems = gemWorth
    earnedDoc = new EarnedAchievement(earnedAttrs)
    yield earnedDoc.save()
    earnedPoints = pointWorth
    earnedGems = gemWorth

  User.saveActiveUser(userID, "achievement")

  if earnedDoc
    update = {$inc: {points: earnedPoints, 'earned.gems': earnedGems}}
    for rewardType, rewards of achievement.get('rewards') ? {}
      continue if rewardType is 'gems'
      if rewards.length
        update.$addToSet ?= {}
        update.$addToSet["earned.#{rewardType}"] = $each: rewards
    yield User.update({_id: mongoose.Types.ObjectId(userID)}, update, {})

  return earnedDoc

module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema)
