// Require dependencies
import sirv from 'sirv';
import polka from 'polka';
import config from 'config';
import multer from 'multer';
import session from 'express-session';
import redirect from '@polka/redirect';
import bodyParser from 'body-parser';
import polkaCompat from 'polka-compat';
import responseTime from 'response-time';
import cookieParser from 'cookie-parser';
import SessionStore from '@edenjs/session-store';
import { v4 as uuid } from 'uuid';

// Require helpers
const aclHelper = helper('user/acl');

// Require Eden dependencies
import view from './view';

/**
 * Create Router class
 */
export default class EdenRouter {
  /**
   * Construct Router class
   */
  constructor(eden) {
    // set eden
    this.eden = eden;

    // Set variables
    this.app = null;
    this.multer = null;

    // Bind methods
    this.build = this.build.bind(this);

    // Bind private methods
    this.apiAction = this.apiAction.bind(this);
    this.initAction = this.initAction.bind(this);
    this.errorAction = this.errorAction.bind(this);

    // Bind super private methods
    this.buildRoute = this.buildRoute.bind(this);
    this.buildUpload = this.buildUpload.bind(this);

    // Run build
    this.building = this.build();
  }


  // /////////////////////////////////////////////////////////////////////////////////////////////
  //
  //  Build Methods
  //
  // /////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * Build Router
   *
   * @async
   */
  async build() {
    // Log building to debug
    this.eden.logger.log('debug', 'building eden router', {
      class : this.constructor.name,
    });

    // create app
    this.app = polka({
      onError : this.errorAction
    });

    // create express app
    await this.eden.hook('eden.router.app', this.app, () => {
      // Create server
      return this.app.listen(this.eden.get('config.port'));
    });

    // Log built to debug
    this.eden.logger.log('info', `[${this.eden.get('config.host')}:${this.eden.get('config.port')}] server listening`, {
      class : this.constructor.name,
    });

    // Set multer
    this.multer = multer(config.get('upload') || {
      dest : '/tmp',
    });

    // hook multer
    await this.eden.hook('eden.router.multer', this.multer);

    // Loop HTTP request types
    ['use', 'get', 'post', 'push', 'delete', 'all'].forEach((type) => {
      // Create HTTP request method
      this[type] = (...typeArgs) => {
        // Call express HTTP request method
        this.app[type](...typeArgs);
      };
    });

    // create initial route
    this.app.use(polkaCompat());
    this.app.use(this.initAction);
    this.app.use('api', this.apiAction);

    // Run eden app hook
    await this.eden.hook('eden.app', this.app, () => {
      // initialize
      SessionStore.initialize(session);

      // Add express middleware
      this.app.use(responseTime());
      this.app.use(cookieParser(config.get('secret')));
      this.app.use(bodyParser.json({
        limit : config.get('upload.limit') || '50mb',
      }));
      this.app.use(bodyParser.urlencoded({
        limit    : config.get('upload.limit') || '50mb',
        extended : true,
      }));
      this.app.use(sirv(`${global.appRoot}/www`, {
        maxAge : config.get('cache.age') || 31536000, // 1Y
      }));
      this.app.use(session({
        key   : config.get('session.key') || 'eden.session.id',
        genid : uuid,
        store : new SessionStore({
          eden : this.eden,
        }),
        secret : config.get('secret'),
        resave : true,
        cookie : config.get('session.cookie') || {
          maxAge   : (24 * 60 * 60 * 1000),
          secure   : false,
          httpOnly : false,
        },
        proxy             : true,
        saveUninitialized : true,
      }));
    });
    
    // Set router classes
    const controllers = Object.keys(this.eden.get('controllers') || {});

    // Run eden routes hook
    await this.eden.hook('eden.router.controllers', controllers, async () => {
      // Loop router classes
      for (const key of controllers) {
        // Load controller
        await this.eden.init(this.eden.get(`controllers.${key}`));
      }
    });

    // Sort routes
    const routes = [];

    // add routes
    controllers.forEach((controller) => {
      // push routes
      routes.push(...(this.eden.get(`controllers.${controller}.routes`).map((route) => {
        // return clone
        return {
          ctrl : controller,
          ...route,
        };
      })));
    });

    // sort routes
    routes.sort((a, b) => {
      // check priority
      if ((a.priority || 0) < (b.priority || 0)) return 1;
      if ((a.priority || 0) > (b.priority || 0)) return -1;

      // return no change
      return 0;
    });

    // Run eden routes hook
    await this.eden.hook('eden.router.routes', routes, async () => {
      // create route map
      for (const route of routes) {
        // add to router
        this.app[route.method.toLowerCase()](route.path, ...(await this.buildRoute(route)));
      }
    });

    // Log built to debug
    this.eden.logger.log('debug', 'completed building eden router', {
      class : this.constructor.name,
    });
  }

