import { DataTable, type DataTableColumn } from './internal/DataTable'
import { useId, useMemo, useState } from 'react'

export interface TimeSeriesPoint {
  ts: string
  value: number
}

export interface TimeSeriesGap {
  from?: string
  to?: string
  reason: string
}

export interface TimeSeriesChartProps {
  label: string
  points: TimeSeriesPoint[]
  status: 'available' | 'unavailable'
  reason?: string
  coverageFrom?: string
  gaps?: TimeSeriesGap[]
  valueLabel?: string
  /** Forces axis tick text to a fixed locale (SSR/hydration determinism). Defaults to the browser/native locale. */
  locale?: string
  /** Forces 12-hour clock on axis tick text. Defaults to the locale's native hour cycle. */
  hour12?: boolean
}

const WIDTH = 720
const HEIGHT = 240
const PADDING_LEFT = 56
const PADDING_RIGHT = 20
const PADDING_TOP = 16
const PADDING_BOTTOM = 44

function validPoints(points: TimeSeriesPoint[]) {
  return points
    .map((point) => ({ ...point, time: Date.parse(point.ts) }))
    .filter((point) => Number.isFinite(point.time) && Number.isFinite(point.value) && point.value >= 0)
    .sort((left, right) => left.time - right.time)
}

function formatPoint(point: TimeSeriesPoint, valueLabel: string) {
  return `${new Date(point.ts).toISOString()}: ${point.value.toLocaleString()} ${valueLabel}`
}

function gapSeparates(left: number, right: number, gaps: TimeSeriesGap[]): boolean {
  return gaps.some((gap) => {
    const from = gap.from ? Date.parse(gap.from) : Number.NaN
    const to = gap.to ? Date.parse(gap.to) : Number.NaN
    return Number.isFinite(from) && Number.isFinite(to) && from < right && to > left
  })
}

function sameLocalCalendarDay(left: number, right: number) {
  const a = new Date(left)
  const b = new Date(right)
  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
}

function formatAxisTime(time: number, minTime: number, maxTime: number, locale?: string, hour12?: boolean) {
  const date = new Date(time)
  const span = maxTime - minTime
  if (span >= 3 * 86_400_000) {
    return date.toLocaleDateString(locale, { month: 'short', day: 'numeric' })
  }
  if (!sameLocalCalendarDay(minTime, maxTime)) {
    return date.toLocaleString(locale, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12 })
  }
  return date.toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit', hour12 })
}

function segmentedPaths(coordinates: Array<{ point: TimeSeriesPoint & { time: number }; x: number; y: number }>, gaps: TimeSeriesGap[]): string[] {
  const paths: string[] = []
  let path = ''
  coordinates.forEach(({ point, x, y }, index) => {
    const previous = coordinates[index - 1]?.point
    if (previous && gapSeparates(previous.time, point.time, gaps)) {
      if (path) paths.push(path)
      path = ''
    }
    path += `${path ? ' L' : 'M'} ${x} ${y}`
  })
  if (path) paths.push(path)
  return paths
}

