import type { Duration, RelativeTime } from '@datadog/browser-core'
import {
  elapsed,
  getPathName,
  isValidUrl,
  ResourceType,
  toServerDuration,
  isIntakeUrl,
  isExperimentalFeatureEnabled,
  ExperimentalFeature,
} from '@datadog/browser-core'

import type { RumPerformanceResourceTiming } from '../../browser/performanceObservable'

import type { ResourceEntryDetailsElement, DeliveryType } from '../../rawRumEvent.types'

export interface ResourceEntryDetails {
  worker?: ResourceEntryDetailsElement
  redirect?: ResourceEntryDetailsElement
  dns?: ResourceEntryDetailsElement
  connect?: ResourceEntryDetailsElement
  ssl?: ResourceEntryDetailsElement
  first_byte?: ResourceEntryDetailsElement
  download?: ResourceEntryDetailsElement
}

export const FAKE_INITIAL_DOCUMENT = 'initial_document'

const RESOURCE_TYPES: Array<[ResourceType, (initiatorType: string, path: string) => boolean]> = [
  [ResourceType.DOCUMENT, (initiatorType: string) => FAKE_INITIAL_DOCUMENT === initiatorType],
  [ResourceType.XHR, (initiatorType: string) => 'xmlhttprequest' === initiatorType],
  [ResourceType.FETCH, (initiatorType: string) => 'fetch' === initiatorType],
  [ResourceType.BEACON, (initiatorType: string) => 'beacon' === initiatorType],
  [ResourceType.CSS, (_: string, path: string) => /\.css$/i.test(path)],
  [ResourceType.JS, (_: string, path: string) => /\.js$/i.test(path)],
  [
    ResourceType.IMAGE,
    (initiatorType: string, path: string) =>
      ['image', 'img', 'icon'].includes(initiatorType) || /\.(gif|jpg|jpeg|tiff|png|svg|ico)$/i.exec(path) !== null,
  ],
  [ResourceType.FONT, (_: string, path: string) => /\.(woff|eot|woff2|ttf)$/i.exec(path) !== null],
  [
    ResourceType.MEDIA,
    (initiatorType: string, path: string) =>
      ['audio', 'video'].includes(initiatorType) || /\.(mp3|mp4)$/i.exec(path) !== null,
  ],
]

export function computeResourceEntryType(entry: RumPerformanceResourceTiming) {
  const url = entry.name
  if (!isValidUrl(url)) {
    return ResourceType.OTHER
  }
  const path = getPathName(url)
  for (const [type, isType] of RESOURCE_TYPES) {
    if (isType(entry.initiatorType, path)) {
      return type
    }
  }
  return ResourceType.OTHER
}

function areInOrder(...numbers: number[]) {
  for (let i = 1; i < numbers.length; i += 1) {
    if (numbers[i - 1] > numbers[i]) {
      return false
    }
  }
  return true
}

export function isResourceEntryRequestType(entry: RumPerformanceResourceTiming) {
  return entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch'
}

export function computeResourceEntryDuration(entry: RumPerformanceResourceTiming): Duration {
  const { duration, startTime, responseEnd } = entry

  // Safari duration is always 0 on timings blocked by cross origin policies.
  if (duration === 0 && startTime < responseEnd) {
    return elapsed(startTime, responseEnd)
  }

  return duration
}

export function computeResourceEntryDetails(entry: RumPerformanceResourceTiming): ResourceEntryDetails | undefined {
  if (!hasValidResourceEntryTimings(entry)) {
    return undefined
  }
  const {
    startTime,
    fetchStart,
    workerStart,
    redirectStart,
    redirectEnd,
    domainLookupStart,
    domainLookupEnd,
    connectStart,
    secureConnectionStart,
    connectEnd,
    requestStart,
    responseStart,
    responseEnd,
  } = entry

  const details: ResourceEntryDetails = {
    download: formatTiming(startTime, responseStart, responseEnd),
    first_byte: formatTiming(startTime, requestStart, responseStart),
  }

  // Make sure a worker processing time is recorded
  if (0 < workerStart && workerStart < fetchStart) {
    details.worker = formatTiming(startTime, workerStart, fetchStart)
  }

  // Make sure a connection occurred
  if (fetchStart < connectEnd) {
    details.connect = formatTiming(startTime, connectStart, connectEnd)

    // Make sure a secure connection occurred
    if (connectStart <= secureConnectionStart && secureConnectionStart <= connectEnd) {
      details.ssl = formatTiming(startTime, secureConnectionStart, connectEnd)
    }
  }

  // Make sure a domain lookup occurred
  if (fetchStart < domainLookupEnd) {
    details.dns = formatTiming(startTime, domainLookupStart, domainLookupEnd)
  }

  // Make sure a redirection occurred
  if (startTime < redirectEnd) {
    details.redirect = formatTiming(startTime, redirectStart, redirectEnd)
  }

  return details
}

