const https = require('https')
const http = require('http')
const ProxyAgent = require('proxy-agent')
const qs = require('querystring')
const fs = require('fs')
const Readable = require('stream').Readable
const FormData = require('form-data')
const Attachment = require('./attachment')
const retry = require('async').retry
const promisifyCall = require('promisify-call')

const debug = require('debug')('mailgun-js')

function isOk (i) {
  return typeof i !== 'undefined' && i !== null
}

function getDataValue (key, input) {
  if (isSpecialParam(key) && (typeof input === 'object')) {
    return JSON.stringify(input)
  } else if (typeof input === 'number' || typeof input === 'boolean') {
    return input.toString()
  }

  return input
}

function isSpecialParam (paramKey) {
  const key = paramKey.toLowerCase()

  return ((key === 'vars' || key === 'members' || key === 'recipient-variables') || (key.indexOf('v:') === 0))
}

function isMultiUnsubsribe (path, data) {
  return path.indexOf('/unsubscribes') && data && Array.isArray(data)
}

function prepareData (data) {
  const params = {}

  for (const key in data) {
    if (key !== 'attachment' && key !== 'inline' && isOk(data[key])) {
      const value = getDataValue(key, data[key])

      if (isOk(value)) {
        params[key] = value
      }
    } else {
      params[key] = data[key]
    }
  }

  return params
}

class Request {
  constructor (options) {
    this.host = options.host
    this.protocol = options.protocol
    this.port = options.port
    this.endpoint = options.endpoint
    this.auth = options.auth
    this.proxy = options.proxy
    this.timeout = options.timeout
    this.retry = options.retry || 1
  }

  _request (method, resource, data, fn) {
    let path = ''.concat(this.endpoint, resource)

    const params = prepareData(data)

    this.payload = ''

    const isMIME = path.indexOf('/messages.mime') >= 0

    this.headers = {}
    if (method === 'GET' || method === 'DELETE') {
      this.payload = qs.stringify(params)
      if (this.payload) path = path.concat('?', this.payload)
    } else {
      if (isMIME) {
        this.headers['Content-Type'] = 'multipart/form-data'
      } else if (method === 'POST' && isMultiUnsubsribe(path, data)) {
        this.headers['Content-Type'] = 'application/json'
      } else {
        this.headers['Content-Type'] = 'application/x-www-form-urlencoded'
      }

      if (params && (params.attachment || params.inline || (isMIME && params.message))) {
        this.prepareFormData(params)
      } else {
        if (method === 'POST' && isMultiUnsubsribe(path, data)) {
          this.payload = JSON.stringify(data)
        } else {
          this.payload = qs.stringify(params)
        }

        if (this.payload) {
          this.headers['Content-Length'] = Buffer.byteLength(this.payload)
        } else {
          this.headers['Content-Length'] = 0
        }
      }
    }

    // check for MIME is true in case of messages GET
    if (method === 'GET' &&
      path.indexOf('/messages') >= 0 &&
      params && params.MIME === true) {
      this.headers.Accept = 'message/rfc2822'
    }

    debug('%s %s', method, path)

    const opts = {
      'hostname': this.host,
      'port': this.port,
      'protocol': this.protocol,
      path,
      method,
      'headers': this.headers,
      'auth': this.auth,
      'agent': false,
      'timeout': this.timeout
    }

    if (this.proxy) {
      opts.agent = new ProxyAgent(this.proxy)
    }

    if (typeof this.retry === 'object' || this.retry > 1) {
      retry(this.retry, (retryCb) => {
        this.callback = retryCb
        this.performRequest(opts)
      }, fn)
    } else {
      this.callback = fn
      this.performRequest(opts)
    }
  }

  request (method, resource, data, fn) {
    if (typeof data === 'function' && !fn) {
      fn = data
      data = {}
    }

    if (!data) {
      data = {}
    }

    return promisifyCall(this, this._request, method, resource, data, fn)
  }

  prepareFormData (data) {
    this.form = new FormData()

    for (const key in data) {
      if ({}.hasOwnProperty.call(data, key)) {
        const obj = data[key]

        if (isOk(obj)) {
          if (key === 'attachment' || key === 'inline') {
            if (Array.isArray(obj)) {
              for (let i = 0; i < obj.length; i++) {
                this.handleAttachmentObject(key, obj[i])
              }
            } else {
              this.handleAttachmentObject(key, obj)
            }
          } else if (key === 'message') {
            this.handleMimeObject(key, obj)
          } else if (Array.isArray(obj)) {
            obj.forEach((element) => {
              if (isOk(element)) {
                const value = getDataValue(key, element)

                if (isOk(value)) {
                  this.form.append(key, value)
                }
              }
            })
          } else {
            const value = getDataValue(key, obj)

            if (isOk(value)) {
              this.form.append(key, value)
            }
          }
        }
      }
    }

    this.headers = this.form.getHeaders()
  }

