/* jslint node: true */
'use strict';

//  ENiGMA½
const PluginModule = require('./plugin_module.js').PluginModule;
const Config = require('./config.js').get;
const Log = require('./logger.js').log;
const { Errors } = require('./enig_error.js');

const _ = require('lodash');
const later = require('@breejs/later');
const path = require('path');
const pty = require('node-pty');
const sane = require('sane');
const moment = require('moment');
const paths = require('path');
const fse = require('fs-extra');

exports.getModule = EventSchedulerModule;
exports.EventSchedulerModule = EventSchedulerModule; //  allow for loadAndStart

exports.moduleInfo = {
    name: 'Event Scheduler',
    desc: 'Support for scheduling arbritary events',
    author: 'NuSkooler',
};

const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/;
const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/;

class ScheduledEvent {
    constructor(events, name) {
        this.name = name;
        this.schedule = this.parseScheduleString(events[name].schedule);
        this.action = this.parseActionSpec(events[name].action);
        if (this.action) {
            this.action.args = events[name].args || [];
        }
    }

    get isValid() {
        if (
            !this.schedule ||
            (!this.schedule.sched && !this.schedule.watchFile) ||
            !this.action
        ) {
            return false;
        }

        if ('method' === this.action.type && !this.action.location) {
            return false;
        }

        return true;
    }

    parseScheduleString(schedStr) {
        if (!schedStr) {
            return false;
        }

        let schedule = {};

        const m = SCHEDULE_REGEXP.exec(schedStr);
        if (m) {
            schedStr = schedStr.substr(0, m.index).trim();

            if ('@watch:' === m[1]) {
                schedule.watchFile = m[2];
            }
        }

        if (schedStr.length > 0) {
            const sched = later.parse.text(schedStr);
            if (-1 === sched.error) {
                schedule.sched = sched;
            }
        }

        //  return undefined if we couldn't parse out anything useful
        if (!_.isEmpty(schedule)) {
            return schedule;
        }
    }

    parseActionSpec(actionSpec) {
        if (actionSpec) {
            if ('@' === actionSpec[0]) {
                const m = ACTION_REGEXP.exec(actionSpec);
                if (m) {
                    if (m[2].indexOf(':') > -1) {
                        const parts = m[2].split(':');
                        return {
                            type: m[1],
                            location: parts[0],
                            what: parts[1],
                        };
                    } else {
                        return {
                            type: m[1],
                            what: m[2],
                        };
                    }
                }
            } else {
                return {
                    type: 'execute',
                    what: actionSpec,
                };
            }
        }
    }

    executeAction(reason, cb) {
        Log.info(
            { eventName: this.name, action: this.action, reason: reason },
            `Executing scheduled event "${this.name}"...`
        );

        if ('method' === this.action.type) {
            const modulePath = path.join(__dirname, '../', this.action.location); //  enigma-bbs base + supplied location (path/file.js')
            try {
                const methodModule = require(modulePath);
                methodModule[this.action.what](this.action.args, err => {
                    if (err) {
                        Log.debug(
                            {
                                error: err.message,
                                eventName: this.name,
                                action: this.action,
                            },
                            'Error performing scheduled event action'
                        );
                    }

                    return cb(err);
                });
            } catch (e) {
                Log.warn(
                    { error: e.message, eventName: this.name, action: this.action },
                    'Failed to perform scheduled event action'
                );

                return cb(e);
            }
        } else if ('execute' === this.action.type) {
            const opts = {
                //  :TODO: cwd
                name: this.name,
                cols: 80,
                rows: 24,
                env: process.env,
            };

            let proc;
            try {
                proc = pty.spawn(this.action.what, this.action.args, opts);
            } catch (e) {
                Log.warn({
                    error: 'Failed to spawn @execute process',
                    reason: e.message,
                    eventName: this.name,
                    action: this.action,
                    what: this.action.what,
                    args: this.action.args,
                });
                return cb(e);
            }

            proc.once('exit', exitCode => {
                if (exitCode) {
                    Log.warn(
                        { eventName: this.name, action: this.action, exitCode: exitCode },
                        'Bad exit code while performing scheduled event action'
                    );
                }
                return cb(
                    exitCode
                        ? Errors.ExternalProcess(
                              `Bad exit code while performing scheduled event action: ${exitCode}`
                          )
                        : null
                );
            });
        }
    }
}

function EventSchedulerModule(options) {
    PluginModule.call(this, options);

    const config = Config();
    if (_.has(config, 'eventScheduler')) {
        this.moduleConfig = config.eventScheduler;
    }

    const self = this;
    this.runningActions = new Set();

    this.performAction = function (schedEvent, reason) {
        if (self.runningActions.has(schedEvent.name)) {
            return; //  already running
        }

        self.runningActions.add(schedEvent.name);

        schedEvent.executeAction(reason, () => {
            self.runningActions.delete(schedEvent.name);
        });
    };
}

//  convienence static method for direct load + start
EventSchedulerModule.loadAndStart = function (cb) {
    const loadModuleEx = require('./module_util.js').loadModuleEx;

    const loadOpts = {
        name: path.basename(__filename, '.js'),
        path: __dirname,
    };

    loadModuleEx(loadOpts, (err, mod) => {
        if (err) {
            return cb(err);
        }

        const modInst = new mod.getModule();
        modInst.startup(err => {
            return cb(err, modInst);
        });
    });
};

EventSchedulerModule.prototype.startup = function (cb) {
    this.eventTimers = [];
    const self = this;

    if (this.moduleConfig && _.has(this.moduleConfig, 'events')) {
        const events = Object.keys(this.moduleConfig.events).map(name => {
            return new ScheduledEvent(this.moduleConfig.events, name);
        });

        events.forEach(schedEvent => {
            if (!schedEvent.isValid) {
                Log.warn({ eventName: schedEvent.name }, 'Invalid scheduled event entry');
                return;
            }

            Log.debug(
                {
                    eventName: schedEvent.name,
                    schedule: this.moduleConfig.events[schedEvent.name].schedule,
                    action: schedEvent.action,
                    next: schedEvent.schedule.sched
                        ? moment(
                              later.schedule(schedEvent.schedule.sched).next(1)
                          ).format('ddd, MMM Do, YYYY @ h:m:ss a')
                        : 'N/A',
                },
                'Scheduled event loaded'
            );

            if (schedEvent.schedule.sched) {
                this.eventTimers.push(
                    later.setInterval(() => {
                        self.performAction(schedEvent, 'Schedule');
                    }, schedEvent.schedule.sched)
                );
            }

            if (schedEvent.schedule.watchFile) {
                const watcher = sane(paths.dirname(schedEvent.schedule.watchFile), {
                    glob: `**/${paths.basename(schedEvent.schedule.watchFile)}`,
                });

                //  :TODO: should track watched files & stop watching @ shutdown?

                ['change', 'add', 'delete'].forEach(event => {
                    watcher.on(event, (fileName, fileRoot) => {
                        const eventPath = paths.join(fileRoot, fileName);
                        if (schedEvent.schedule.watchFile === eventPath) {
                            self.performAction(schedEvent, `Watch file: ${eventPath}`);
                        }
                    });
                });

                fse.exists(schedEvent.schedule.watchFile, exists => {
                    if (exists) {
                        self.performAction(
                            schedEvent,
                            `Watch file: ${schedEvent.schedule.watchFile}`
                        );
                    }
                });
            }
        });
    }

    cb(null);
};

EventSchedulerModule.prototype.shutdown = function (cb) {
    if (this.eventTimers) {
        this.eventTimers.forEach(et => et.clear());
    }

    cb(null);
};