/**
 * Entries with negative duration are unexpected and should be dismissed. The intake will ignore RUM
 * Resource events with negative durations anyway.
 * Since Chromium 128, more entries have unexpected negative durations, see
 * https://issues.chromium.org/issues/363031537
 */
export function hasValidResourceEntryDuration(entry: RumPerformanceResourceTiming) {
  return entry.duration >= 0
}

export function hasValidResourceEntryTimings(entry: RumPerformanceResourceTiming) {
  // Ensure timings are in the right order. On top of filtering out potential invalid
  // RumPerformanceResourceTiming, it will ignore entries from requests where timings cannot be
  // collected, for example cross origin requests without a "Timing-Allow-Origin" header allowing
  // it.
  const areCommonTimingsInOrder = areInOrder(
    entry.startTime,
    entry.fetchStart,
    entry.domainLookupStart,
    entry.domainLookupEnd,
    entry.connectStart,
    entry.connectEnd,
    entry.requestStart,
    entry.responseStart,
    entry.responseEnd
  )

  const areRedirectionTimingsInOrder = hasRedirection(entry)
    ? areInOrder(entry.startTime, entry.redirectStart, entry.redirectEnd, entry.fetchStart)
    : true

  return areCommonTimingsInOrder && areRedirectionTimingsInOrder
}

function hasRedirection(entry: RumPerformanceResourceTiming) {
  return entry.redirectEnd > entry.startTime
}
function formatTiming(origin: RelativeTime, start: RelativeTime, end: RelativeTime) {
  if (origin <= start && start <= end) {
    return {
      duration: toServerDuration(elapsed(start, end)),
      start: toServerDuration(elapsed(origin, start)),
    }
  }
}

/**
 * The 'nextHopProtocol' is an empty string for cross-origin resources without CORS headers,
 * meaning the protocol is unknown, and we shouldn't report it.
 * https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/nextHopProtocol#cross-origin_resources
 */
export function computeResourceEntryProtocol(entry: RumPerformanceResourceTiming) {
  return entry.nextHopProtocol === '' ? undefined : entry.nextHopProtocol
}

/**
 * Handles the 'deliveryType' property to distinguish between supported values ('cache', 'navigational-prefetch'),
 * undefined (unsupported in some browsers), and other cases ('other' for unknown or unrecognized values).
 * see: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/deliveryType
 */
export function computeResourceEntryDeliveryType(entry: RumPerformanceResourceTiming): DeliveryType | undefined {
  return entry.deliveryType === '' ? 'other' : entry.deliveryType
}

export function computeResourceEntrySize(entry: RumPerformanceResourceTiming) {
  // Make sure a request actually occurred
  if (entry.startTime < entry.responseStart) {
    const { encodedBodySize, decodedBodySize, transferSize } = entry
    return {
      size: decodedBodySize,
      encoded_body_size: encodedBodySize,
      decoded_body_size: decodedBodySize,
      transfer_size: transferSize,
    }
  }
  return {
    size: undefined,
    encoded_body_size: undefined,
    decoded_body_size: undefined,
    transfer_size: undefined,
  }
}

export function isAllowedRequestUrl(url: string) {
  return url && (!isIntakeUrl(url) || isExperimentalFeatureEnabled(ExperimentalFeature.TRACK_INTAKE_REQUESTS))
}

const DATA_URL_REGEX = /data:(.+)?(;base64)?,/g
export const MAX_RESOURCE_VALUE_CHAR_LENGTH = 24_000

export function sanitizeIfLongDataUrl(url: string, lengthLimit: number = MAX_RESOURCE_VALUE_CHAR_LENGTH): string {
  if (url.length <= lengthLimit || !url.startsWith('data:')) {
    return url
  }
  // truncate url first to a random length to prevent match error when the url is too long
  const dataUrlMatchArray = url.substring(0, 100).match(DATA_URL_REGEX)
  if (!dataUrlMatchArray) {
    return url
  }
  return `${dataUrlMatchArray[0]}[...]`
}
