/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
// {
//   	"jobId":"uuid",
// 	"status":"UNKNOWN | STARTING | STARTED | COMPLETED | FAILED",
// 	"progressDetails": {	 
// 	  "totalWorkAmount":1000,
//       "remainingWork":0,
// 	  "estimatedTimeToCompletionInMillis":0
// 	}
//   	"validVoteCount":1000,
//   	"invalidVoteCount":0,
// 	"errorCount":0
// }
// [1:14:33 PM] Antonio Dias: ---- DECRYPTION ------
// {
//   	"jobId":"uuid",
// 	"status":"UNKNOWN | STARTING | STARTED | COMPLETED | FAILED",
// 	"progressDetails": {
// 	  "totalWorkAmount":1000,
//       "remainingWork":0,
// 	  "estimatedTimeToCompletionInMillis":0
// 	}
//   	"auditableVotesCount":1000,
//   	"decryptedVotesCount":0,
// 	"errorCount":0
// }
// [1:17:46 PM] Antonio Dias: ---- VOTING CARD GENERATION -----
// {
//   	"jobId":"uuid",
// 	"status":"UNKNOWN | STARTING | STARTED | COMPLETED | FAILED",
// 	"progressDetails": {
// 	  "totalWorkAmount":1000,
//       "remainingWork":0,
// 	  "estimatedTimeToCompletionInMillis":0
// 	}
// 	"verificationCardSetId":"uuid_2"
//   	"generatedCount":1000,
//   	"errorCount":0
// }


