/*
 * 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 async = require('async');
var bp = require('body-parser');
var cHelpers = require('../lib/helpers');
var debug = require('debug')('swagger-tools:middleware:metadata');
var mHelpers = require('./helpers');
var multer = require('multer');
var parseurl = require('parseurl');
var pathToRegexp = require('path-to-regexp');

// Upstream middlewares
var bodyParserOptions = {
  extended: false
};
var multerOptions = {
  storage: multer.memoryStorage()
};
var textBodyParserOptions = {
  type: '*/*'
};

var jsonBodyParser = bp.json();
var parseQueryString = mHelpers.parseQueryString;
var queryParser = function (req, res, next) {
  if (_.isUndefined(req.query)) {
    req.query = parseQueryString(req);
  }

  return next();
};
var realTextBodyParser = bp.text(textBodyParserOptions);
var textBodyParser = function (req, res, next) {
  if (_.isUndefined(req.body)) {
    realTextBodyParser(req, res, next);
  } else {
    next();
  }
};
var urlEncodedBodyParser = bp.urlencoded(bodyParserOptions);
var bodyParser = function (req, res, next) {
  if (_.isUndefined(req.body)) {
    urlEncodedBodyParser(req, res, function (err) {
      if (err) {
        next(err);
      } else {
        jsonBodyParser(req, res, next);
      }
    });
  } else {
    next();
  }
};
var realMultiPartParser = multer(multerOptions);
var makeMultiPartParser = function (parser) {
  return function (req, res, next) {
    if (_.isUndefined(req.files)) {
      parser(req, res, next);
    } else {
      next();
    }
  };
};

// Helper functions
var expressStylePath = function (basePath, apiPath) {
  basePath = parseurl({url: basePath || '/'}).pathname || '/';

  // Make sure the base path starts with '/'
  if (basePath.charAt(0) !== '/') {
    basePath = '/' + basePath;
  }

  // Make sure the base path ends with '/'
  if (basePath.charAt(basePath.length - 1) !== '/') {
    basePath = basePath + '/';
  }

  // Make sure the api path does not start with '/' since the base path will end with '/'
  if (apiPath.charAt(0) === '/') {
    apiPath = apiPath.substring(1);
  }

  // Replace Swagger syntax for path parameters with Express' version (All Swagger path parameters are required)
  return (basePath + apiPath).replace(/{/g, ':').replace(/}/g, '');
};

