/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Apigee Corporation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

'use strict';

var _ = require('lodash');
var cHelpers = require('../lib/helpers');
var debug = require('debug')('swagger-tools:middleware:router');
var fs = require('fs');
var mHelpers = require('./helpers');
var path = require('path');

var defaultOptions = {
  controllers: {},
  useStubs: false // Should we set this automatically based on process.env.NODE_ENV?
};
var getHandlerName = function (req) {
  var handlerName;

  switch (req.swagger.swaggerVersion) {
  case '1.2':
    handlerName = req.swagger.operation.nickname;
    break;

  case '2.0':
    if (req.swagger.operation['x-swagger-router-controller'] || req.swagger.path['x-swagger-router-controller']) {
      handlerName = (req.swagger.operation['x-swagger-router-controller'] ?
        req.swagger.operation['x-swagger-router-controller'] :
        req.swagger.path['x-swagger-router-controller']) + '_' +
        (req.swagger.operation.operationId ? req.swagger.operation.operationId : req.method.toLowerCase());
    } else {
      handlerName = req.swagger.operation.operationId;
    }

    break;
  }

  return handlerName;
};

var handlerCacheFromDir = function (dirOrDirs) {
  var handlerCache = {};
  var jsFileRegex = /\.(coffee|js|ts)$/;
  var dirs = [];

  if (_.isArray(dirOrDirs)) {
    dirs = dirOrDirs;
  } else {
    dirs.push(dirOrDirs);
  }

  debug('  Controllers:');

  _.each(dirs, function (dir) {
    _.each(fs.readdirSync(dir), function (file) {
      var controllerName = file.replace(jsFileRegex, '');
      var controller;

      if (file.match(jsFileRegex)) {
        controller = require(path.resolve(path.join(dir, controllerName)));

        debug('    %s%s:',
              path.resolve(path.join(dir, file)),
              (_.isPlainObject(controller) ? '' : ' (not an object, skipped)'));

        if (_.isPlainObject(controller)) {
          _.each(controller, function (value, name) {
            var handlerId = controllerName + '_' + name;

            debug('      %s%s',
                  handlerId,
                  (_.isFunction(value) ? '' : ' (not a function, skipped)'));

            // TODO: Log this situation

            if (_.isFunction(value) && !handlerCache[handlerId]) {
              handlerCache[handlerId] = value;
            }
          });
        }
      }
    });
  });

  return handlerCache;
};
var getMockValue = function (version, schema) {
  var type = _.isPlainObject(schema) ? schema.type : schema;
  var value;

  if (!type) {
    type = 'object';
  }

  switch (type) {
  case 'array':
    value = [getMockValue(version, _.isArray(schema.items) ? schema.items[0] : schema.items)];

    break;

  case 'boolean':
    if (version === '1.2' && !_.isUndefined(schema.defaultValue)) {
      value = schema.defaultValue;
    } else if (version === '2.0' && !_.isUndefined(schema.default)) {
      value = schema.default;
    } else if (_.isArray(schema.enum)) {
      value = schema.enum[0];
    } else {
      value = 'true';
    }

    // Convert value if necessary
    value = value === 'true' || value === true ? true : false;

    break;

  case 'file':
  case 'File':
    value = 'Pretend this is some file content';

    break;

  case 'integer':
    if (version === '1.2' && !_.isUndefined(schema.defaultValue)) {
      value = schema.defaultValue;
    } else if (version === '2.0' && !_.isUndefined(schema.default)) {
      value = schema.default;
    } else if (_.isArray(schema.enum)) {
      value = schema.enum[0];
    } else {
      value = 1;
    }

    // Convert value if necessary
    if (!_.isNumber(value)) {
      value = parseInt(value, 10);
    }

    // TODO: Handle constraints and formats

    break;

  case 'object':
    value = {};

    _.each(schema.allOf, function (parentSchema) {
      _.each(parentSchema.properties, function (property, propName) {
        value[propName] = getMockValue(version, property);
      });
    });

    _.each(schema.properties, function (property, propName) {
      value[propName] = getMockValue(version, property);
    });

    break;

  case 'number':
    if (version === '1.2' && !_.isUndefined(schema.defaultValue)) {
      value = schema.defaultValue;
    } else if (version === '2.0' && !_.isUndefined(schema.default)) {
      value = schema.default;
    } else if (_.isArray(schema.enum)) {
      value = schema.enum[0];
    } else {
      value = 1.0;
    }

    // Convert value if necessary
    if (!_.isNumber(value)) {
      value = parseFloat(value);
    }

    // TODO: Handle constraints and formats

    break;

  case 'string':
    if (version === '1.2' && !_.isUndefined(schema.defaultValue)) {
      value = schema.defaultValue;
    } else if (version === '2.0' && !_.isUndefined(schema.default)) {
      value = schema.default;
    } else if (_.isArray(schema.enum)) {
      value = schema.enum[0];
    } else {
      if (schema.format === 'date') {
        value = new Date().toISOString().split('T')[0];
      } else if (schema.format === 'date-time') {
        value = new Date().toISOString();
      } else {
        value = 'Sample text';
      }
    }

    break;
  }

  return value;
};
var mockResponse = function (req, res, next, handlerName) {
  var method = req.method.toLowerCase();
  var operation = req.swagger.operation;
  var sendResponse = function (err, response) {
    if (err) {
      debug('next with error: %j', err);
      return next(err);
    } else {
      debug('send mock response: %s', response);

      // Explicitly set the response status to 200 if not present (Issue #269)
      if (_.isUndefined(req.statusCode)) {
        res.statusCode = 200;
      }

      // Mock mode only supports JSON right now
      res.setHeader('Content-Type', 'application/json');

      return res.end(response);
    }
  };
  var spec = cHelpers.getSpec(req.swagger.swaggerVersion);
  var stubResponse = 'Stubbed response for ' + handlerName;
  var apiDOrSO;
  var responseType;

  switch (req.swagger.swaggerVersion) {
  case '1.2':
    apiDOrSO = req.swagger.apiDeclaration;
    responseType = operation.type;

    break;

  case '2.0':
    apiDOrSO = req.swagger.swaggerObject;

    if (method === 'post' && operation.responses['201']) {
      responseType = operation.responses['201'];

      res.statusCode = 201;
    } else if (method === 'delete' && operation.responses['204']) {
      responseType = operation.responses['204'];

      res.statusCode = 204;
    } else if (operation.responses['200']) {
      responseType = operation.responses['200'];
    } else if (operation.responses['default']) {
      responseType = operation.responses['default'];
    } else {
      responseType = 'void';
    }

    break;
  }

  if (_.isPlainObject(responseType) || mHelpers.isModelType(spec, responseType)) {
    if (req.swagger.swaggerVersion === '1.2') {
      spec.composeModel(apiDOrSO, responseType, function (err, result) {
        if (err) {
          return sendResponse(undefined, err);
        } else {
          // Should we handle this differently as undefined typically means the model doesn't exist
          return sendResponse(undefined, _.isUndefined(result) ?
                                           stubResponse :
                                           JSON.stringify(getMockValue(req.swagger.swaggerVersion, result)));
        }
      });
    } else {
      return sendResponse(undefined, JSON.stringify(getMockValue(req.swagger.swaggerVersion, responseType.schema || responseType)));
    }
  } else {
    return sendResponse(undefined, getMockValue(req.swagger.swaggerVersion, responseType));
  }
};
var createStubHandler = function (req, res, next, handlerName) {
  // TODO: Handle headers for 2.0
  // TODO: Handle examples (per mime-type) for 2.0
  // TODO: Handle non-JSON response types

  return function stubHandler (req, res, next) {
    mockResponse(req, res, next, handlerName);
  };
};