(function() {
    'use strict';

    angular.module('jobqueue', [])

    .factory('jobqueue', function(_, $http, $interval, $timeout, endpoints, $rootScope) {

        var poll_interval = null;
        var poll_interval_ms = 3000;

        var jobs = {};

        var batchCount = 0;
        var batchSummary = {};
        var batchSummaryErrorCallback = null;
        var batchSummarySuccessCallback = null;
        var batchesTotals = {};

        // add a new job to the queue

        var job = function(objid, jobid, type, alias, callback) {

            // add to jobqueue (will replace any existing job for this same obj)

            jobs[objid] = {
                objid: objid,
                jobid: jobid,
                type: type,
                alias: alias,
                callback: callback,
                batch: batchCount,
                status: {
                    queued: true,
                    processing: false,
                    completed: false,
                    error: false,
                    pct: 0,
                    eta: 0,
                    itmeCount: 0,
                    itmeCount2: 0,
                    errorCount: 0,
                    startTS: 0,
                    endTS: 0
                }
            };

            // keep track of 'batches' of jobs so we can give error feedback per batch

            if (!batchSummary[batchCount]) {
                batchSummary[batchCount] = {
                    batch: batchCount,
                    type: type,
                    total: 1,
                    ok: 0,
                    err: 0,
                    errors: [],
                    startTS: new Date().getTime(),
                    remainingMS: 0,
                    completedPct: 0

                };
            } else {
                batchSummary[batchCount].total++;
            }

            // start polling status

            if (!poll_interval) {
                poll_interval = $interval(update_queue, poll_interval_ms);
            }
        };


        // signal a new batch number

        // note both callbacks are 'static', i.e.: every invocation of 'batch'
        // will override the callbacks. 

        var batch = function(summarySuccessCallback, summaryErrorCallback) {
            batchCount++;
            batchSummaryErrorCallback = summaryErrorCallback;
            batchSummarySuccessCallback = summarySuccessCallback;
        };

        // get progress info for a job (by object id)

        var getJobStatus = function(objid) {

            var job = jobs[objid];
            return (job && job.status) ? job.status : undefined;
        };

        // get batches
        // @param types String|Array of String|nothing
        // @return array of batches (possibly empty)

        var getBatches = function(types) {

            if (types && typeof types === 'string') {
                types = [types];
            }
            var batches = _.filter(batchSummary, function(b) {
                return types ? _.findIndex(types, function(t) {
                    return t == b.type;
                }) >= 0 : true;
            });
            return (batches || []);
        };

        // get grand totals of batches
        // @param types String|Array of String|nothing
        // @return array of batchesTotals (possibly empty)

        var getBatchesTotals = function(types) {

            if (types && typeof types === 'string') {
                types = [types];
            }
            var bt = _.filter(batchesTotals, function(b) {
                return types ? _.findIndex(types, function(t) {
                    return t == b.type;
                }) >= 0 : true;
            });
            return (bt || []);
        };

        // update progress info for a batch
        // (simple approach based on measurement of completed jobs)

        var update_batch = function(batch) {

            var elapsedMS = new Date().getTime() - batch.startTS;
            var completed = batch.ok + batch.err;
            var avgMS = completed ? elapsedMS / completed : 0;
            var remainingMS = (batch.total - completed) * avgMS;
            var totalEstimatedMS = elapsedMS + remainingMS;
            var completedPct = completed ? Math.ceil(elapsedMS * 100 / totalEstimatedMS) : 0;

            batch.remainingMS = remainingMS;
            batch.completedPct = completedPct;

        };

        // update progress info for a batch
        // (considering spring-batch estimates for jobs in progress)

        var update_batch_estimate = function(batch) {

            var avgJobMS = 0,
                activeJobs = 0,
                activeJobsMS = 0,
                completedJobs = 0,
                completedJobsMS = 0,
                completedPctSum = 0,
                batchStartTS = batch.startTS,
                queuedJobs = 0,
                maxEta = 0;

            // collect job info

            _.filter(jobs, {
                batch: batch.batch
            }).forEach(function(j) {

                if (j.status.endTS) { // job completed

                    if (j.status.startTS && j.status.startTS < batchStartTS) batchStartTS = j.status.startTS;
                    completedJobsMS += (j.status.endTS - j.status.startTS);
                    completedJobs++;
                    completedPctSum += 100;

                } else if (j.status.eta > 0) { // job in progress

                    if (j.status.startTS && j.status.startTS < batchStartTS) batchStartTS = j.status.startTS;
                    activeJobsMS += (new Date().getTime() - j.status.startTS + j.status.eta);
                    activeJobs++;
                    if (j.status.eta > maxEta) {
                        maxEta = j.status.eta;
                    }
                    completedPctSum += j.status.pct;

                } else { // queued

                    queuedJobs++;
                }

            });

            // compute global progress %

            batch.completedPct = batch.total ? completedPctSum / batch.total : 0;

            // estimate remaining time for the batch

            if (!queuedJobs && activeJobs === 1) {

                // if queue is empty and there is only one job, eta is simply the job's eta

                batch.remainingMS = maxEta;

            } else {

                // else, eta is a proportion of total elapsed time and total % complete up to now

                var totalElapsed = new Date().getTime() - batchStartTS;
                batch.remainingMS = Math.ceil(batch.completedPct ? totalElapsed * 100 / batch.completedPct - totalElapsed : 0);

            }

            batch.totalItems = batch.total;
            batch.processedItems = batch.err + batch.ok;

            // batch log
            $rootScope.log('b;' +
                batch.type + batch.batch + ';' +
                batch.remainingMS + ';' +
                batch.completedPct + ';' +
                queuedJobs
            );



        };


        // poll status for all active jobs in the queue

        var update_queue = function() {

            var active_jobs = _.filter(jobs, function(j) {
                return !j.status.completed && !j.status.error;
            });

            // still active jobs ?

            if (!active_jobs.length) {

                // nope, cancel status polling 

                $interval.cancel(poll_interval);
                poll_interval = null;
                batchSummary = {};
                batchesTotals = {};

            } else {

                // query status for each job type

                var active_types = _.groupBy(active_jobs, 'type');

                _.forOwn(active_types, function(value, type) {

                    var endpoint = endpoints.host + endpoints.progress_bulk.replace('{type}', type);
                    $http.get(endpoint).then(

                        function(job_info_response) {
                            try {

                                // update status for each returned job

                                job_info_response.data.forEach(function(jobinfo) {
                                    var job = _.find(jobs, {
                                        jobid: jobinfo.jobId
                                    });
                                    if (job) {
                                        update_progress(job, jobinfo);
                                    }
                                });

                            } catch (e) {
                                console.log('unparseable job status response');
                            }

                            // check error notifications for each 'batch' (group of jobs of same interaction)

                            batch_summary();

                            // update consolidated progress of batches 

                            var bt = {};
                            _.each(batchSummary, function(batch) {

                                // estimate progress info for this batch
                                update_batch_estimate(batch); // update_batch(batch);

                                // add up totals for all batches
                                if (!bt[batch.type]) {
                                    bt[batch.type] = {
                                        type: batch.type,
                                        total: 0,
                                        processed: 0
                                    };
                                }
                                bt[batch.type].total += batch.totalItems;
                                bt[batch.type].processed += batch.processedItems;
                            });
                            batchesTotals = bt;

                        },
                        function(error_response) {
                            console.log('job status request error', error_response);
                        });
                });

            }

            /* */

        };

        // update progress info for one job

        var update_progress = function(job, job_info) {

            // status

            switch (job_info.status) {
                case 'UNKNOWN':
                    job.status.queued = false;
                    job.status.processing = true;
                    job.status.completed = false;
                    job.status.error = false;
                    break;
                case 'STARTING':
                case 'STARTED':
                    job.status.queued = false;
                    job.status.processing = true;
                    job.status.completed = false;
                    job.status.error = false;
                    setStartTS(job);
                    break;
                case 'COMPLETED':
                    job.status.queued = false;
                    job.status.processing = false;
                    job.status.error = false;
                    if (!job.status.completed) {
                        if (batchSummary[job.batch]) {
                            batchSummary[job.batch].ok++;
                        }
                        job.status.completed = true;
                    }
                    setEndTS(job);
                    break;
                case 'FAILED':
                    job.status.queued = false;
                    job.status.processing = false;
                    job.status.completed = true;
                    if (!job.status.error) {
                        if (batchSummary[job.batch]) {
                            batchSummary[job.batch].err++;
                            batchSummary[job.batch].errors.push(job.alias);
                        }
                        job.status.error = true;
                    }
                    setEndTS(job);
                    break;
                default:
                    console.log('unexpected job status: ' + job_info.status);
                    break;
            };

            // completion %

            try {
                var remaining = parseInt(job_info.progressDetails.remainingWork, 10);
                var total = parseInt(job_info.progressDetails.totalWorkAmount, 10);
                job.status.pct = Math.abs((total - remaining) * 100 / total);
                if (job.status.pct > 100) {
                    job.status.pct = 100;
                }
            } catch (e) {
                job.status.pct = 0;
            }

            // eta

            try {
                job.status.eta = parseInt(job_info.progressDetails.estimatedTimeToCompletionInMillis, 10);
            } catch (ignore) {
                job.status.eta = 0;
            }

            // items && error count
            // every job returns these counts with different properties
            // we just test what properties are present and infer the job type,
            // then populate our itemCount, itemCount2 and errorCount

            job.status.itemCount = 0;
            job.status.itemCount2 = 0;
            job.status.errorCount = 0;

            // voting card generation

            if (job_info.generatedCount !== undefined) {

                try {
                    job.status.itemCount = parseInt(job_info.generatedCount, 10);
                    job.status.errorCount = parseInt(job_info.errorCount, 10);
                } catch (ignore) {}
            }

            // cleansing

            if (job_info.validVoteCount !== undefined) {
                try {
                    job.status.itemCount = parseInt(job_info.validVoteCount, 10);
                    job.status.itemCount2 = parseInt(job_info.invalidVoteCount, 10);
                    job.status.errorCount = parseInt(job_info.errorCount, 10);
                } catch (ignore) {}
            };

            // decryption

            if (job_info.auditableVotesCount !== undefined) {
                try {
                    job.status.itemCount = parseInt(job_info.auditableVotesCount, 10);
                    job.status.itemCount2 = parseInt(job_info.decryptedVotesCount, 10);
                    job.status.errorCount = parseInt(job_info.errorCount, 10);
                } catch (ignore) {}
            };

            // if the job just completed, call the callback if supplied

            if (job.status.completed) {
                if (job.callback && typeof job.callback === 'function') {
                    try {
                        job.callback(job.alias);
                        job.callback = null;
                    } catch (ignore) {}
                }
            }

            $rootScope.log('j;' +
                job.alias + ';' +
                job.status.eta + ';' +
                job.status.pct  + ';' +
                job_info.status
            );

        };

        // set the start time stamp for a job

        var setStartTS = function(job) {

            if (job && job.status && !job.status.startTS) {
                job.status.startTS = new Date().getTime();
            }

        };

        // set the start time stamp for a job

        var setEndTS = function(job) {

            if (job && job.status && !job.status.endTS) {
                job.status.endTS = new Date().getTime();
            }

        };

        // check for complete batches

        var batch_summary = function() {

            _.each(batchSummary, function(b, k, bs) {

                // batch complete?

                if (b.total <= b.ok + b.err) {

                    // anything went wrong?

                    if (b.err && batchSummaryErrorCallback && typeof batchSummaryErrorCallback === 'function') {

                        batchSummaryErrorCallback(b.type, _.uniq(b.errors.sort()));

                    }

                    // report what went well:

                    if (b.ok > 0 && batchSummarySuccessCallback && typeof batchSummarySuccessCallback === 'function') {
                        batchSummarySuccessCallback(b.type, b.ok);
                    }

                    // we're done with this batch

                    delete bs[k];
                }

            });

        };

        // export

        return {
            job: job,
            batch: batch,
            getJobStatus: getJobStatus,
            getBatches: getBatches,
            getBatchesTotals: getBatchesTotals
        };

    });

})();
