text: /* 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 action...'
          );
  
          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);
  };
  