import type {
  Duration,
  XhrCompleteContext,
  XhrStartContext,
  ClocksState,
  FetchStartContext,
  FetchResolveContext,
  ContextManager,
} from '@datadog/browser-core'
import {
  RequestType,
  ResponseBodyAction,
  elapsed,
  initFetchObservable,
  initXhrObservable,
  timeStampNow,
} from '@datadog/browser-core'
import type { RumSessionManager } from '..'
import type { RumConfiguration } from './configuration'
import type { LifeCycle } from './lifeCycle'
import { LifeCycleEventType } from './lifeCycle'
import { isAllowedRequestUrl } from './resource/resourceUtils'
import type { Tracer } from './tracing/tracer'
import { startTracer } from './tracing/tracer'
import type { SpanIdentifier, TraceIdentifier } from './tracing/identifier'
import { findGraphQlConfiguration } from './resource/graphql'

export interface CustomContext {
  requestIndex: number
  spanId?: SpanIdentifier
  traceId?: TraceIdentifier
  traceSampled?: boolean
}
export interface RumFetchStartContext extends FetchStartContext, CustomContext {}
export interface RumFetchResolveContext extends FetchResolveContext, CustomContext {}
export interface RumXhrStartContext extends XhrStartContext, CustomContext {}
export interface RumXhrCompleteContext extends XhrCompleteContext, CustomContext {}

export interface RequestStartEvent {
  requestIndex: number
  url: string
}
export interface RequestCompleteEvent {
  requestIndex: number
  type: RequestType
  method: string
  url: string
  status: number
  responseType?: string
  startClocks: ClocksState
  duration: Duration
  spanId?: SpanIdentifier
  traceId?: TraceIdentifier
  traceSampled?: boolean
  xhr?: XMLHttpRequest
  response?: Response
  input?: unknown
  init?: RequestInit
  error?: Error
  isAborted: boolean
  isAbortedOnStart: boolean
  handlingStack?: string
  requestBody?: unknown
  responseBody?: string
}

let nextRequestIndex = 1

export function startRequestCollection(
  lifeCycle: LifeCycle,
  configuration: RumConfiguration,
  sessionManager: RumSessionManager,
  userContext: ContextManager,
  accountContext: ContextManager
) {
  const tracer = startTracer(configuration, sessionManager, userContext, accountContext)
  trackXhr(lifeCycle, configuration, tracer)
  trackFetch(lifeCycle, configuration, tracer)
}

export function trackXhr(lifeCycle: LifeCycle, configuration: RumConfiguration, tracer: Tracer) {
  const subscription = initXhrObservable(configuration).subscribe((rawContext) => {
    const context = rawContext as RumXhrStartContext | RumXhrCompleteContext
    if (!isAllowedRequestUrl(context.url)) {
      return
    }

    switch (context.state) {
      case 'start':
        tracer.traceXhr(context, context.xhr)
        context.requestIndex = getNextRequestIndex()

        lifeCycle.notify(LifeCycleEventType.REQUEST_STARTED, {
          requestIndex: context.requestIndex,
          url: context.url,
        })
        break
      case 'complete':
        tracer.clearTracingIfNeeded(context)
        lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, {
          ...context,
          type: RequestType.XHR,
          isAbortedOnStart: false,
        })
        break
    }
  })

  return { stop: () => subscription.unsubscribe() }
}

export function trackFetch(lifeCycle: LifeCycle, configuration: RumConfiguration, tracer: Tracer) {
  const subscription = initFetchObservable({
    responseBodyAction: (context) => {
      if (findGraphQlConfiguration(context.url, configuration)?.trackResponseErrors) {
        return ResponseBodyAction.COLLECT
      }
      return ResponseBodyAction.WAIT
    },
  }).subscribe((rawContext) => {
    const context = rawContext as RumFetchResolveContext | RumFetchStartContext
    if (!isAllowedRequestUrl(context.url)) {
      return
    }

    switch (context.state) {
      case 'start':
        tracer.traceFetch(context)
        context.requestIndex = getNextRequestIndex()

        lifeCycle.notify(LifeCycleEventType.REQUEST_STARTED, {
          requestIndex: context.requestIndex,
          url: context.url,
        })
        break
      case 'resolve':
        tracer.clearTracingIfNeeded(context)
        lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, {
          ...context,
          duration: elapsed(context.startClocks.timeStamp, timeStampNow()),
          type: RequestType.FETCH,
          requestBody: context.init?.body,
        })
        break
    }
  })
  return { stop: () => subscription.unsubscribe() }
}

function getNextRequestIndex() {
  const result = nextRequestIndex
  nextRequestIndex += 1
  return result
}
