/*******************************************************************************
 * Copyright 2017 IBM Corp.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
'use strict';
var Probe = require('../lib/probe.js');
var aspect = require('../lib/aspect.js');
var request = require('../lib/request.js');
var util = require('util');
var url = require('url');
var am = require('../');
var semver = require('semver');

var methods;
// In Node.js < v8.0.0 'get' calls 'request' so we only instrument 'request'
if (semver.lt(process.version, '8.0.0')) {
  methods = ['request'];
} else {
  methods = ['request', 'get'];
}

// Probe to instrument outbound http requests

function HttpOutboundProbe() {
  Probe.call(this, 'http'); // match the name of the module we're instrumenting
}
util.inherits(HttpOutboundProbe, Probe);

function getRequestItems(options) {
  var returnObject = { requestMethod: 'GET', urlRequested: '', headers: '' };
  if (options !== null) {
    var parsedOptions;
    switch (typeof options) {
      case 'object':
        returnObject.urlRequested = formatURL(options);
        parsedOptions = options;
        break;
      case 'string':
        returnObject.urlRequested = options;
        parsedOptions = url.parse(options);
        break;
    }
    if (parsedOptions.method) { returnObject.requestMethod = parsedOptions.method; }
    if (parsedOptions.headers) { returnObject.headers = parsedOptions.headers; }
  }
  return returnObject;
}

HttpOutboundProbe.prototype.attach = function(name, target) {
  var that = this;
  if (name === 'http') {
    if (target.__outboundProbeAttached__) return target;
    target.__outboundProbeAttached__ = true;
    aspect.around(
      target,
      methods,
      // Before 'http.request' function
      function(obj, methodName, methodArgs, probeData) {
        // Start metrics
        that.metricsProbeStart(probeData);
        that.requestProbeStart(probeData);
        // End metrics
        aspect.aroundCallback(
          methodArgs,
          probeData,
          function(target, args, probeData) {
            // Get HTTP request method from options
            var ri = getRequestItems(methodArgs[0]);
            that.metricsProbeEnd(probeData, ri.requestMethod, ri.urlRequested, args[0], ri.headers);
            that.requestProbeEnd(probeData, ri.requestMethod, ri.urlRequested, args[0], ri.headers);
          },
          function(target, args, probeData, ret) {
            // Don't need to do anything after the callback
            return ret;
          }
        );
      },
      // After 'http.request' function returns
      function(target, methodName, methodArgs, probeData, rc) {
        // If no callback has been used then end the metrics after returning from the method instead
        if (aspect.findCallbackArg(methodArgs) === undefined) {
          // Need to get request method and URL again
          var ri = getRequestItems(methodArgs[0]);
          // End metrics (no response available so pass empty object)
          that.metricsProbeEnd(probeData, ri.requestMethod, ri.urlRequested, {}, ri.headers);
          that.requestProbeEnd(probeData, ri.requestMethod, ri.urlRequested, {}, ri.headers);
        }
        return rc;
      }
    );
  }
  return target;
};

// Get a URL as a string from the options object passed to http.get or http.request
// See https://nodejs.org/api/http.html#http_http_request_options_callback
function formatURL(httpOptions) {
  var url;
  if (httpOptions.protocol) {
    url = httpOptions.protocol;
  } else {
    url = 'http:';
  }
  url += '//';
  if (httpOptions.auth) {
    url += httpOptions.auth + '@';
  }
  if (httpOptions.host) {
    url += httpOptions.host;
  } else if (httpOptions.hostname) {
    url += httpOptions.hostname;
  } else {
    url += 'localhost';
  }
  if (httpOptions.port && !url.includes(':' + httpOptions.port)) {
    url += ':' + httpOptions.port;
  }
  if (httpOptions.path) {
    url += httpOptions.path;
  } else {
    url += '/';
  }
  return url;
}

/*
 * Lightweight metrics probe for HTTP requests
 *
 * These provide:
 *   time:            time event started
 *   method:          HTTP method, eg. GET, POST, etc
 *   url:             The url requested
 *   requestHeaders:  The HTTP headers for the request
 *   duration:        The time for the request to respond
 *   contentType:     HTTP content-type
 *   statusCode:      HTTP status code
 */
HttpOutboundProbe.prototype.metricsEnd = function(probeData, method, url, res, headers) {
  if (probeData && probeData.timer) {
    probeData.timer.stop();
    am.emit('http-outbound', {
      time: probeData.timer.startTimeMillis,
      method: method,
      url: url,
      duration: probeData.timer.timeDelta,
      statusCode: res.statusCode,
      contentType: res.headers ? res.headers['content-type'] : undefined,
      requestHeaders: headers,
    });
  }
};

/*
 * Heavyweight request probes for HTTP outbound requests
 */
HttpOutboundProbe.prototype.requestStart = function(probeData, method, url) {
  var reqType = 'http-outbound';
  // Do not mark as a root request
  probeData.req = request.startRequest(reqType, url, false, probeData.timer);
};

HttpOutboundProbe.prototype.requestEnd = function(probeData, method, url, res, headers) {
  if (probeData && probeData.req)
    probeData.req.stop({
      url: url,
      statusCode: res.statusCode,
      contentType: res.headers ? res.headers['content-type'] : undefined,
      requestHeaders: headers,
    });
};

module.exports = HttpOutboundProbe;