var send405 = function (req, res, next) {
  var allowedMethods = [];
  var err = new Error('Route defined in Swagger specification (' +
                        (_.isUndefined(req.swagger.api) ? req.swagger.apiPath : req.swagger.api.path) +
                        ') but there is no defined ' +
                      (req.swagger.swaggerVersion === '1.2' ? req.method.toUpperCase() : req.method.toLowerCase()) + ' operation.');

  if (!_.isUndefined(req.swagger.api)) {
    _.each(req.swagger.api.operations, function (operation) {
      allowedMethods.push(operation.method.toUpperCase());
    });
  } else {
    _.each(req.swagger.path, function (operation, method) {
      if (cHelpers.swaggerOperationMethods.indexOf(method.toUpperCase()) !== -1) {
        allowedMethods.push(method.toUpperCase());
      }
    });
  }

  err.allowedMethods = allowedMethods;

  res.setHeader('Allow', allowedMethods.sort().join(', '));
  res.statusCode = 405;

  return next(err);
};

/**
 * Middleware for using Swagger information to route requests to handlers.  Due to the differences between Swagger 1.2
 * and Swagger 2.0, the way in which your Swagger document(s) are annotated to work with this middleware differs as well
 * so please view the documentation below for more details:
 *
 *     https://github.com/apigee-127/swagger-tools/blob/master/docs/Middleware.md#swaggerrouteroptions
 *
 * This middleware also requires that you use the swagger-metadata middleware before this middleware.  This middleware
 * also makes no attempt to work around invalid Swagger documents.  If you would like to validate your requests using
 * the swagger-validator middleware, you must use it prior to using this middleware.
 *
 * @param {object} [options] - The middleware options
 * @param {(string|object|string[]} [options.controllers=./controllers] - If this is a string or string array, this is
 *                                                                        the path, or paths, to find the controllers
 *                                                                        in.  If it's an object, the keys are the
 *                                                                        controller "name" (as described above) and the
 *                                                                        value is a function.
 * @param {boolean} [options.useStubs=false] - Whether or not to stub missing controllers and methods
 *
 * @returns the middleware function
 */