export function TimeSeriesChart({
  label,
  points,
  status,
  reason,
  coverageFrom,
  gaps = [],
  valueLabel = 'events',
  locale,
  hour12,
}: TimeSeriesChartProps) {
  const titleId = useId()
  const descriptionId = useId()
  const [activeIndex, setActiveIndex] = useState<number | null>(null)
  const valid = useMemo(() => validPoints(points), [points])
  const maxValue = Math.max(1, ...valid.map((point) => point.value))
  const minTime = valid.at(0)?.time ?? 0
  const maxTime = valid.at(-1)?.time ?? minTime
  const timeSpan = Math.max(1, maxTime - minTime)
  const plotWidth = WIDTH - PADDING_LEFT - PADDING_RIGHT
  const plotHeight = HEIGHT - PADDING_TOP - PADDING_BOTTOM
  const coordinates = valid.map((point) => ({
    point,
    x: PADDING_LEFT + ((point.time - minTime) / timeSpan) * plotWidth,
    y: PADDING_TOP + (1 - point.value / maxValue) * plotHeight,
  }))
  const yTicks = [0, maxValue / 2, maxValue]
  const xTicks = [minTime, minTime + timeSpan / 2, maxTime]
  const paths = segmentedPaths(coordinates, gaps)
  const tableColumns: DataTableColumn<(typeof valid)[number]>[] = [
    { id: 'time', header: 'Time', cell: (point) => new Date(point.ts).toISOString() },
    { id: 'value', header: valueLabel, cell: (point) => <span className="block text-right tabular-nums">{point.value.toLocaleString()}</span> },
  ]
  const active = activeIndex === null ? null : coordinates[activeIndex]?.point ?? null

  if (status === 'unavailable') {
    return <p role="status" className="rounded-xl border border-border bg-surface p-4 text-sm text-fg-muted">{label} unavailable{reason ? ` — ${reason}` : ''}</p>
  }

  if (valid.length === 0) {
    return <p role="status" className="rounded-xl border border-border bg-surface p-4 text-sm text-fg-muted">No measured {label.toLowerCase()} points in this covered range.</p>
  }

  return (
    <figure className="min-w-0" aria-labelledby={titleId} aria-describedby={descriptionId}>
      <figcaption id={titleId} className="text-sm font-semibold text-fg">{label}</figcaption>
      <p id={descriptionId} className="mb-2 text-xs text-fg-muted">
        {coverageFrom ? `Measured from ${new Date(coverageFrom).toISOString()}. ` : ''}
        Peak {maxValue.toLocaleString()} {valueLabel}. Times use your browser locale and time zone; point details use absolute ISO time.
      </p>
      {gaps.length > 0 ? <ul aria-label={`${label} coverage gaps`} className="mb-2 space-y-1 text-xs text-warning">{gaps.map((gap, index) => <li key={`${gap.from ?? ''}:${gap.to ?? ''}:${index}`}>Coverage gap: {gap.reason}</li>)}</ul> : null}
      <div className="relative overflow-x-auto rounded-xl border border-border bg-surface-subtle p-2">
        <svg viewBox={`0 0 ${WIDTH} ${HEIGHT}`} role="img" aria-label={`${label} over time`} className="block h-auto min-w-[36rem] max-w-full">
          {yTicks.map((tick) => {
            const y = PADDING_TOP + (1 - tick / maxValue) * plotHeight
            return <g key={`y:${tick}`}><line x1={PADDING_LEFT} y1={y} x2={WIDTH - PADDING_RIGHT} y2={y} stroke="var(--mod-color-border)" strokeDasharray={tick === 0 ? undefined : '3 4'} /><text x={PADDING_LEFT - 8} y={y + 4} textAnchor="end" fill="var(--mod-color-fg-muted)" fontSize="11">{tick.toLocaleString(undefined, { maximumFractionDigits: 1 })}</text></g>
          })}
          {xTicks.map((tick, index) => {
            const x = PADDING_LEFT + ((tick - minTime) / timeSpan) * plotWidth
            return <text key={`x:${tick}:${index}`} x={x} y={HEIGHT - 22} textAnchor={index === 0 ? 'start' : index === xTicks.length - 1 ? 'end' : 'middle'} fill="var(--mod-color-fg-muted)" fontSize="11">{formatAxisTime(tick, minTime, maxTime, locale, hour12)}</text>
          })}
          <text x={PADDING_LEFT + plotWidth / 2} y={HEIGHT - 5} textAnchor="middle" fill="var(--mod-color-fg-muted)" fontSize="11">Time</text>
          <text transform={`translate(12 ${PADDING_TOP + plotHeight / 2}) rotate(-90)`} textAnchor="middle" fill="var(--mod-color-fg-muted)" fontSize="11">{valueLabel}</text>
          {paths.map((path, index) => <path key={index} data-series-segment d={path} fill="none" stroke="var(--mod-color-chart-series)" strokeWidth="2" vectorEffect="non-scaling-stroke" />)}
          {coordinates.map(({ point, x, y }, index) => <g key={`${point.ts}:${index}`}><circle cx={x} cy={y} r="4" fill="var(--mod-color-chart-series)" stroke="var(--mod-color-surface)" strokeWidth="2" /><circle role="button" tabIndex={0} aria-label={formatPoint(point, valueLabel)} data-point-x={x} data-point-y={y} cx={x} cy={y} r="16" fill="transparent" onFocus={() => setActiveIndex(index)} onBlur={() => setActiveIndex(null)} onMouseEnter={() => setActiveIndex(index)} onMouseLeave={() => setActiveIndex(null)} /></g>)}
        </svg>
        {active ? <output className="absolute left-3 top-3 rounded-md border border-border bg-surface px-2 py-1 text-xs tabular-nums text-fg shadow-sm">{formatPoint(active, valueLabel)}</output> : null}
      </div>
      <details className="mt-2 text-sm text-fg-muted">
        <summary className="cursor-pointer rounded-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent">View data table</summary>
        <DataTable caption={`${label} values`} columns={tableColumns} rows={valid} getRowId={(point) => point.ts} />
      </details>
    </figure>
  )
}
