let openapiUtil = require('../util/openapi');
let request = require('request');
let Action = require('./action');
let Response = require('./response');
let rssParser = require('rss-parser');
let zlib = require('zlib');
const ZLIB_OPTIONS = {
  flush: zlib.Z_SYNC_FLUSH,
  finishFlush: zlib.Z_SYNC_FLUSH
}

const BODY_METHODS = ['put', 'patch', 'post'];

const getActionFromOperation = module.exports = function(method, path, openapi, integration, modifyReq) {
  let op = openapi.paths[path][method];
  let params = op.parameters || [];
  let hasRequiredParam = !!params.filter(p => p.required).length;
  let inputSchema = {
    type: hasRequiredParam ? 'object' : ['object', 'null'],
    properties: {},
    additionalProperties: false,
    definitions: openapi.definitions,
  };
  let names = openapiUtil.getUniqueNames(params);
  params.forEach((param, idx) => {
    let name = names[idx];
    inputSchema.properties[name] = getSchemaFromParam(param);
    if (param.required) {
      inputSchema.required = inputSchema.required || [];
      inputSchema.required.push(name);
    }
  });
  let response = getDefaultResponse(op);
  let outputSchema = Object.assign({definitions: openapi.definitions}, response.schema);
  let actionSecurity = {};
  if (!op.security || op.security.length) {
    actionSecurity = integration.security;
  } else {
    actionSecurity[integration.id] = false;
  }
  return new Action({
    title: op.operationId || (method.toUpperCase() + ' ' + path),
    description: op.description || op.summary,
    inputSchema: params.length ? inputSchema : {},
    outputSchema: outputSchema,
    security: actionSecurity,
    ajv: integration.ajv,
    handler: function(input, ctx) {
      input = input || {};
      let account = ctx.accounts[integration.id];
      let scheme = openapiUtil.getBestScheme(openapi.schemes);
      if (!openapi.host && (!account || !account.host)) {
        throw new Error("The 'host' field must be specified in the " + integration.id + " account");
      }
      let url = (account && account.host) || (scheme + '://' + openapi.host);
      let reqOpts = {
        method,
        url,
        qs: {},
        qsStringifyOptions: {},
        headers: {},
        form: {},
        formData: {},
        body: null,
        encoding: null,
      }
      if (openapi.basePath && openapi.basePath !== '/') reqOpts.url += openapi.basePath;
      reqOpts.url += path;
      let addParam = (loc, name, val, isFile) => {
        if (val === undefined) return;
        if (loc === 'query') {
          reqOpts.qs[name] = val;
        } else if (loc === 'header') {
          reqOpts.headers[name] = val;
        } else if (loc === 'path') {
          reqOpts.url = reqOpts.url.replace('{' + name + '}', val);
        } else if (loc === 'body') {
          reqOpts.body = JSON.stringify(val);
        } else if (loc === 'formData') {
          if (isFile) {
            if (typeof val === 'object') {
              let content = val.encoding ? new Buffer(val.content, val.encoding) : val.content;
              val = {
                value: content,
                options: {
                  filename: val.filename || name,
                  contentType: val.contentType,
                  knownLength: val.knownLength,
                }
              }
            } else {
              val = {
                value: val,
                options: {
                  filename: name,
                }
              }
            }
            reqOpts.formData[name] = val;
          } else {
            reqOpts.form[name] = val;
          }
        }
      }
      let names = openapiUtil.getUniqueNames(params);
      params.forEach((param, idx) => {
        let val = input[names[idx]];
        if (param.in === 'query' && Array.isArray(val)) {
          if (param.collectionFormat === 'multi') {
            reqOpts.qsStringifyOptions.arrayFormat = 'repeat';
          } else {
            reqOpts.qsStringifyOptions.sep = openapiUtil.getCollectionFormatSeparator(param.collectionFormat);
          }
        }
        addParam(param.in, param.name, val, param.type === 'file');
      });

      let hasRefreshToken = false;
      let oauthDef = null;
      if (account) {
        for (let key in openapi.securityDefinitions || {}) {
          let def = openapi.securityDefinitions[key];
          if (def.type === 'basic' && account.username && account.password) {
            let details = account.username + ':' + account.password;
            addParam('header', 'Authorization', "Basic " + new Buffer(details, 'utf8').toString('base64'));
          } else if (def.type === 'apiKey' && account[key]) {
            addParam(def.in, def.name, account[key]);
          } else if (def.type === 'oauth2' && account.access_token) {
            hasRefreshToken = !!account.refresh_token;
            if (!oauthDef || oauthDef.flow === 'implicit') {
              oauthDef = def;
            }
            if (def.in) {
              addParam(def.in, def.name, account.access_token);
            } else {
              addParam('header', 'Authorization', "Bearer " + account.access_token);
            }
          }
        }
      }

      if (Object.keys(reqOpts.formData).length === 0) {
        delete reqOpts.formData;
      }
      if (Object.keys(reqOpts.form).length === 0) {
        delete reqOpts.form;
      }

      let refreshOAuthToken = (callback) => {
        let form = {
          client_id: account.client_id,
          client_secret: account.client_secret,
          refresh_token: account.refresh_token,
          grant_type: 'refresh_token'
        };
        request.post({
          url: account.refresh_url || oauthDef.refreshUrl || oauthDef.tokenUrl,
          headers: account.refresh_headers || {},
          json: true,
          form,
        }, (err, resp, body) => {
          if (err) return callback(err);
          if (resp.statusCode >= 300) return callback(new Error(resp.statusCode));
          account.access_token = body.access_token;
          account.refresh_token = body.refresh_token || account.refresh_token;
          Action.callOAuthRefreshCallbacks(account);
          addParam('header', 'Authorization', "Bearer " + body.access_token);
          callback();
        })
      }
      if (BODY_METHODS.indexOf(method) !== -1 && reqOpts.body !== null) {
        let consumes = op.consumes || ['application/json'];
        let cType = consumes.indexOf('application/json') === -1 ? consumes[0] : 'application/json';
        addParam('header', 'Content-Type', cType);
      }

      addParam('header', 'User-Agent', 'DataFire');

      let sendRequest = (resolve, reject, isRetry) => {
        request(reqOpts, (err, resp, body) => {
          if (err) {
            return reject(err);
          }
          if (body) {
            let encoding = resp.headers['content-encoding'];
            if (encoding === 'gzip') {
              body = zlib.gunzipSync(body, ZLIB_OPTIONS).toString('utf8');
            } else if (encoding === 'deflate') {
              body = zlib.inflateSync(body, ZLIB_OPTIONS).toString('utf8');
            } else {
              body = body.toString('utf8');
            }
          }
          if (!isRetry && resp.statusCode === 401 && hasRefreshToken) {
            refreshOAuthToken(err => {
              if (err) reject(new Response({statusCode: 401}));
              else sendRequest(resolve, reject, true);
            })
            return;
          } else if (resp.statusCode >= 300) {
            return reject(new Response({statusCode: resp.statusCode, body}));
          }
          let ctype = resp.headers['content-type'] || '';
          if (body && ctype.indexOf('application/json') !== -1) {
            body = JSON.parse(body);
            resolve(body);
          } else if (openapi.info['x-datafire'] && openapi.info['x-datafire'].type === 'rss') {
            rssParser.parseString(body, (err, feed) => {
              if (err) reject(err);
              else resolve(feed);
            })
          } else {
            resolve(body);
          }
        })
      }
      if (modifyReq) modifyReq(reqOpts, ctx);
      return new Promise(sendRequest);
    }
  });
}

const getSchemaFromParam = function(param) {
  if (param.in === 'body' && param.schema) return param.schema;
  let schema = {
    type: param.type,
  };
  if (param.type === 'file') {
    schema.type = ['string', 'object'];
    schema.properties = {
      content: {type: 'string'},
      encoding: {type: 'string', enum: ['ascii', 'utf8', 'utf16le', 'base64', 'binary', 'hex']},
      contentType: {type: 'string'},
      filename: {type: 'string'},
    }
  }
  schema.type = param.type === 'file' ? ['string', 'object'] : param.type;
  openapiUtil.PARAM_SCHEMA_FIELDS.forEach(f => {
    if (param[f] !== undefined) schema[f] = param[f];
  })
  return schema;
}

const getDefaultResponse = function(op) {
  let keys = Object.keys(op.responses).sort();
  return op.responses[keys[0]];
}

