import type {
  TrackingConsentState,
  DeflateWorker,
  Context,
  ContextManager,
  BoundedBuffer,
  Telemetry,
  TimeStamp,
} from '@datadog/browser-core'
import {
  createBoundedBuffer,
  display,
  canUseEventBridge,
  displayAlreadyInitializedError,
  willSyntheticsInjectRum,
  noop,
  timeStampNow,
  clocksNow,
  getEventBridge,
  initFeatureFlags,
  addTelemetryConfiguration,
  initFetchObservable,
  CustomerContextKey,
  buildAccountContextManager,
  buildGlobalContextManager,
  buildUserContextManager,
  monitorError,
  sanitize,
  startTelemetry,
  TelemetryService,
  mockable,
} from '@datadog/browser-core'
import type { Hooks } from '../domain/hooks'
import { createHooks } from '../domain/hooks'
import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration'
import {
  validateAndBuildRumConfiguration,
  fetchAndApplyRemoteConfiguration,
  serializeRumConfiguration,
} from '../domain/configuration'
import type { ViewOptions } from '../domain/view/trackViews'
import type {
  DurationVital,
  CustomVitalsState,
  FeatureOperationOptions,
  FailureReason,
} from '../domain/vital/vitalCollection'
import { startDurationVital, stopDurationVital } from '../domain/vital/vitalCollection'
import { callPluginsMethod } from '../domain/plugins'
import type { StartRumResult } from './startRum'
import type { RumPublicApiOptions, Strategy } from './rumPublicApi'

export type DoStartRum = (
  configuration: RumConfiguration,
  deflateWorker: DeflateWorker | undefined,
  initialViewOptions: ViewOptions | undefined,
  telemetry: Telemetry,
  hooks: Hooks
) => StartRumResult