  handleMimeObject (key, obj) {
    if (typeof obj === 'string') {
      if (fs.existsSync(obj) && fs.statSync(obj).isFile()) {
        this.form.append('message', fs.createReadStream(obj))
      } else {
        this.form.append('message', Buffer.from(obj), {
          'filename': 'message.mime',
          'contentType': 'message/rfc822',
          'knownLength': obj.length
        })
      }
    } else if (obj instanceof Readable) {
      this.form.append('message', obj)
    }
  }

  handleAttachmentObject (key, obj) {
    if (!this.form) this.form = new FormData()

    if (Buffer.isBuffer(obj)) {
      debug('appending buffer to form data. key: %s', key)
      this.form.append(key, obj, {
        'filename': 'file'
      })
    } else if (typeof obj === 'string') {
      debug('appending stream to form data. key: %s obj: %s', key, obj)
      this.form.append(key, fs.createReadStream(obj))
    } else if ((typeof obj === 'object') && (obj.readable === true)) {
      debug('appending readable stream to form data. key: %s obj: %s', key, obj)
      this.form.append(key, obj)
    } else if ((typeof obj === 'object') && (obj instanceof Attachment)) {
      const attachmentType = obj.getType()

      if (attachmentType === 'path') {
        debug('appending attachment stream to form data. key: %s data: %s filename: %s', key, obj.data, obj.filename)
        this.form.append(key, fs.createReadStream(obj.data), {
          'filename': obj.filename || 'attached file'
        })
      } else if (attachmentType === 'buffer') {
        debug('appending attachment buffer to form data. key: %s filename: %s', key, obj.filename)
        const formOpts = {
          'filename': obj.filename || 'attached file'
        }

        if (obj.contentType) {
          formOpts.contentType = obj.contentType
        }

        if (obj.knownLength) {
          formOpts.knownLength = obj.knownLength
        }

        this.form.append(key, obj.data, formOpts)
      } else if (attachmentType === 'stream') {
        if (obj.knownLength && obj.contentType) {
          debug('appending attachment stream to form data. key: %s filename: %s', key, obj.filename)

          this.form.append(key, obj.data, {
            'filename': obj.filename || 'attached file',
            'contentType': obj.contentType,
            'knownLength': obj.knownLength
          })
        } else {
          debug('missing content type or length for attachment stream. key: %s', key)
        }
      }
    } else {
      debug('unknown attachment type. key: %s', key)
    }
  }

  handleResponse (res) {
    let chunks = ''
    let error

    res.on('data', (chunk) => {
      chunks += chunk
    })

    res.on('error', (err) => {
      error = err
    })

    res.on('end', () => {
      let body

      debug('response status code: %s content type: %s error: %s', res.statusCode, res.headers['content-type'], error)

      // FIXME: An ugly hack to overcome invalid response type in mailgun api (see http://bit.ly/1eF30fU).
      // We skip content-type validation for 'campaings' endpoint assuming it is JSON.
      const skipContentTypeCheck = res.req && res.req.path && res.req.path.match(/\/campaigns/)
      const isJSON = res.headers['content-type'] && res.headers['content-type'].indexOf('application/json') >= 0

      if (chunks && !error && (skipContentTypeCheck || isJSON)) {
        try {
          body = JSON.parse(chunks)
        } catch (e) {
          error = e
        }
      }

      if (process.env.DEBUG_MAILGUN_FORCE_RETRY) {
        error = new Error('Force retry error')
        delete process.env.DEBUG_MAILGUN_FORCE_RETRY
      }

      if (!error && res.statusCode !== 200) {
        let msg = body || chunks || res.statusMessage

        if (body) {
          msg = body.message || body.response
        }

        error = new Error(msg)
        error.statusCode = res.statusCode
      }

      return this.callback(error, body)
    })
  }

  performRequest (options) {
    const method = options.method

    if (this.form && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
      let alreadyHandled = false
      this.form.submit(options, (err, res) => {
        if (alreadyHandled) {
          return
        }
        alreadyHandled = true

        if (err) {
          return this.callback(err)
        }

        return this.handleResponse(res)
      })
    } else {
      let req

      if (options.protocol === 'http:') {
        req = http.request(options, (res) => {
          return this.handleResponse(res)
        })
      } else {
        req = https.request(options, (res) => {
          return this.handleResponse(res)
        })
      }

      if (options.timeout) {
        req.setTimeout(options.timeout, () => {
          // timeout occurs
          req.abort()
        })
      }

      req.on('error', (e) => {
        return this.callback(e)
      })

      if (this.payload && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
        req.write(this.payload)
      }

      req.end()
    }
  }
}

module.exports = Request