var processOperationParameters = function (swaggerMetadata, pathKeys, pathMatch, req, res, next) {
  var version = swaggerMetadata.swaggerVersion;
  var spec = cHelpers.getSpec(cHelpers.getSwaggerVersion(version === '1.2' ?
                                                         swaggerMetadata.resourceListing :
                                                         swaggerMetadata.swaggerObject), true);
  var parameters = !_.isUndefined(swaggerMetadata) ?
                     (version === '1.2' ? swaggerMetadata.operation.parameters : swaggerMetadata.operationParameters) :
                     undefined;

  if (!parameters) {
    return next();
  }

  debug('  Processing Parameters');

  var parsers = _.reduce(parameters, function (requestParsers, parameter) {
    var contentType = req.headers['content-type'];
    var paramLocation = version === '1.2' ? parameter.paramType : parameter.schema.in;
    var paramType = mHelpers.getParameterType(version === '1.2' ? parameter : parameter.schema);
    var parsableBody = mHelpers.isModelType(spec, paramType) || ['array', 'object'].indexOf(paramType) > -1;
    var parser;

    switch (paramLocation) {
      case 'body':
      case 'form':
      case 'formData':
        if (paramType.toLowerCase() === 'file' || (contentType && contentType.split(';')[0] === 'multipart/form-data')) {
          // Do not add a parser, multipart will be handled after
          break;
        } else if (paramLocation !== 'body' || parsableBody) {
          parser = bodyParser;
        } else {
          parser = textBodyParser;
        }

        break;

      case 'query':
        parser = queryParser;

        break;
    }

    if (parser && requestParsers.indexOf(parser) === -1) {
      requestParsers.push(parser);
    }

    return requestParsers;
  }, []);

  // Multipart is handled by multer, which needs an array of {parameterName, maxCount}
  var multiPartFields = _.reduce(parameters, function (fields, parameter) {
    var paramLocation = version === '1.2' ? parameter.paramType : parameter.schema.in;
    var paramType = mHelpers.getParameterType(version === '1.2' ? parameter : parameter.schema);
    var paramName = version === '1.2' ? parameter.name : parameter.schema.name;

    switch (paramLocation) {
      case 'body':
      case 'form':
      case 'formData':
        if (paramType.toLowerCase() === 'file') {
          // Swagger spec does not allow array of files, so maxCount should be 1
          fields.push({name: paramName, maxCount: 1});
        }
        break;
    }

    return fields;
  }, []);
  
  var contentType = req.headers['content-type'];
  if (multiPartFields.length) {
    // If there are files, use multer#fields
    parsers.push(makeMultiPartParser(realMultiPartParser.fields(multiPartFields)));
  } else if (contentType && contentType.split(';')[0] === 'multipart/form-data') {
    // If no files but multipart form, use empty multer#array for text fields
    parsers.push(makeMultiPartParser(realMultiPartParser.array()));
  }

  async.map(parsers, function (parser, callback) {
    parser(req, res, callback);
  }, function (err) {
    if (err) {
      return next(err);
    }

    _.each(parameters, function (parameterOrMetadata, index) {
      var parameter = version === '1.2' ? parameterOrMetadata : parameterOrMetadata.schema;
      var pLocation = version === '1.2' ? parameter.paramType : parameter.in;
      var pType = mHelpers.getParameterType(parameter);
      var oVal;
      var value;

      debug('    %s', parameter.name);
      debug('      Type: %s%s', pType, !_.isUndefined(parameter.format) ? ' (format: ' + parameter.format + ')': '');

      // Located here to make the debug output pretty
      oVal = mHelpers.getParameterValue(version, parameter, pathKeys, pathMatch, req, debug);
      value = mHelpers.convertValue(oVal, _.isUndefined(parameter.schema) ? parameter : parameter.schema, pType, pLocation);

      debug('      Value: %s', value);

      swaggerMetadata.params[parameter.name] = {
        path: version === '1.2' ?
                swaggerMetadata.operationPath.concat(['parameters', index.toString()]) :
                parameterOrMetadata.path,
        schema: parameter,
        originalValue: oVal,
        value: value
      };
    });

    return next();
  });
};
var processSwaggerDocuments = function (rlOrSO, apiDeclarations) {
  if (_.isUndefined(rlOrSO)) {
    throw new Error('rlOrSO is required');
  } else if (!_.isPlainObject(rlOrSO)) {
    throw new TypeError('rlOrSO must be an object');
  }

  var spec = cHelpers.getSpec(cHelpers.getSwaggerVersion(rlOrSO), true);
  var apiCache = {};
  var composeParameters = function (apiPath, method, path, operation) {
    var cParams = [];
    var seenParams = [];

    _.each(operation.parameters, function (parameter, index) {
      cParams.push({
        path: apiPath.concat([method, 'parameters', index.toString()]),
        schema: parameter
      });

      seenParams.push(parameter.name + ':' + parameter.in);
    });

    _.each(path.parameters, function (parameter, index) {
      if (seenParams.indexOf(parameter.name + ':' + parameter.in) === -1) {
        cParams.push({
          path: apiPath.concat(['parameters', index.toString()]),
          schema: parameter
        });
      }
    });

    return cParams;
  };
  var createCacheEntry = function (adOrSO, apiOrPath, indexOrName, indent) {
    var apiPath = spec.version === '1.2' ? apiOrPath.path : indexOrName;
    var expressPath = expressStylePath(adOrSO.basePath, spec.version === '1.2' ? apiOrPath.path: indexOrName);
    var keys = [];
    var handleSubPaths = !(rlOrSO.paths && rlOrSO.paths[apiPath]['x-swagger-router-handle-subpaths']);
    var re = pathToRegexp(expressPath, keys, { end: handleSubPaths });
    var cacheKey = re.toString();
    var cacheEntry;

    // This is an absolute path, use it as the cache key
    if (expressPath.indexOf('{') === -1) {
      cacheKey = expressPath;
    }

    debug(new Array(indent + 1).join(' ') + 'Found %s: %s',
          (spec.version === '1.2' ? 'API' : 'Path'),
          apiPath);

    cacheEntry = apiCache[cacheKey] = spec.version === '1.2' ?
      {
        api: apiOrPath,
        apiDeclaration: adOrSO,
        apiIndex: indexOrName,
        keys: keys,
        params: {},
        re: re,
        operations: {},
        resourceListing: rlOrSO
      } :
      {
        apiPath: indexOrName,
        path: apiOrPath,
        keys: keys,
        re: re,
        operations: {},
        swaggerObject: {
          original: rlOrSO,
          resolved: adOrSO
        }
      };

    return cacheEntry;
  };

  debug('  Identified Swagger version: %s', spec.version);

  if (spec.version === '1.2') {
    if (_.isUndefined(apiDeclarations)) {
      throw new Error('apiDeclarations is required');
    } else if (!_.isArray(apiDeclarations)) {
      throw new TypeError('apiDeclarations must be an array');
    }

    debug('  Number of API Declarations: %d', apiDeclarations.length);

    _.each(apiDeclarations, function (apiDeclaration, adIndex) {
      debug('  Processing API Declaration %d', adIndex);

      _.each(apiDeclaration.apis, function (api, apiIndex) {
        var cacheEntry = createCacheEntry(apiDeclaration, api, apiIndex, 4);

        cacheEntry.resourceIndex = adIndex;

        _.each(api.operations, function (operation, operationIndex) {
          cacheEntry.operations[operation.method.toLowerCase()] = {
            operation: operation,
            operationPath: ['apis', apiIndex.toString(), 'operations', operationIndex.toString()],
            operationParameters: operation.parameters
          };
        });
      });
    });
  } else {
    // To avoid running into issues with references throughout the Swagger object we will use the resolved version.
    // Getting the resolved version is an asynchronous process but since initializeMiddleware caches the resolved document
    // this is a synchronous action at this point.
    spec.resolve(rlOrSO, function (err, resolved) {
      // Gather the paths, their path regex patterns and the corresponding operations
      _.each(resolved.paths, function (path, pathName) {
        var cacheEntry = createCacheEntry(resolved, path, pathName, 2);

        _.each(['get', 'put', 'post', 'delete', 'options', 'head', 'patch'], function (method) {
          var operation = path[method];

          if (!_.isUndefined(operation)) {
            cacheEntry.operations[method] = {
              operation: operation,
              operationPath: ['paths', pathName, method],
              // Required since we have to compose parameters based on the operation and the path
              operationParameters: composeParameters(['paths', pathName], method, path, operation)
            };
          }
        });
      });
    });
  }

  return apiCache;
};

