const
    util = require('util'),
    EventEmitter = require('events').EventEmitter,
    moment = require('moment'),
    Logger = require('../logs/clsLogger').clsLogger,
    DownloadManager = require('./DownloadManager'),
    //smgUtils = require('../utils/smgutils'),
    //SchedulesDB = require('../db/schedulesDB'),
    chalk = require('chalk'),
    _ = require('lodash');

const schedulesDownloader = class schedulesDownloader {
    constructor({db, config, utils,schedulesdb} = {}) {
        this.schedulesDB = schedulesdb;
        this.db = db;
        // this._sequelize = this.db._sequelize;
        // this.Schedules = this.db.Schedules;
        // this.Schedules_Details = this.db.Schedules_Details;
        this.sutils = utils;
        this.logger = new Logger();
        this.dm = new DownloadManager({options: {directory: utils.SchedulesSaveDir, speed: 0}, config, utils});
        this.dm.start();
        this.config = config;

        this.eventTypes = {
            onProgress: 'onProgress',
            onDownloadCompleted: 'onDownloadCompleted',
            onError: 'onError',
            onListCompleted: 'onListCompleted'
        };

        this.db.on(this.db.eventTypes.Schedules_Details_bulkInsert, async (arr) => {
            this.logger.DownloadSchedules('Bulk insert occured, we have ' + arr.length + ' schedule files');
            await  this.start();
        });
        this.dm.on(this.dm.eventTypes.DownloadCompleted, async (pageStatus, pageMessage, filename)=>{
            //console.log('File Download finished')
            if(pageStatus==200) {
                await this.schedulesDB.updateScheduleFileDownloadedStatus(1, filename);
                this.logger.DownloadSchedules(chalk.bgGreen(`${filename} is set Downloaded in signage.db`));
            }else{
                await this.schedulesDB.updateScheduleFileDownloadedStatus(0, filename);
            }
        });
        this.dm.on(this.dm.eventTypes.Error, (file) => {
            this.emit(this.eventTypes.onError, file);
        });
        this.dm.on(this.dm.eventTypes.Progress, (filename, percentage, currentStr, durationStr, speed, current, length) => {
            this.emit(this.eventTypes.onProgress, filename, percentage, currentStr, durationStr, speed, current, length);
        });
        this.dm.on(this.dm.eventTypes.QueueIsEmpty, () => {
            this.emit(this.eventTypes.onListCompleted);
        });
    }


    //todo: uncomment clearSchedules for production.
     async start() {
        if (!this.dm.checkTime()){
            this.logger.DownloadSchedules('Not a good time to download.');
            return;
        }
        this.dm.clearQue();
        let fileStatus = await this.checkFiles();
        let files = await this.downloadSchedules();
        // console.log('dur')
    }

    async checkDownloads() {
        let availableDownloadTime = await this.db.getAvailableDownloadTime();
        if (availableDownloadTime.length==0){
            this.logger.DownloadSchedules('database is empty and cannot take available download times, skipping');
            return;
        }
        this.dm.options.startTime = availableDownloadTime[0].startTime;
        this.dm.options.endTime = availableDownloadTime[0].endTime;
        this.dm.options.speed = availableDownloadTime[0].speedLimit;
        let now = new moment().utc(true);
        let today = Math.pow(2, now.weekday());
        try {
            let schedules = await (await this.schedulesDB.getSchedules());
            if (!schedules){
                this.logger.DownloadSchedules('schedules is empty');
                return [];
            }
            schedules = await schedules.where(c => !c.Schedules_Details.isDownloaded);

            let inactivePast = await schedules.where(c => now.isAfter(c.endDate) && !c.isActive);

            let activePast = await schedules.where(c => now.isAfter(c.endDate) && c.isActive);

            let inactiveFuture = await schedules.where(c => now.isBefore(c.startDate) && !c.isActive).orderBy(c => c.hasPriority);

            let beforeNow = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && c.startTime.isBefore(now));

            let activeFuture = await schedules.where(c => now.isBefore(c.startDate) && c.isActive).orderBy(c => c.hasPriority).thenBy(c => c.startDate);

            let activeCurrent = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive);

            let activeCurrentToday = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && now.isBetween(c.startTime, c.endTime, null, '[]') && c.isActive &&
                (c.daysEffected & today) == today);

            let hasPriority = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && c.hasPriority).orderBy(c => c.startTime);

            let afterNow = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && now.isBetween(c.startTime, c.endTime, null, '[]') && c.isActive &&
                (c.daysEffected & today) == today);

            let hasPriorityToday = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && c.hasPriority &&
                (c.daysEffected & today) == today && c.startTime.isAfter(now));

            let arrFiles = [];
            schedules = null;

            if(inactivePast.length > 0) {
                arrFiles.unshift(inactivePast);
            }
            inactivePast = null;
            if(activePast.length > 0) {
                arrFiles.unshift(activePast);
            }
            activePast = null;
            if(inactiveFuture.length > 0) {
                arrFiles.unshift(inactiveFuture);
            }
            inactiveFuture = null;
            if(beforeNow.length > 0) {
                arrFiles.unshift(beforeNow)
            }
            beforeNow = null;
            if(activeFuture.length > 0) {
                arrFiles.unshift(activeFuture);
            }
            activeFuture = null;
            if(activeCurrent.length > 0) {
                arrFiles.unshift(activeCurrent);
            }
            activeCurrent = null;
            if(activeCurrentToday.length > 0) {
                arrFiles.unshift(activeCurrentToday);
            }
            activeCurrentToday = null;
            if(hasPriority.length > 0) {
                arrFiles.unshift(hasPriority)
            }
            hasPriority = null;

            if(afterNow.length > 0) {
                arrFiles.unshift(afterNow);
            }
            if(hasPriorityToday.length > 0) {
                arrFiles.unshift(hasPriorityToday);
            }
            hasPriorityToday = null;
            afterNow = null;

            if (arrFiles != undefined && arrFiles != null && arrFiles.length > 0) {
                arrFiles = _.flattenDeep(arrFiles);
                let q  = [];
                await arrFiles.forEach(c => c.Schedules_Details.forEach(d => q.push(d)));
                q = await q.groupBy(c => c.FileName).select(c => c.first()).select(c => {
                    return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName, orderID: c.orderID}
                });
                this.logger.DownloadSchedules(`Found ${q.length} schedule files`);
                return q;
            } else {
                return [];
            }
        } catch(ex) {
            this.logger.DownloadSchedules(chalk.bgRed('Error fetching schedules files: ' + ex.message));
            return [];
        }
    }
    //todo: add active future if this method gonna be final decision.
    async checkDownloadsOld() {
        console.log('schedulelar basladi ' + new moment().utc(true).toJSON().substr(11, 19));
        let now = new moment().utc(true);
        let today = Math.pow(2, now.weekday());
        let arr = [];
        let schedules = await (await this.schedulesDB.getSchedules());
        // push files that are supposed to today, has priority and is after current time.
        let hasPriorityTodayAfter = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Schedules_Details.isDownloaded && c.hasPriority &&
            (c.daysEffected & today) == today && c.startTime.isAfter(now)).orderBy(c => c.startTime).forEach(c => c.Schedules_Details.forEach(d => arr.push(d)));
        hasPriorityTodayAfter = null;
        if (arr != undefined && arr.length > 0) {
            arr = await arr.groupBy(c => c.FileName).select(c => c.first()).select(c => {
                return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName}
            });
            console.log('priority bitti ' + new moment().utc(true).toJSON().substr(11, 19) + ' ' + arr.length + ' dosya buldum.');
            return arr;
        }
        // push schedules that are supposed to play today and after current time
        arr = [];
        let afterNow = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Schedules_Details.isDownloaded &&
            (c.daysEffected & today) == today && c.startTime.isAfter(now)).orderBy(c => c.startTime).forEach(c => c.Schedules_Details.forEach(d => arr.push(d)));
        afterNow = null;
        if (arr != undefined && arr.length > 0) {
            arr = await arr.groupBy(c => c.FileName).select(c => c.first()).select(c => {
                return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName}
            });
            return arr;
        }
        // push schedules with priority
        arr = [];
        let hasPriority = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Schedules_Details.isDownloaded && c.hasPriority)
            .orderBy(c => c.startDate).forEach(c => c.Schedules_Details.forEach(d => arr.push(d)));
        hasPriority = null;
        if (arr != undefined && arr.length > 0) {
            arr = await arr.groupBy(c => c.FileName).select(c => c.first()).select(c => {
                return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName}
            });
            return arr;
        }
        // push schedules where we are between start and end dates.
        arr = [];
        let activeFuture = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Schedules_Details.isDownloaded)
            .orderBy(c => c.startTime).forEach(c => c.Schedules_Details.forEach(d => arr.push(d)));
        activeFuture = null;
        if (arr != undefined && arr.length > 0) {
            arr = await arr.groupBy(c => c.FileName).select(c => c.first()).select(c => {
                return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName}
            });
            return arr;
        }
        // push schedules wjere we are between start and end dates but play time already passed.
        arr = [];
        let beforeNow = await schedules.where(c => now.isBetween(c.startDate, c.endDate, null, '[]') && c.isActive && !c.Schedules_Details.isDownloaded && c.startTime.isBefore(now))
            .orderBy(c => c.startTime).forEach(c => c.Schedules_Details.forEach(d => arr.push(d)));
        beforeNow = null;
        if (arr != undefined && arr.length > 0) {
            arr = await arr.groupBy(c => c.FileName).select(c => c.first()).select(c => {
                return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName}
            });
            return arr;
        }
        // push inactive future schedules
        arr = [];
        let inactiveFuture = await schedules.where(c => now.isBefore(c.startDate) && !c.isActive && !c.Schedules_Details.isDownloaded).orderBy(c => c.hasPriority).thenBy(c => c.startDate)
            .forEach(c => c.Schedules_Details.forEach(d => arr.push(d)));
        inactiveFuture = null;
        if (arr != undefined && arr.length > 0) {
            arr = await arr.groupBy(c => c.FileName).select(c => c.first()).select(c => {
                return {FileName: c.FileName, FileSize: c.FileSize, OriginalName: c.OriginalName}
            });
            return arr;
        }
    }

    async downloadSchedules() {
        let newSchedules = await this.checkDownloads();
        if (!newSchedules) {
            this.logger.DownloadSchedules('seems there is nothing to download');
            return;
        }
        let uniqueArray = await this.checkAlreadyHave(newSchedules);
        if (uniqueArray == undefined || uniqueArray.length == 0 || uniqueArray == null) return newSchedules;
        newSchedules = null;
        let downloadSize = uniqueArray.select(c => c.FileSize).sum();
        //clear que before sending new list
        this.dm.clearQue();
        this.dm.setTotalSize(downloadSize);
        //todo: uncomment below if we decide on queue rather than array
        // await uniqueArray.forEach(file => {
        //     let fname = file.FileName;
        //     this.dm.addFileToQue(fname)
        // });
        await this.dm.addArrayToQue(uniqueArray);
        this.logger.DownloadSchedules('We have ' + uniqueArray.length + ' unique schedule files to download.');
    }
    //todo: check local and db files, requeue files that aren't downloaded.
    // async downloadSkippedFiles() {
    //     this.dm.on(this.dm.eventTypes.QueueIsEmpty, () => {
    //         let arr = [];
    //         let fpath = this.sutils.SchedulesSaveDir;
    //         let databaseFiles = await (await (this.schedulesDB.getSchedules()));
    //         let localFiles = this.sutils.getFilesInfo(fpath);
    //         let remoteNames = databaseFiles.select((f) => {
    //             return f.FileName;
    //         });
    //         let localNames = localFiles.select((f) => {
    //             return f.file;
    //         });
    //
    //         arr = remoteNames.where((c) => {
    //             localNames.contains(c) == false
    //         });
    //         if(arr.length > 0) {
    //             arr.forEach(c => {
    //                 this.dm.addFileToQue(c)
    //             });
    //         }
    //         arr = null;
    //     });
    // }

    //todo: further fixing needed.
    async checkAlreadyHave(filesToCheck) {
        //get local files
        let fpath = this.sutils.SchedulesSaveDir;
        let localFiles = await (await this.sutils.getFilesInfo(fpath));
        //remove the files we have from download list
        if (localFiles == undefined || localFiles == null || localFiles.length == 0) {
            this.logger.DownloadSchedules('There are no schedule files in drive. Adding ' + filesToCheck.length + ' files to schedules downloader que.');
            return filesToCheck;
        }
        localFiles = await localFiles.select(c => {
            return { FileName: c.file, FileSize: c.size }
        });
        filesToCheck = await filesToCheck.select(c => {
            return { FileName: c.FileName, FileSize: c.FileSize }
        });
        //join files on db and files on local storage
        let join = filesToCheck.innerJoin(localFiles,
            (_filesToCheck) => {
                return _filesToCheck.FileName
            },
            (_localFiles) => {
                return _localFiles.FileName
            },
            (fc, lf) => {
                return {
                    dbFileName: fc.FileName,
                    dbFileSize: fc.FileSize,
                    localFileName: lf.FileName,
                    localFileSize: lf.FileSize
                }
            });
        //size and names are equal files, update db that they are isDownloaded=true
        let FilesWeAlreadyHave = join.where(c => c.dbFileSize == c.localFileSize);
        if (FilesWeAlreadyHave.length > 0) {
            FilesWeAlreadyHave.forEach(async c => {
                await this.schedulesDB.updateScheduleFileDownloadedStatus(true, c.dbFileName);
            });
        }
        //size not matches, find and delete them and set them isDownloaded=false
        let falseSize = join.where(c => c.dbFileSize != c.localFileSize);
        if (falseSize.length > 0) {
            falseSize.forEach(async c => {
                // added c.FileName because falseSize didnt have 'FileName' and update file download status and deleteFilesArray uses FileName.
                c.FileName = c.localFileName;
                await this.schedulesDB.updateScheduleFileDownloadedStatus(false, c.FileName);
            });
            this.sutils.deleteFilesArray(falseSize, fpath);
            this.logger.DownloadSchedules(`${falseSize.length} incomplete downloaded files were deleted from drive`);
        }
        //now get the filenames only from FilesWeAlreadyHave and remove them from original list and set them isDownloaded=false
        let fileNamesOnly = FilesWeAlreadyHave.select(c => c.dbFileName);
        let filesToDownload = filesToCheck.where(c => !fileNamesOnly.contains(c.FileName));
        if (filesToDownload != undefined && filesToDownload.length > 0) {
            filesToDownload.forEach(async c => {
               await this.schedulesDB.updateScheduleFileDownloadedStatus(false, c.FileName)
            });
            return filesToDownload;
        } else {
            return [];
        }
    }
    //todo: needs further fixes.
    async checkFiles() {
        let fpath = this.sutils.SchedulesSaveDir;
        let remoteFiles = await this.schedulesDB.getSchedules();
        if (remoteFiles == undefined || remoteFiles.length == 0 || remoteFiles == null) return [];
        let localFiles = await this.sutils.getFilesInfo(fpath);
        if (localFiles == undefined || localFiles.length == 0) return;
        let databaseFiles = [];
        await remoteFiles.select(c => c.Schedules_Details.forEach(d => databaseFiles.push(d)));
        databaseFiles = await databaseFiles.groupBy(c => c.FileName).select(c => c.first());
        remoteFiles = null;
        localFiles = await localFiles.select(c => {
            return { FileName: c.file, FileSize: c.size };
        });
        // join two tables for comparision
        let join = await databaseFiles.innerJoin(localFiles,
            (_databaseFiles) => {
                return _databaseFiles.FileName
            },
            (_localFiles) => {
                return _localFiles.FileName
            },
            (df, lf) => {
                return {
                    dbFileName: df.FileName,
                    dbFileSize: df.FileSize,
                    localFileName: lf.FileName,
                    localFileSize: lf.FileSize
                }
            });
        // filter, delete and set isDownloaded false files where sizes doesnt match with db.
        let falseSize = await join.where(c => c.dbFileSize != c.localFileSize);
        if(falseSize.length > 0) {
            falseSize = await falseSize.select(c => {
                return { FileName: c.localFileName }
            });
            falseSize.forEach(async c => {
                await this.schedulesDB.updateScheduleFileDownloadedStatus(false, c.FileName);
            });
            this.sutils.deleteFilesArray(falseSize, fpath);
            this.logger.DownloadSchedules(`${falseSize.length} incomplete downloaded files were deleted from drive`);
        }
        // filtering and deleting files that are in drive but not on db.
        // push db and local file names directly into array, not as objects
        let localNames = await localFiles.select(c => c.FileName);
        let dbFileNames = await databaseFiles.select(c => c.FileName);
        //compare 2 arrays and push local files where it doesnt appear in db file names.
        let noShow = await localNames.filter(c => !dbFileNames.contains(c));
        // convert filenames to objects as deleteFilesArray method works with objects.
        if (noShow.length > 0) {
            noShow = await noShow.select(c => {
                return { FileName: c}
            });
            this.sutils.deleteFilesArray(noShow, fpath);
            if (noShow){
                if (noShow.length>=0){
                    this.logger.System(chalk.blue(`${noShow.length} schedule files were deleted from drive as they were removed from newmanager.`));
                }
            }

        }
        // garbage collection
        noShow = null;
        falseSize = null;
        localFiles = null;
        databaseFiles = null;
    }
};

util.inherits(schedulesDownloader, EventEmitter);
module.exports = schedulesDownloader;