exports = module.exports = function (options) {
  var handlerCache = {};

  debug('Initializing swagger-router middleware');

  // Set the defaults
  options = _.defaults(options || {}, defaultOptions);

  debug('  Mock mode: %s', options.useStubs === true ? 'enabled' : 'disabled');

  if (_.isPlainObject(options.controllers)) {
    debug('  Controllers:');

    // Create the handler cache from the passed in controllers object
    _.each(options.controllers, function (func, handlerName) {
      debug('    %s', handlerName);

      if (!_.isFunction(func)) {
        throw new Error('options.controllers values must be functions');
      }
    });

    handlerCache = options.controllers;
  } else {
    // Create the handler cache from the modules in the controllers directory
    handlerCache = handlerCacheFromDir(options.controllers);
  }

  return function swaggerRouter (req, res, next) {
    var operation = req.swagger ? req.swagger.operation : undefined;
    var handler;
    var handlerName;
    var rErr;

    debug('%s %s', req.method, req.url);
    debug('  Will process: %s', _.isUndefined(operation) ? 'no' : 'yes');

    if (req.swagger) {
      if (operation) {
        handlerName = getHandlerName(req);
        handler = handlerCache[handlerName];

        req.swagger.useStubs = options.useStubs;

        debug('  Route handler: %s', handlerName);
        debug('    Missing: %s', _.isUndefined(handler) ? 'yes' : 'no');
        debug('    Ignored: %s', options.ignoreMissingHandlers === true ? 'yes' : 'no');
        debug('    Using mock: %s', options.useStubs && _.isUndefined(handler) ? 'yes' : 'no');

        if (_.isUndefined(handler) && options.useStubs === true) {
          handler = handlerCache[handlerName] = createStubHandler(handlerName);
        }

        if (!_.isUndefined(handler)) {
          try {
            return handler(req, res, next);
          } catch (err) {
            rErr = err;

            debug('Handler threw an unexpected error: %s\n%s', err.message, err.stack);
          }
        } else if (options.ignoreMissingHandlers !== true) {
          rErr = new Error('Cannot resolve the configured swagger-router handler: ' + handlerName);

          res.statusCode = 500;
        }
      } else {
        debug('  No handler for method: %s', req.method);

        return send405(req, res, next);
      }
    }

    if (rErr) {
      mHelpers.debugError(rErr, debug);
    }

    return next(rErr);
  };
};