/**
 * Middleware for providing Swagger information to downstream middleware and request handlers.  For all requests that
 * match a Swagger path, 'req.swagger' will be provided with pertinent Swagger details.  Since Swagger 1.2 and 2.0
 * differ a bit, the structure of this object will change so please view the documentation below for more details:
 *
 *     https://github.com/apigee-127/swagger-tools/blob/master/docs/Middleware.md#swagger-metadata
 *
 * @param {object} rlOrSO - The Resource Listing (Swagger 1.2) or Swagger Object (Swagger 2.0)
 * @param {object[]} apiDeclarations - The array of API Declarations (Swagger 1.2)
 *
 * @returns the middleware function
 */
exports = module.exports = function (rlOrSO, apiDeclarations) {
  debug('Initializing swagger-metadata middleware');

  var apiCache = processSwaggerDocuments(rlOrSO, apiDeclarations);
  var swaggerVersion = cHelpers.getSwaggerVersion(rlOrSO);

  if (_.isUndefined(rlOrSO)) {
    throw new Error('rlOrSO is required');
  } else if (!_.isPlainObject(rlOrSO)) {
    throw new TypeError('rlOrSO must be an object');
  }

  if (swaggerVersion === '1.2') {
    if (_.isUndefined(apiDeclarations)) {
      throw new Error('apiDeclarations is required');
    } else if (!_.isArray(apiDeclarations)) {
      throw new TypeError('apiDeclarations must be an array');
    }
  }

  return function swaggerMetadata (req, res, next) {
    var method = req.method.toLowerCase();
    var path = parseurl(req).pathname;
    var cacheEntry;
    var match;
    var metadata;

    cacheEntry = apiCache[path] || _.find(apiCache, function (metadata) {
      match = metadata.re.exec(path);
      return _.isArray(match);
    });

    debug('%s %s', req.method, req.url);
    debug('  Is a Swagger path: %s', !_.isUndefined(cacheEntry));

    // Request does not match an API defined in the Swagger document(s)
    if (!cacheEntry) {
      return next();
    }

    metadata = swaggerVersion === '1.2' ?
      {
        api: cacheEntry.api,
        apiDeclaration: cacheEntry.apiDeclaration,
        apiIndex: cacheEntry.apiIndex,
        params: {},
        resourceIndex: cacheEntry.resourceIndex,
        resourceListing: cacheEntry.resourceListing
      } :
    {
      apiPath : cacheEntry.apiPath,
      path: cacheEntry.path,
      params: {},
      swaggerObject: cacheEntry.swaggerObject.resolved
    };

    if (_.isPlainObject(cacheEntry.operations[method])) {
      metadata.operation = cacheEntry.operations[method].operation;
      metadata.operationPath = cacheEntry.operations[method].operationPath;

      if (swaggerVersion === '1.2') {
        metadata.authorizations = metadata.operation.authorizations || cacheEntry.apiDeclaration.authorizations;
      } else {
        metadata.operationParameters = cacheEntry.operations[method].operationParameters;
        metadata.security = metadata.operation.security || metadata.swaggerObject.security || [];
      }
    }

    metadata.swaggerVersion = swaggerVersion;

    req.swagger = metadata;

    debug('  Is a Swagger operation: %s', !_.isUndefined(metadata.operation));

    if (metadata.operation) {
      // Process the operation parameters
      return processOperationParameters(metadata, cacheEntry.keys, match, req, res, next, debug);
    } else {
      return next();
    }
  };
};
