import type { RelativeTime } from '@datadog/browser-core'
import {
  DOM_EVENT,
  ONE_MINUTE,
  addEventListeners,
  findLast,
  ExperimentalFeature,
  isExperimentalFeatureEnabled,
} from '@datadog/browser-core'
import type { RumConfiguration } from '../../configuration'
import { createPerformanceObservable, RumPerformanceEntryType } from '../../../browser/performanceObservable'
import { findLcpResourceEntry, getNavigationEntry, sanitizeFirstByte } from '../../../browser/performanceUtils'
import type { RumLargestContentfulPaintTiming } from '../../../browser/performanceObservable'
import { getSelectorFromElement } from '../../getSelectorFromElement'
import type { FirstHidden } from './trackFirstHidden'

// Discard LCP timings above a certain delay to avoid incorrect data
// It happens in some cases like sleep mode or some browser implementations
export const LCP_MAXIMUM_DELAY = 10 * ONE_MINUTE

export interface LargestContentfulPaint {
  value: RelativeTime
  targetSelector?: string
  resourceUrl?: string
  subParts?: {
    loadDelay: RelativeTime
    loadTime: RelativeTime
    renderDelay: RelativeTime
  }
}

/**
 * Track the largest contentful paint (LCP) occurring during the initial View.  This can yield
 * multiple values, only the most recent one should be used.
 * Documentation: https://web.dev/lcp/
 * Reference implementation: https://github.com/GoogleChrome/web-vitals/blob/master/src/onLCP.ts
 */
export function trackLargestContentfulPaint(
  configuration: RumConfiguration,
  firstHidden: FirstHidden,
  eventTarget: Window,
  callback: (largestContentfulPaint: LargestContentfulPaint) => void
) {
  // Ignore entries that come after the first user interaction. According to the documentation, the
  // browser should not send largest-contentful-paint entries after a user interact with the page,
  // but the web-vitals reference implementation uses this as a safeguard.
  let firstInteractionTimestamp = Infinity
  const { stop: stopEventListener } = addEventListeners(
    configuration,
    eventTarget,
    [DOM_EVENT.POINTER_DOWN, DOM_EVENT.KEY_DOWN],
    (event) => {
      firstInteractionTimestamp = event.timeStamp
    },
    { capture: true, once: true }
  )

  let biggestLcpSize = 0
  const performanceLcpSubscription = createPerformanceObservable(configuration, {
    type: RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT,
    buffered: true,
  }).subscribe((entries) => {
    const lcpEntry = findLast(
      entries,
      (entry): entry is RumLargestContentfulPaintTiming =>
        entry.entryType === RumPerformanceEntryType.LARGEST_CONTENTFUL_PAINT &&
        entry.startTime < firstInteractionTimestamp &&
        entry.startTime < firstHidden.timeStamp &&
        entry.startTime < LCP_MAXIMUM_DELAY &&
        // Ensure to get the LCP entry with the biggest size, see
        // https://bugs.chromium.org/p/chromium/issues/detail?id=1516655
        entry.size > biggestLcpSize
    )
    if (lcpEntry) {
      let lcpTargetSelector
      if (lcpEntry.element) {
        lcpTargetSelector = getSelectorFromElement(lcpEntry.element, configuration.actionNameAttribute)
      }

      const resourceUrl = computeLcpEntryUrl(lcpEntry)
      const lcpValue = lcpEntry.startTime

      const subParts = isExperimentalFeatureEnabled(ExperimentalFeature.LCP_SUBPARTS)
        ? computeLcpSubParts(resourceUrl, lcpValue)
        : undefined

      callback({
        value: lcpValue,
        targetSelector: lcpTargetSelector,
        resourceUrl,
        subParts,
      })
      biggestLcpSize = lcpEntry.size
    }
  })

  return {
    stop: () => {
      stopEventListener()
      performanceLcpSubscription.unsubscribe()
    },
  }
}

// The property url report an empty string if the value is not available, we shouldn't report it in this case.
function computeLcpEntryUrl(entry: RumLargestContentfulPaintTiming) {
  return entry.url === '' ? undefined : entry.url
}

/**
 * Compute the LCP sub-parts breakdown (loadDelay, loadTime, renderDelay).
 * Returns undefined if navigation timing data or TTFB is unavailable.
 */
function computeLcpSubParts(
  resourceUrl: string | undefined,
  lcpValue: RelativeTime
): LargestContentfulPaint['subParts'] {
  const firstByte = sanitizeFirstByte(getNavigationEntry())

  if (firstByte === undefined) {
    return undefined
  }

  const lcpResourceEntry = resourceUrl ? findLcpResourceEntry(resourceUrl, lcpValue) : undefined
  const lcpRequestStart = lcpResourceEntry
    ? (Math.max(firstByte, lcpResourceEntry.requestStart || lcpResourceEntry.startTime) as RelativeTime)
    : firstByte

  // Cap at LCP time to handle resources that continue downloading after LCP (e.g., videos)
  const lcpResponseEnd = Math.min(lcpValue, Math.max(lcpRequestStart, lcpResourceEntry?.responseEnd || 0))

  return {
    loadDelay: (lcpRequestStart - firstByte) as RelativeTime,
    loadTime: (lcpResponseEnd - lcpRequestStart) as RelativeTime,
    renderDelay: (lcpValue - lcpResponseEnd) as RelativeTime,
  }
}
