<commit_before>
###*
 * Chunker is a data model? for structuring notifications and karma points.
 * It was created to facilitate aggregating items in chunks (ie notifications
 * by type, or karma points by source).
 * The Chunker methods handle creation and deletion (undoing) of these items,
 * as well as ... ?
###

# Documentation? HAH, you wish.

assert = require 'assert'
lodash = require 'lodash'
async = require 'async'
mongoose = require 'mongoose'
bunyan = require 'app/config/bunyan'
please = require 'app/lib/please'
TMERA = require 'app/lib/tmera'

User = mongoose.model 'User'

class Chunker

	logger = bunyan({ service: 'Chunker' })

	# @cfield is an array of ids of @itemModel objects (containing many @itemModel's),
	# the last of which is currently 'active' and holds the latest @itemModels.
	constructor: (@cfield, @chunkModel, @itemModel, @Types, @Handlers, @Generators, @aggregateTimeout=Infinity) ->
		@mname = @chunkModel.modelName
		for type of @Types
			assert typeof @Handlers[type].instance isnt 'undefined',
				'Handler for instance of '+@mname+' of type '+type+' is not registered.'
			assert typeof @Handlers[type].instance is 'function',
				'Handler for instance of '+@mname+' of type '+type+' is not a function.'
			assert typeof @Handlers[type].item isnt 'undefined',
				'Handler for item of '+@mname+' of type '+type+' is not registered.'
			assert typeof @Handlers[type].item is 'function',
				'Handler for item of '+@mname+' of type '+type+' is not a function.'

	# Fix a situtation when the last object the user chunk's id array doesn't exist.
	fixUserAndGetChunk: (user, cb) ->
		please {$model:'User'}, '$isFn'
		# Identify next logs.
		fixLogger = logger.child({ attemptFix: Math.ceil(Math.random()*100) })
		fixLogger.error('[0] User(%s) supposed chunk '+@mname+'(%s) doesn\'t exist. Attempting
			to fix it.', user._id, user[@cfield][user[@cfield].length-1])
		# Find chunks related to the user.
		@itemModel
			.find({ user: user._id })
			.sort('updated_at')
			.select('updated_at _id')
			.exec TMERA (docs) =>
				if docs.length
				# There are real chunks related to that user. These may or may not have
				# been in the @cfield array.
					fixLogger.error('[1] %s '+@chunk<EMAIL>Name+' found for user.', docs.length)
					# Update user's @cfield with correct data.
					update = { $set: {} }
					update.$set[@cfield] = lodash.pluck(docs, '_id')
					User.findOneAndUpdate { _id: user._id }, update, (err, udoc) =>
						if err
							fixLogger.error(err, '[3] Attempted fix: Failed to update '+@cfield+
								' for user(%s).', user._id)
							return
						fixLogger.error('[3] Attempted fix: Fixed '+@cfield+' attribute
							for user(%s).', user._id, udoc[@cfield])
						# Get the last chunk (all of it, now)
						@chunkModel.findOne { _id: docs[docs.length-1] }, (err, chunk) =>
							if err or not udoc
								throw err or new Error('Kill yourself. Really.')
							cb(null, chunk)
				else
				# No chunks related to the user exist? WTF
					fixLogger.error('[1] No '+@chunk<EMAIL>+' found for user. Creating.')
					newAttr = lodash.pluck(docs, '_id')
					self.createChunk user, false, (err, chunk) =>
						if err
							fixLogger.error(err, '2. Failed to create chunk for user(%s)', user._id)
							return cb(err)
						if not chunk
							throw new Error('WTF! created '+@chunkModel.<EMAIL>Name+' object is null')
						cb(null, chunk)

	createChunk: (user, push=false, cb) ->
		please {$model:'User'}, {$is:false}, '$isFn' # Non tested consequences for push=true
		logger.debug('Creating '+@chunkModel.modelName+' chunk for user %s', user._id)
		chunk = new @chunkModel {
			user: user._id
		}
		chunk.save (err, chunk) =>
			if err
				logger.error(err, 'Failed to create '+@chunkModel.modelName+' chunk for user(%s)',
					user._id)
				return cb(err)
			if push
				action = { $push: {} }
				action.$push[@cfield] = chunk._id
			else
				action = {}
				action[@cfield] = [chunk._id]
			User.findOneAndUpdate { _id: user._id }, action, (err) =>
				if err
					logger.error(err,
						'Failed to save '+@cfield+' (=%s) attribute to user (%s)',
						chunk._id, user._id)
			cb(null, chunk)

	getFromUser: (user, cb) ->
		please {$model:'User'}, '$isFn'
		self = @

		if user[@cfield] and user[@cfield].length
			latest = user[@cfield][user[@cfield].length-1]
			@chunkModel.findOne { _id: latest }, (err, chunk) =>
				if err
					logger.error(err, 'Failed finding '+@chunkModel.modelName+'(%s) for user(%s)',
						latest, user._id)
					throw err
				if chunk
					return cb(null, chunk)
				else
					# OPS! This shouldn't be happening.
					# Log as error and try to fix it.
					# Don't even try other ids in the @cfield field.
					self.fixUserAndGetChunk(user, cb)
		else
			# Chunks are to be created when they're needed for the first time.
			logger.debug("User (%s) has no "+@chunkModel.modelName+".", user._id)
			self.createChunk user, false, (err, chunk) =>
				if err
					logger.error(err, 'Failed to create chunk for user(%s)', user._id)
					return cb(err)
				if not chunk
					throw new Error('WTF! created '+@chunkModel.modelName+' object is null')
				cb(null, chunk)

	addItemToChunk: (item, chunk, cb) ->
		please {$model:@itemModel}, {$model:@chunkModel}, '$isFn'
		@chunkModel.findOneAndUpdate {
			_id: chunk._id
		}, {
			$push: 	{ items: item }
			$set: 	{	updated_at: Date.now() }
		}, TMERA (doc) ->
			cb(null, doc)

	aggregateInChunk: (item, latestItemData, instance, chunk, cb) ->
		please {$model:@itemModel}, '$skip', {$model:@chunkModel}, '$isFn'
		logger.debug("UPDATE", chunk._id, item)

		@chunkModel.findOneAndUpdate {
			_id: chunk._id
			# 'items.identifier': item.identifier
			'items._id': item._id
		}, {
			$set: {
				updated_at: Date.now()
				'items.$.object': latestItemData # Update object, just in case
			}
			'items.$.updated_at': Date.now()
			$inc: { 'items.$.multiplier': 1 }
			$push: { 'items.$.instances': instance }
		}, cb<commit_msg>Ops! Fixed in chunker<commit_after>
###*
 * Chunker is a data model? for structuring notifications and karma points.
 * It was created to facilitate aggregating items in chunks (ie notifications
 * by type, or karma points by source).
 * The Chunker methods handle creation and deletion (undoing) of these items,
 * as well as ... ?
###

# Documentation? HAH, you wish.

assert = require 'assert'
lodash = require 'lodash'
async = require 'async'
mongoose = require 'mongoose'
bunyan = require 'app/config/bunyan'
please = require 'app/lib/please'
TMERA = require 'app/lib/tmera'

User = mongoose.model 'User'

class Chunker

	logger = bunyan({ service: 'Chunker' })

	# @cfield is an array of ids of @itemModel objects (containing many @itemModel's),
	# the last of which is currently 'active' and holds the latest @itemModels.
	constructor: (@cfield, @chunkModel, @itemModel, @Types, @Handlers, @Generators, @aggregateTimeout=Infinity) ->
		@mname = @chunkModel.modelName
		for type of @Types
			assert typeof @Handlers[type].instance isnt 'undefined',
				'Handler for instance of '+@mname+' of type '+type+' is not registered.'
			assert typeof @Handlers[type].instance is 'function',
				'Handler for instance of '+@mname+' of type '+type+' is not a function.'
			assert typeof @Handlers[type].item isnt 'undefined',
				'Handler for item of '+@mname+' of type '+type+' is not registered.'
			assert typeof @Handlers[type].item is 'function',
				'Handler for item of '+@mname+' of type '+type+' is not a function.'

	# Fix a situtation when the last object the user chunk's id array doesn't exist.
	fixUserAndGetChunk: (user, cb) ->
		please {$model:'User'}, '$isFn'
		# Identify next logs.
		fixLogger = logger.child({ attemptFix: Math.ceil(Math.random()*100) })
		fixLogger.error('[0] User(%s) supposed chunk '+@mname+'(%s) doesn\'t exist. Attempting
			to fix it.', user._id, user[@cfield][user[@cfield].length-1])
		# Find chunks related to the user.
		@itemModel
			.find({ user: user._id })
			.sort('updated_at')
			.select('updated_at _id')
			.exec TMERA (docs) =>
				if docs.length
				# There are real chunks related to that user. These may or may not have
				# been in the @cfield array.
					fixLogger.error('[1] %s '+@chunkModel.modelName+' found for user.', docs.length)
					# Update user's @cfield with correct data.
					update = { $set: {} }
					update.$set[@cfield] = lodash.pluck(docs, '_id')
					User.findOneAndUpdate { _id: user._id }, update, (err, udoc) =>
						if err
							fixLogger.error(err, '[3] Attempted fix: Failed to update '+@cfield+
								' for user(%s).', user._id)
							return
						fixLogger.error('[3] Attempted fix: Fixed '+@cfield+' attribute
							for user(%s).', user._id, udoc[@cfield])
						# Get the last chunk (all of it, now)
						@chunkModel.findOne { _id: docs[docs.length-1] }, (err, chunk) =>
							if err or not udoc
								throw err or new Error('Kill yourself. Really.')
							cb(null, chunk)
				else
				# No chunks related to the user exist? WTF
					fixLogger.error('[1] No '+@chunkModel.modelName+' found for user. Creating.')
					newAttr = lodash.pluck(docs, '_id')
					self.createChunk user, false, (err, chunk) =>
						if err
							fixLogger.error(err, '2. Failed to create chunk for user(%s)', user._id)
							return cb(err)
						if not chunk
							throw new Error('WTF! created '+@chunkModel.modelName+' object is null')
						cb(null, chunk)

	createChunk: (user, push=false, cb) ->
		please {$model:'User'}, {$is:false}, '$isFn' # Non tested consequences for push=true
		logger.debug('Creating '+@chunkModel.modelName+' chunk for user %s', user._id)
		chunk = new @chunkModel {
			user: user._id
		}
		chunk.save (err, chunk) =>
			if err
				logger.error(err, 'Failed to create '+@chunkModel.modelName+' chunk for user(%s)',
					user._id)
				return cb(err)
			if push
				action = { $push: {} }
				action.$push[@cfield] = chunk._id
			else
				action = {}
				action[@cfield] = [chunk._id]
			User.findOneAndUpdate { _id: user._id }, action, (err) =>
				if err
					logger.error(err,
						'Failed to save '+@cfield+' (=%s) attribute to user (%s)',
						chunk._id, user._id)
			cb(null, chunk)

	getFromUser: (user, cb) ->
		please {$model:'User'}, '$isFn'
		self = @

		if user[@cfield] and user[@cfield].length
			latest = user[@cfield][user[@cfield].length-1]
			@chunkModel.findOne { _id: latest }, (err, chunk) =>
				if err
					logger.error(err, 'Failed finding '+@chunkModel.modelName+'(%s) for user(%s)',
						latest, user._id)
					throw err
				if chunk
					return cb(null, chunk)
				else
					# OPS! This shouldn't be happening.
					# Log as error and try to fix it.
					# Don't even try other ids in the @cfield field.
					self.fixUserAndGetChunk(user, cb)
		else
			# Chunks are to be created when they're needed for the first time.
			logger.debug("User (%s) has no "+@chunkModel.modelName+".", user._id)
			self.createChunk user, false, (err, chunk) =>
				if err
					logger.error(err, 'Failed to create chunk for user(%s)', user._id)
					return cb(err)
				if not chunk
					throw new Error('WTF! created '+@chunkModel.modelName+' object is null')
				cb(null, chunk)

	addItemToChunk: (item, chunk, cb) ->
		please {$model:@itemModel}, {$model:@chunkModel}, '$isFn'
		@chunkModel.findOneAndUpdate {
			_id: chunk._id
		}, {
			$push: 	{ items: item }
			$set: 	{	updated_at: Date.now() }
		}, TMERA (doc) ->
			cb(null, doc)

	aggregateInChunk: (item, latestItemData, instance, chunk, cb) ->
		please {$model:@itemModel}, '$skip', '$skip', {$model:@chunkModel}, '$isFn'
		logger.debug("UPDATE", chunk._id, item)

		@chunkModel.findOneAndUpdate {
			_id: chunk._id
			# 'items.identifier': item.identifier
			'items._id': item._id
		}, {
			$set: {
				updated_at: Date.now()
				'items.$.object': latestItemData # Update object, just in case
			}
			'items.$.updated_at': Date.now()
			$inc: { 'items.$.multiplier': 1 }
			$push: { 'items.$.instances': instance }
		}, cb
{'fraction_non_alphanumeric': 0.1499206548066483, 'fraction_numerical': 0.002171552660152009, 'mean_word_length': 3.358937022206043, 'pattern_counts': {'":': 0, '<': 6, '<?xml version=': 0, '>': 36, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1752209', 'n_tokens_mistral': 4763, 'n_tokens_neox': 3955, 'n_words': 1255}