  /**
   * builds route from eden config
   *
   * @param route 
   */
  async buildRoute(route) {
    // Set path
    let { path, method } = route;

    // no path
    if (!path || !method) return;

    // set args
    const args = [];

    // Run route args hook
    await this.eden.hook('eden.router.route', {
      args,
      path,
      route,
    }, async () => {
      // Check upload
      const upload = this.buildUpload(route);

      // Push upload to args
      if (upload) args.push(upload);

      // Add actual route
      args.push(async (req, res, next) => {
        // EDEN ROUTE METHOD
        req.path  = path;
        req.route = route;

        // Set title
        if (route.title) req.title(route.title);

        // Run acl middleware
        const aclCheck = await aclHelper.middleware(req, res, route);

        // Check acl result
        if (aclCheck === 0) {
          // Run next
          return next();
        } if (aclCheck === 2) {
          // Return
          return null;
        }

        // Try catch
        try {
          // Get controller
          const ctrl = await this.eden.get(`controller.${route.ctrl}`);

          // Try run controller function
          return ctrl[route.fn](req, res, next);
        } catch (e) {
          // Set error
          this.eden.error(e);

          // Run next
          return next();
        }
      });
    });

    // return array
    return args;
  }

  /**
   * builds upload from eden config
   *
   * @param route 
   */
  buildUpload(route) {
    // Add upload middleware
    if (route.method !== 'post') return false;

    // Set type
    const upType = (route.upload && route.upload.type) || 'array';

    // Set middle
    let upApply = [];

    // Check type
    if (route.upload && (route.upload.fields || route.upload.name)) {
      if (upType === 'array') {
        // Push name array
        upApply.push(route.upload.name);
      } else if (upType === 'single') {
        // Push single name
        upApply.push(route.upload.name);
      } else if (upType === 'fields') {
        // Push fields
        upApply = route.upload.fields;
      }
    }

    // Check if any
    if (upType === 'any') upApply = [];

    // Create upload middleware
    return this.multer[upType](...upApply);
  }


  // /////////////////////////////////////////////////////////////////////////////////////////////
  //
  //  Action Methods
  //
  // /////////////////////////////////////////////////////////////////////////////////////////////

  /**
   * initialize eden route action
   *
   * @param req 
   * @param res 
   * @param next 
   */
  initAction(req, res, next) {
    // add headers
    req.set    = res.set = req.header = res.header = res.setHeader; // simple header set methods
    res.head   = ''; // to add to head tag
    res.foot   = ''; // to add post body tag
    res.page   = {}; // page variables
    res.style  = ''; // to add to style tag
    res.class  = ''; // body class
    res.locals = {}; // middleware variables for rendering

    // status
    res.status = (status) => {
      res.__status = status;
    };

    // replace send
    const send = res.send;
    res.send = (data, ...args) => {
      // status
      if (res.__status) {
        // send
        send(data, res.__status);
      } else {
        // send default
        send(data, ...args);
      }
    };

    // Set header
    res.header('X-Powered-By', 'EdenJS');

    // create render function
    res.render = async (...args) => {
      // set view
      if (typeof args[0] !== 'string') args.unshift(req.route.view);

      // render
      res.header('Content-Type', 'text/html; charset=utf-8');
      res.end(await view.render({ req, res, next }, ...args));
    };
    res.json = async (data) => {
      // send json
      res.header('Content-Type', 'application/json; charset=utf-8');

      // send
      res.end(JSON.stringify(data));
    };
    res.redirect = (url) => {
      redirect(res, url);
    };

    // Set isJSON request
    req.isJSON = res.isJSON = (req.headers.accept || req.headers.Accept || '').includes('application/json');

    // create timer
    req.timer = {
      start : new Date().getTime(),
      route : new Date().getTime(),
    };

    // Check is JSON
    if (req.isJSON) {
      // Set header
      res.header('Content-Type', 'application/json');
    }

    // Run next
    next();
  }

  /**
   * API action
   *
   * @param req 
   * @param res 
   * @param next 
   */
  apiAction(req, res, next) {
    // Set headers
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');

    // Run next
    next();
  }

  /**
   * Use error handler
   *
   * @param {express} app
   *
   * @private
   */
  async errorAction(err, req, res) {
    // check JSON
    if (req.isJSON) {
      // send json
      res.header('Content-Type', 'application/json; charset=utf-8');
    } else {
      // send HTML
      res.header('Content-Type', 'text/html; charset=utf-8');
    }

    // render
    res.statusCode = err.code;
    res.end(await view.render({ req, res }, 'error', {
      message : err.message || '404 page not found',
    }));
  }
}
