/*
 * Copyright Elasticsearch B.V. and other contributors where applicable.
 * Licensed under the BSD 2-Clause License; you may not use this file except in
 * compliance with the BSD 2-Clause License.
 */

'use strict'

const TYPE = 'messaging'
const SUBTYPE = 'sns'
const ACTION = 'publish'
const PHONE_NUMBER = '<PHONE_NUMBER>'
const MAX_SNS_MESSAGE_ATTRIBUTES = 10

function getArnOrPhoneNumberFromRequest (request) {
  let arn = request && request.params && request.params.TopicArn
  if (!arn) {
    arn = request && request.params && request.params.TargetArn
  }
  if (!arn) {
    arn = request && request.params && request.params.PhoneNumber
  }
  return arn
}

function getRegionFromRequest (request) {
  return request && request.service &&
        request.service.config && request.service.config.region
}

function getSpanNameFromRequest (request) {
  const topicName = getDestinationNameFromRequest(request)
  return `SNS PUBLISH to ${topicName}`
}

function getMessageContextFromRequest (request) {
  return {
    queue: {
      name: getDestinationNameFromRequest(request)
    }
  }
}

function getAddressFromRequest (request) {
  return request && request.service && request.service.endpoint &&
    request.service.endpoint.hostname
}

function getPortFromRequest (request) {
  return request && request.service && request.service.endpoint &&
    request.service.endpoint.port
}

function getMessageDestinationContextFromRequest (request) {
  return {
    address: getAddressFromRequest(request),
    port: getPortFromRequest(request),
    cloud: {
      region: getRegionFromRequest(request)
    }
  }
}

function getDestinationNameFromRequest (request) {
  const phoneNumber = request && request.params && request.params.PhoneNumber
  if (phoneNumber) {
    return PHONE_NUMBER
  }

  const topicArn = request && request.params && request.params.TopicArn
  const targetArn = request && request.params && request.params.TargetArn

  if (topicArn) {
    const parts = topicArn.split(':')
    const topicName = parts.pop()
    return topicName
  }

  if (targetArn) {
    const fullName = targetArn.split(':').pop()
    if (fullName.lastIndexOf('/') !== -1) {
      return fullName.substring(0, fullName.lastIndexOf('/'))
    } else {
      return fullName
    }
  }
}

function shouldIgnoreRequest (request, agent) {
  if (request.operation !== 'publish') {
    return true
  }

  // is the named topic on our ignore list?
  if (agent._conf && agent._conf.ignoreMessageQueuesRegExp) {
    const queueName = getArnOrPhoneNumberFromRequest(request)
    if (queueName) {
      for (const rule of agent._conf.ignoreMessageQueuesRegExp) {
        if (rule.test(queueName)) {
          return true
        }
      }
    }
  }

  return false
}

// Though our spec only mentions a 10-message-attribute limit for *SQS*, we'll
// do the same limit here, because
// https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html
// mentions the 10-message-attribute limit for SQS subscriptions.
function propagateTraceContextInMessageAttributes (parentSpan, msgParams, log, targetId) {
  const msgAttrs = msgParams.MessageAttributes
  if (msgAttrs && Object.keys(msgAttrs).length + 2 > MAX_SNS_MESSAGE_ATTRIBUTES) {
    log.warn('cannot propagate trace-context with SNS message to %s, too many MessageAttributes', targetId)
  } else {
    parentSpan.propagateTraceContextHeaders(msgAttrs, function (msgAttrs, name, value) {
      if (name.startsWith('elastic-')) {
        return
      }
      msgAttrs[name] = { DataType: 'String', StringValue: value }
    })
  }
}

function instrumentationSns (orig, origArguments, request, AWS, agent, { version, enabled }) {
  if (shouldIgnoreRequest(request, agent)) {
    return orig.apply(request, origArguments)
  }

  const ins = agent._instrumentation
  const log = agent.logger
  const parentRunContext = ins.currRunContext()
  const name = getSpanNameFromRequest(request)
  const span = ins.createSpan(name, TYPE, SUBTYPE, ACTION, { exitSpan: true })

  // W3C trace-context propagation.
  const parent = span || parentRunContext.currSpan() || parentRunContext.currTransaction()
  if (parent) {
    // Currently only instrumenting a 'publish' operation, for which we want
    // to propagate trace-context.
    // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SNS.html#publish-property
    const paramsCopy = Object.assign({}, request.params)
    paramsCopy.MessageAttributes = Object.assign({}, paramsCopy.MessageAttributes)
    request.params = paramsCopy
    const targetId = getArnOrPhoneNumberFromRequest(request)
    propagateTraceContextInMessageAttributes(parent, request.params, log, targetId)
  }

  if (!span) {
    return orig.apply(request, origArguments)
  }

  span._setDestinationContext(getMessageDestinationContextFromRequest(request))
  span.setMessageContext(getMessageContextFromRequest(request))

  const onComplete = function (response) {
    if (response && response.error) {
      agent.captureError(response.error)
    }
    span.end()
  }
  // Bind onComplete to the span's run context so that `captureError` picks
  // up the correct currentSpan.
  const spanRunContext = parentRunContext.enterSpan(span)
  request.on('complete', ins.bindFunctionToRunContext(spanRunContext, onComplete))

  const cb = origArguments[origArguments.length - 1]
  if (typeof cb === 'function') {
    origArguments[origArguments.length - 1] = ins.bindFunctionToRunContext(parentRunContext, cb)
  }
  return ins.withRunContext(spanRunContext, orig, request, ...origArguments)
}

module.exports = {
  instrumentationSns,

  // exported for testing
  getSpanNameFromRequest,
  getDestinationNameFromRequest,
  getMessageDestinationContextFromRequest,
  getArnOrPhoneNumberFromRequest
}