export function createPreStartStrategy(
  { ignoreInitIfSyntheticsWillInjectRum = true, startDeflateWorker }: RumPublicApiOptions,
  trackingConsentState: TrackingConsentState,
  customVitalsState: CustomVitalsState,
  doStartRum: DoStartRum
): Strategy {
  const bufferApiCalls = createBoundedBuffer<StartRumResult>()

  // TODO next major: remove the globalContextManager, userContextManager and accountContextManager from preStartStrategy and use an empty context instead
  const globalContext = buildGlobalContextManager()
  bufferContextCalls(globalContext, CustomerContextKey.globalContext, bufferApiCalls)

  const userContext = buildUserContextManager()
  bufferContextCalls(userContext, CustomerContextKey.userContext, bufferApiCalls)

  const accountContext = buildAccountContextManager()
  bufferContextCalls(accountContext, CustomerContextKey.accountContext, bufferApiCalls)

  let firstStartViewCall:
    | { options: ViewOptions | undefined; callback: (startRumResult: StartRumResult) => void }
    | undefined
  let deflateWorker: DeflateWorker | undefined

  let cachedInitConfiguration: RumInitConfiguration | undefined
  let cachedConfiguration: RumConfiguration | undefined
  let telemetry: Telemetry | undefined
  const hooks = createHooks()

  const trackingConsentStateSubscription = trackingConsentState.observable.subscribe(tryStartRum)

  const emptyContext: Context = {}

  function tryStartRum() {
    if (!cachedInitConfiguration || !cachedConfiguration || !trackingConsentState.isGranted()) {
      return
    }

    // Start telemetry only once, when we have consent and configuration
    if (!telemetry) {
      telemetry = mockable(startTelemetry)(TelemetryService.RUM, cachedConfiguration, hooks)
    }

    trackingConsentStateSubscription.unsubscribe()

    let initialViewOptions: ViewOptions | undefined

    if (cachedConfiguration.trackViewsManually) {
      if (!firstStartViewCall) {
        return
      }
      // An initial view is always created when starting RUM.
      // When tracking views automatically, any startView call before RUM start creates an extra
      // view.
      // When tracking views manually, we use the ViewOptions from the first startView call as the
      // initial view options, and we remove the actual startView call so we don't create an extra
      // view.
      bufferApiCalls.remove(firstStartViewCall.callback)
      initialViewOptions = firstStartViewCall.options
    }

    const startRumResult = doStartRum(cachedConfiguration, deflateWorker, initialViewOptions, telemetry, hooks)

    bufferApiCalls.drain(startRumResult)
  }

  function doInit(initConfiguration: RumInitConfiguration, errorStack?: string) {
    const eventBridgeAvailable = canUseEventBridge()
    if (eventBridgeAvailable) {
      initConfiguration = overrideInitConfigurationForBridge(initConfiguration)
    }

    // Update the exposed initConfiguration to reflect the bridge and remote configuration overrides
    cachedInitConfiguration = initConfiguration
    addTelemetryConfiguration(serializeRumConfiguration(initConfiguration))

    if (cachedConfiguration) {
      displayAlreadyInitializedError('DD_RUM', initConfiguration)
      return
    }

    const configuration = validateAndBuildRumConfiguration(initConfiguration, errorStack)
    if (!configuration) {
      return
    }

    if (!eventBridgeAvailable && !configuration.sessionStoreStrategyType) {
      display.warn('No storage available for session. We will not send any data.')
      return
    }

    if (configuration.compressIntakeRequests && !eventBridgeAvailable && startDeflateWorker) {
      deflateWorker = startDeflateWorker(
        configuration,
        'Datadog RUM',
        // Worker initialization can fail asynchronously, especially in Firefox where even CSP
        // issues are reported asynchronously. For now, the SDK will continue its execution even if
        // data won't be sent to Datadog. We could improve this behavior in the future.
        noop
      )
      if (!deflateWorker) {
        // `startDeflateWorker` should have logged an error message explaining the issue
        return
      }
    }

    cachedConfiguration = configuration

    // Instrument fetch to track network requests
    // This is needed in case the consent is not granted and some customer
    // library (Apollo Client) is storing uninstrumented fetch to be used later
    // The subscription is needed so that the instrumentation process is completed
    initFetchObservable().subscribe(noop)

    trackingConsentState.tryToInit(configuration.trackingConsent)
    tryStartRum()
  }

  const addDurationVital = (vital: DurationVital) => {
    bufferApiCalls.add((startRumResult) => startRumResult.addDurationVital(vital))
  }

  const addOperationStepVital = (
    name: string,
    stepType: 'start' | 'end',
    options?: FeatureOperationOptions,
    failureReason?: FailureReason
  ) => {
    bufferApiCalls.add((startRumResult) =>
      startRumResult.addOperationStepVital(
        sanitize(name)!,
        stepType,
        sanitize(options) as FeatureOperationOptions,
        sanitize(failureReason) as FailureReason | undefined
      )
    )
  }

  const strategy: Strategy = {
    init(initConfiguration, publicApi, errorStack) {
      if (!initConfiguration) {
        display.error('Missing configuration')
        return
      }
      // Set the experimental feature flags as early as possible, so we can use them in most places
      initFeatureFlags(initConfiguration.enableExperimentalFeatures)

      // Expose the initial configuration regardless of initialization success.
      cachedInitConfiguration = initConfiguration

      // If we are in a Synthetics test configured to automatically inject a RUM instance, we want
      // to completely discard the customer application RUM instance by ignoring their init() call.
      // But, we should not ignore the init() call from the Synthetics-injected RUM instance, so the
      // internal `ignoreInitIfSyntheticsWillInjectRum` option is here to bypass this condition.
      if (ignoreInitIfSyntheticsWillInjectRum && willSyntheticsInjectRum()) {
        return
      }

      callPluginsMethod(initConfiguration.plugins, 'onInit', { initConfiguration, publicApi })

      if (initConfiguration.remoteConfigurationId) {
        fetchAndApplyRemoteConfiguration(initConfiguration, { user: userContext, context: globalContext })
          .then((initConfiguration) => {
            if (initConfiguration) {
              doInit(initConfiguration, errorStack)
            }
          })
          .catch(monitorError)
      } else {
        doInit(initConfiguration, errorStack)
      }
    },

    get initConfiguration() {
      return cachedInitConfiguration
    },

    getInternalContext: noop as () => undefined,

    stopSession: noop,

    addTiming(name, time = timeStampNow()) {
      bufferApiCalls.add((startRumResult) => startRumResult.addTiming(name, time))
    },

    setLoadingTime: ((callTimestamp: TimeStamp) => {
      bufferApiCalls.add((startRumResult) => startRumResult.setLoadingTime(callTimestamp))
    }) as Strategy['setLoadingTime'],

    startView(options, startClocks = clocksNow()) {
      const callback = (startRumResult: StartRumResult) => {
        startRumResult.startView(options, startClocks)
      }
      bufferApiCalls.add(callback)

      if (!firstStartViewCall) {
        firstStartViewCall = { options, callback }
        tryStartRum()
      }
    },

    setViewName(name) {
      bufferApiCalls.add((startRumResult) => startRumResult.setViewName(name))
    },

    // View context APIs

    setViewContext(context) {
      bufferApiCalls.add((startRumResult) => startRumResult.setViewContext(context))
    },

    setViewContextProperty(key, value) {
      bufferApiCalls.add((startRumResult) => startRumResult.setViewContextProperty(key, value))
    },

    getViewContext: () => emptyContext,

    globalContext,
    userContext,
    accountContext,

    addAction(action) {
      bufferApiCalls.add((startRumResult) => startRumResult.addAction(action))
    },

    startAction(name, options) {
      const startClocks = clocksNow()
      bufferApiCalls.add((startRumResult) => startRumResult.startAction(name, options, startClocks))
    },

    stopAction(name, options) {
      const stopClocks = clocksNow()
      bufferApiCalls.add((startRumResult) => startRumResult.stopAction(name, options, stopClocks))
    },

    startResource(url, options) {
      const startClocks = clocksNow()
      bufferApiCalls.add((startRumResult) => startRumResult.startResource(url, options, startClocks))
    },

    stopResource(url, options) {
      const stopClocks = clocksNow()
      bufferApiCalls.add((startRumResult) => startRumResult.stopResource(url, options, stopClocks))
    },

    addError(providedError) {
      bufferApiCalls.add((startRumResult) => startRumResult.addError(providedError))
    },

    addFeatureFlagEvaluation(key, value) {
      bufferApiCalls.add((startRumResult) => startRumResult.addFeatureFlagEvaluation(key, value))
    },

    startDurationVital(name, options) {
      return startDurationVital(customVitalsState, name, options)
    },

    stopDurationVital(name, options) {
      stopDurationVital(addDurationVital, customVitalsState, name, options)
    },

    addDurationVital,
    addOperationStepVital,
  }

  return strategy
}

function overrideInitConfigurationForBridge(initConfiguration: RumInitConfiguration): RumInitConfiguration {
  return {
    ...initConfiguration,
    applicationId: '00000000-aaaa-0000-aaaa-000000000000',
    clientToken: 'empty',
    sessionSampleRate: 100,
    defaultPrivacyLevel: initConfiguration.defaultPrivacyLevel ?? getEventBridge()?.getPrivacyLevel(),
  }
}

function bufferContextCalls(
  preStartContextManager: ContextManager,
  name: CustomerContextKey,
  bufferApiCalls: BoundedBuffer<StartRumResult>
) {
  preStartContextManager.changeObservable.subscribe(() => {
    const context = preStartContextManager.getContext()
    bufferApiCalls.add((startRumResult) => startRumResult[name].setContext(context))
  })
}
