/** Formats a millisecond duration as milliseconds/seconds/minutes/hours, matching mockup precision (1 decimal for hours). */
export function formatDurationMs(ms: number): string {
  if (ms < 1000) return `${Math.round(ms)}ms`
  if (ms < 60_000) return `${Math.round(ms / 1000)}s`
  if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m`
  return `${(ms / 3_600_000).toFixed(1)}h`
}

export function formatRelativeTime(epochMs: number, nowMs = Date.now()): string {
  const deltaMs = epochMs - nowMs
  const absoluteMs = Math.abs(deltaMs)
  if (absoluteMs < 60_000) return 'just now'

  const [amount, unit] =
    absoluteMs < 3_600_000
      ? [Math.floor(absoluteMs / 60_000), 'm']
      : absoluteMs < 86_400_000
        ? [Math.floor(absoluteMs / 3_600_000), 'h']
        : [Math.floor(absoluteMs / 86_400_000), 'd']

  return deltaMs > 0 ? `in ${amount}${unit}` : `${amount}${unit} ago`
}

export function compareOptional(left: number | null, right: number | null, direction: 'asc' | 'desc'): number {
  if (left === null) return right === null ? 0 : 1
  if (right === null) return -1
  return direction === 'asc' ? left - right : right - left
}

/** Formats a 0-100 percentage, rounded to the nearest whole percent. */
export function formatPercent(value: number): string {
  return `${Math.round(value)}%`
}

/** Clamps a value/max ratio into a 0-100 percentage, tolerating max=0. */
export function ratioPercent(value: number, max: number): number {
  if (max <= 0) return 0
  return Math.min(100, Math.max(0, (value / max) * 100))
}

const TRACE_TICK_STEPS_MS = [
  1, 2, 5, 10, 20, 50, 100, 200, 500,
  1_000, 2_000, 5_000, 10_000, 15_000, 30_000, 60_000, 120_000, 300_000, 600_000, 900_000, 1_800_000,
  3_600_000, 7_200_000, 14_400_000, 21_600_000, 43_200_000, 86_400_000, 172_800_000, 604_800_000,
]

const TRACE_AXIS_TIME_ZONE = 'UTC'
const TRACE_AXIS_H23 = { hourCycle: 'h23' as const }

const traceAxisFormatterHms = new Intl.DateTimeFormat('en-US', {
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit',
  ...TRACE_AXIS_H23,
  timeZone: TRACE_AXIS_TIME_ZONE,
})

const traceAxisFormatterHmsMs = new Intl.DateTimeFormat('en-US', {
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit',
  fractionalSecondDigits: 3,
  ...TRACE_AXIS_H23,
  timeZone: TRACE_AXIS_TIME_ZONE,
})

const traceAxisFormatterHm = new Intl.DateTimeFormat('en-US', {
  hour: '2-digit',
  minute: '2-digit',
  ...TRACE_AXIS_H23,
  timeZone: TRACE_AXIS_TIME_ZONE,
})

const traceAxisFormatterDateHm = new Intl.DateTimeFormat('en-US', {
  month: 'short',
  day: 'numeric',
  hour: '2-digit',
  minute: '2-digit',
  ...TRACE_AXIS_H23,
  timeZone: TRACE_AXIS_TIME_ZONE,
})

const traceAxisFormatterDateHms = new Intl.DateTimeFormat('en-US', {
  month: 'short',
  day: 'numeric',
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit',
  ...TRACE_AXIS_H23,
  timeZone: TRACE_AXIS_TIME_ZONE,
})

const traceAxisFormatterDateHmYear = new Intl.DateTimeFormat('en-US', {
  month: 'short',
  day: 'numeric',
  year: 'numeric',
  hour: '2-digit',
  minute: '2-digit',
  ...TRACE_AXIS_H23,
  timeZone: TRACE_AXIS_TIME_ZONE,
})

const traceAxisFormatterDateHmsYear = new Intl.DateTimeFormat('en-US', {
  month: 'short',
  day: 'numeric',
  year: 'numeric',
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit',
  ...TRACE_AXIS_H23,
  timeZone: TRACE_AXIS_TIME_ZONE,
})

type TraceAxisFormatKind = 'hms' | 'hmsMs' | 'hm' | 'dateHm' | 'dateHms' | 'dateHmYear' | 'dateHmsYear'

function traceAxisCandidateTimes(startMs: number, endMs: number, stepMs: number): number[] {
  const times = [startMs]
  for (let tickMs = firstAlignedAfter(startMs, stepMs); tickMs < endMs; tickMs += stepMs) {
    times.push(tickMs)
  }
  if (times.at(-1) !== endMs) times.push(endMs)
  return times
}

function traceAxisHmLabelsCollide(startMs: number, endMs: number, stepMs: number): boolean {
  const labels = traceAxisCandidateTimes(startMs, endMs, stepMs).map((epochMs) =>
    traceAxisFormatterHm.format(epochMs),
  )
  for (let index = 1; index < labels.length; index += 1) {
    if (labels[index] === labels[index - 1]) return true
  }
  return false
}

function traceAxisFormatKind(startMs: number, endMs: number, stepMs: number): TraceAxisFormatKind {
  const spanMs = Math.max(1, endMs - startMs)
  const crossesUtcDate = utcDayNumber(startMs) !== utcDayNumber(endMs)
  const crossesUtcYear = utcYear(startMs) !== utcYear(endMs)
  const showDate = spanMs >= 86_400_000 || crossesUtcDate
  const showMilliseconds = stepMs < 1_000 || spanMs < 1_000
  const showSeconds =
    showMilliseconds ||
    stepMs < 60_000 ||
    (spanMs <= 120_000 && traceAxisHmLabelsCollide(startMs, endMs, stepMs))
  if (showDate) {
    if (crossesUtcYear) return showSeconds ? 'dateHmsYear' : 'dateHmYear'
    return showSeconds ? 'dateHms' : 'dateHm'
  }
  if (showMilliseconds) return 'hmsMs'
  return showSeconds ? 'hms' : 'hm'
}

function utcDayNumber(epochMs: number): number {
  return Math.floor(epochMs / 86_400_000)
}

function utcYear(epochMs: number): number {
  return new Date(epochMs).getUTCFullYear()
}

/** Wall-clock label for a trace time-axis tick; granularity follows tick step and UTC bounds. */
export function formatTraceAxisTime(
  epochMs: number,
  startMs: number,
  endMs: number,
  stepMs = selectTraceAxisStep(startMs, endMs, 8),
): string {
  switch (traceAxisFormatKind(startMs, endMs, stepMs)) {
    case 'hms':
      return traceAxisFormatterHms.format(epochMs)
    case 'hmsMs':
      return traceAxisFormatterHmsMs.format(epochMs)
    case 'hm':
      return traceAxisFormatterHm.format(epochMs)
    case 'dateHm':
      return traceAxisFormatterDateHm.format(epochMs)
    case 'dateHms':
      return traceAxisFormatterDateHms.format(epochMs)
    case 'dateHmYear':
      return traceAxisFormatterDateHmYear.format(epochMs)
    case 'dateHmsYear':
      return traceAxisFormatterDateHmsYear.format(epochMs)
  }
}

function floorToUtcWallClock(epochMs: number, stepMs: number): number {
  return Math.floor(epochMs / stepMs) * stepMs
}

function firstAlignedAfter(epochMs: number, stepMs: number): number {
  const aligned = floorToUtcWallClock(epochMs, stepMs)
  return aligned <= epochMs ? aligned + stepMs : aligned
}

function countInteriorTicks(startMs: number, endMs: number, stepMs: number): number {
  const firstTickMs = firstAlignedAfter(startMs, stepMs)
  if (firstTickMs >= endMs) return 0
  return Math.floor((endMs - 1 - firstTickMs) / stepMs) + 1
}

function selectTraceAxisStep(startMs: number, endMs: number, maxTicks: number): number {
  const spanMs = Math.max(1, endMs - startMs)
  const maxInterior = Math.max(0, maxTicks - 2)

  for (const candidate of TRACE_TICK_STEPS_MS) {
    if (countInteriorTicks(startMs, endMs, candidate) <= maxInterior) return candidate
  }

  const ladderMax = TRACE_TICK_STEPS_MS[TRACE_TICK_STEPS_MS.length - 1]!
  let lo = 2
  let hi = Math.max(2, Math.ceil(spanMs / ladderMax) + 1)
  while (lo < hi) {
    const mid = Math.floor((lo + hi) / 2)
    if (countInteriorTicks(startMs, endMs, ladderMax * mid) <= maxInterior) hi = mid
    else lo = mid + 1
  }
  return ladderMax * lo
}

/** Evenly stepped UTC wall-clock ticks across trace bounds, with at most maxTicks labels. */
export function traceAxisTicks(
  startMs: number,
  endMs: number,
  maxTicks = 8,
): Array<{ pct: number; label: string; epochMs: number }> {
  const spanMs = Math.max(1, endMs - startMs)
  const stepMs = selectTraceAxisStep(startMs, endMs, maxTicks)
  const formatTick = (epochMs: number) => formatTraceAxisTime(epochMs, startMs, endMs, stepMs)
  const ticks: Array<{ pct: number; label: string; epochMs: number }> = [
    {
      pct: 0,
      epochMs: startMs,
      label: formatTick(startMs),
    },
  ]

  for (let tickMs = firstAlignedAfter(startMs, stepMs); tickMs < endMs; tickMs += stepMs) {
    if (tickMs - startMs < stepMs || endMs - tickMs < stepMs) continue
    const pct = ((tickMs - startMs) / spanMs) * 100
    ticks.push({
      pct,
      epochMs: tickMs,
      label: formatTick(tickMs),
    })
  }

  const lastTick = ticks.at(-1)
  if (lastTick?.epochMs !== endMs) {
    ticks.push({
      pct: 100,
      epochMs: endMs,
      label: formatTick(endMs),
    })
  }

  return ticks
}
