import { Span, SpanStatusCode, context, trace } from '@opentelemetry/api';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { B3Propagator } from '@opentelemetry/propagator-b3';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
import { ICreateSpan, IInitTracer } from '../types';

export const initTracer = ({ serviceName, exporterUrl, attributes }: IInitTracer) => {
  const exporter = new OTLPTraceExporter({ url: exporterUrl })
  const resource = new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: serviceName })
  const provider = new WebTracerProvider({ resource })

  provider.addSpanProcessor(new BatchSpanProcessor(exporter))
  provider.register({
    contextManager: new ZoneContextManager(),
    propagator: new B3Propagator()
  })

  const excludeHost = ['www.google-analytics.com', 'api-js.mixpanel.com', 'sdk-01.moengage.com', 'stats.g.doubleclick.net']

  const customAttributes = (span: Span) => {
    span.setAttribute('http.path', attributes.path)
    span.setAttribute('user.uuid', attributes.uuid)
  }

  const customAttributesToResourceFetchSpan = (span: Span, resource: PerformanceResourceTiming) => {
    span.setAttribute('resource.tcp.duration_ms', resource.connectEnd - resource.connectStart)
  }
  
  registerInstrumentations({
    instrumentations: [
      getWebAutoInstrumentations({
        '@opentelemetry/instrumentation-document-load': {
          applyCustomAttributesOnSpan: {
            documentLoad: customAttributes,
            resourceFetch: customAttributesToResourceFetchSpan
          }
        },
        '@opentelemetry/instrumentation-xml-http-request': {
          propagateTraceHeaderCorsUrls: [/.+/g],
          clearTimingResources: true,
          applyCustomAttributesOnSpan: (span: Span | any, xhr: XMLHttpRequest | any) => {
            const attributes = span.attributes
            const hostRequest = attributes['http.url']
            const urlHost = new URL(hostRequest).hostname
            const isExclude = excludeHost.some(host => urlHost.includes(host))
            const spanName = `${attributes['http.method']} ${isExclude ? urlHost : xhr.__zone_symbol__xhrURL || attributes['http.url']}`
            span.setAttribute('response', xhr.response)
            span.setAttribute('responseText', xhr.responseText)
            span.updateName(spanName)
            customAttributes(span)
          }
        },
        '@opentelemetry/instrumentation-fetch': {
          propagateTraceHeaderCorsUrls: [/.+/g],
          clearTimingResources: true,
          applyCustomAttributesOnSpan: (span: Span | any, request, result) => {
            const attributes = span.attributes
            const hostRequest = attributes['http.host']
            if (excludeHost.includes(hostRequest)) { return }
            if (attributes.component === 'fetch') {
              span.updateName(
                `${attributes['http.method']} ${attributes['http.url']}`
              )
            }
            if (result instanceof Error) {
              span.setStatus({
                code: SpanStatusCode.ERROR,
                message: result.message
              })
              span.recordException(result.stack || result.name)
            }
            customAttributes(span)
          }
        }
      })
    ]
  })

  return provider.getTracer(serviceName);
};

export const createSpan = ({ tracer, name, func = () => {}, attributes, events }: ICreateSpan) => {
  const span = tracer.startSpan(name)
  return context.with(trace.setSpan(context.active(), span), () => {
    // mapping custom attributes
    if (span.isRecording() && attributes) {
      for (const key in attributes) {
        if (attributes.hasOwnProperty(key)) {
          const value = attributes[key]
          span.setAttribute(key, value)
        }
      }
    }

    // mapping custom attributes
    if (events) {
      span.addEvent(events.name, events.keyValue)
    }

    // execute function to trace
    try {
      const result = func()
      span.end()
      return result
    } catch (error) {
      span.setStatus({ code: SpanStatusCode.ERROR })
      span.end()
      throw error
    }
  })
}