/*
 * Copyright The OpenTelemetry Authors
 *
 * 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
 *
 *      https://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.
 */

const api = require('@opentelemetry/api')
const { hrTimeToMicroseconds } = require('@opentelemetry/core')
const zipkinTypes = require('./types')

const ZIPKIN_SPAN_KIND_MAPPING = {
  [api.SpanKind.CLIENT]: zipkinTypes.SpanKind.CLIENT,
  [api.SpanKind.SERVER]: zipkinTypes.SpanKind.SERVER,
  [api.SpanKind.CONSUMER]: zipkinTypes.SpanKind.CONSUMER,
  [api.SpanKind.PRODUCER]: zipkinTypes.SpanKind.PRODUCER,
  // When absent, the span is local.
  [api.SpanKind.INTERNAL]: undefined,
}

const defaultStatusCodeTagName = 'otel.status_code'
const defaultStatusErrorTagName = 'error'

// The PM2.io backend and frontend read the pre-stable HTTP semantic convention
// tags (http.method, http.status_code, http.target, ...). Newer
// @opentelemetry/instrumentation-http (>= 0.220.0) only emits the stable
// names, so we alias them back to the legacy ones.
const LEGACY_HTTP_TAG_ALIASES = {
  'http.request.method': 'http.method',
  'http.response.status_code': 'http.status_code',
  'url.full': 'http.url',
  'url.scheme': 'http.scheme',
}

function _addLegacyHttpTags (tags) {
  for (const stableName of Object.keys(LEGACY_HTTP_TAG_ALIASES)) {
    const legacyName = LEGACY_HTTP_TAG_ALIASES[stableName]
    if (tags[stableName] !== undefined && tags[legacyName] === undefined) {
      tags[legacyName] = tags[stableName]
    }
  }
  // Legacy http.target is the path with the query string
  if (tags['url.path'] !== undefined && tags['http.target'] === undefined) {
    tags['http.target'] = tags['url.query'] !== undefined
      ? tags['url.path'] + '?' + tags['url.query']
      : tags['url.path']
  }
}

/**
 * Translate OpenTelemetry ReadableSpan to ZipkinSpan format
 * @param span Span to be translated
 */
function toZipkinSpan (span, serviceName, statusCodeTagName, statusErrorTagName) {
  const zipkinSpan = {
    traceId: span.spanContext().traceId,
    parentId: span.parentSpanId || (span.parentSpanContext && span.parentSpanContext.spanId),
    name: span.name,
    id: span.spanContext().spanId,
    kind: ZIPKIN_SPAN_KIND_MAPPING[span.kind],
    timestamp: hrTimeToMicroseconds(span.startTime),
    duration: Math.round(hrTimeToMicroseconds(span.duration)),
    localEndpoint: { serviceName },
    tags: _toZipkinTags(span, statusCodeTagName, statusErrorTagName),
    annotations: span.events.length
      ? _toZipkinAnnotations(span.events)
      : undefined,
  }

  return zipkinSpan
}

/** Converts OpenTelemetry Span properties to Zipkin Tags format. */
function _toZipkinTags (
  { attributes, resource, status, droppedAttributesCount, droppedEventsCount, droppedLinksCount },
  statusCodeTagName,
  statusErrorTagName
) {
  const tags = {}
  for (const key of Object.keys(attributes)) {
    tags[key] = String(attributes[key])
  }
  _addLegacyHttpTags(tags)
  if (status.code !== api.SpanStatusCode.UNSET) {
    tags[statusCodeTagName] = String(api.SpanStatusCode[status.code])
  }
  if (status.code === api.SpanStatusCode.ERROR && status.message) {
    tags[statusErrorTagName] = status.message
  }
  /* Add droppedAttributesCount as a tag */
  if (droppedAttributesCount) {
    tags['otel.dropped_attributes_count'] = String(droppedAttributesCount)
  }

  /* Add droppedEventsCount as a tag */
  if (droppedEventsCount) {
    tags['otel.dropped_events_count'] = String(droppedEventsCount)
  }

  /* Add droppedLinksCount as a tag */
  if (droppedLinksCount) {
    tags['otel.dropped_links_count'] = String(droppedLinksCount)
  }

  Object.keys(resource.attributes).forEach(
    name => (tags[name] = String(resource.attributes[name]))
  )

  return tags
}

/**
 * Converts OpenTelemetry Events to Zipkin Annotations format.
 */
function _toZipkinAnnotations (events) {
  return events.map(event => ({
    timestamp: Math.round(hrTimeToMicroseconds(event.time)),
    value: event.name,
  }))
}

module.exports = {
  defaultStatusCodeTagName,
  defaultStatusErrorTagName,
  toZipkinSpan,
  _toZipkinTags,
  _toZipkinAnnotations,
}
