/**
 * Formatting and environment hooks shared by every shell component.
 *
 * Numbers on this site are evidence, so formatting is centralised: an exec who
 * sees `0.81` in one panel and `0.812` in the next assumes one of them is
 * rounded in someone's favour. Everything that renders a recorded number goes
 * through here, and everything that renders one wears `.nums`.
 */
import { useCallback, useSyncExternalStore } from 'react';

/**
 * A missing recorded number is an em dash, never a zero. `0` is a measurement;
 * `—` is the absence of one, and the difference is the whole point of the site.
 */
export const DASH = '—';

export function formatNumber(value: number, digits = 3): string {
  if (!Number.isFinite(value)) return DASH;
  return value.toFixed(digits);
}

export function formatOrDash(value: number | null | undefined, digits = 3): string {
  return value === null || value === undefined ? DASH : formatNumber(value, digits);
}

export function formatInt(value: number | null | undefined): string {
  if (value === null || value === undefined || !Number.isFinite(value)) return DASH;
  // en-US grouping is pinned rather than taken from the visitor: the page is
  // prerendered, and a locale-dependent separator makes the built HTML and the
  // hydrated DOM disagree.
  return value.toLocaleString('en-US');
}

export function formatMs(ms: number | null | undefined): string {
  if (ms === null || ms === undefined || !Number.isFinite(ms)) return DASH;
  if (ms < 1000) return `${Math.round(ms)} ms`;
  return `${(ms / 1000).toFixed(ms < 10_000 ? 2 : 1)} s`;
}

/**
 * The verification delta. Seven decimals is not decoration: it is the number
 * that tells a sceptical engineer we compared floats rather than strings.
 */
export function formatDelta(delta: number): string {
  if (!Number.isFinite(delta)) return DASH;
  // -0 prints as "-0.0000000" and reads like a failure. Normalise it.
  const normalised = Object.is(delta, -0) ? 0 : delta;
  return normalised.toFixed(7);
}

export function formatSigned(value: number, digits = 2): string {
  if (!Number.isFinite(value)) return DASH;
  const sign = value > 0 ? '+' : '';
  return `${sign}${value.toFixed(digits)}`;
}

/**
 * `2026-08-12` and `2026-08-12T09:31:00Z` both render as `12 Aug 2026`.
 * Formatted in UTC on purpose: a bare ISO date parses as midnight UTC, and a
 * visitor west of Greenwich would otherwise see the day before the capture.
 */
export function formatDate(iso: string): string {
  const date = new Date(iso);
  if (Number.isNaN(date.getTime())) return iso;
  return date.toLocaleDateString('en-GB', {
    day: 'numeric',
    month: 'short',
    year: 'numeric',
    timeZone: 'UTC',
  });
}

/** `finish_reason` values are snake_case off the wire; humans read spaces. */
export function humaniseToken(token: string): string {
  return token.replace(/[_-]+/g, ' ');
}

/**
 * Media queries as React state. `useSyncExternalStore` rather than an effect,
 * because the server snapshot is explicit: the prerendered HTML is built at the
 * desktop, motion-allowed default and corrects itself on the client.
 */
export function useMediaQuery(query: string, serverValue = false): boolean {
  const subscribe = useCallback(
    (onChange: () => void) => {
      if (typeof window === 'undefined' || !window.matchMedia) return () => undefined;
      const list = window.matchMedia(query);
      list.addEventListener('change', onChange);
      return () => list.removeEventListener('change', onChange);
    },
    [query],
  );
  const getSnapshot = useCallback(() => {
    if (typeof window === 'undefined' || !window.matchMedia) return serverValue;
    return window.matchMedia(query).matches;
  }, [query, serverValue]);
  return useSyncExternalStore(subscribe, getSnapshot, () => serverValue);
}

/**
 * Reduced motion is not only a CSS concern here — the reasoning stream is a JS
 * timer and has to resolve to its final state immediately. Re-exported from the
 * player rather than reimplemented: two subscriptions to the same media query
 * can disagree for a frame, and the frame they disagree on is the one where an
 * animation starts.
 */
export { usePrefersReducedMotion } from '@/lib/demo-kit/player';

/** The one breakpoint the shell branches on: the drawer/panel split. */
export function useIsDesktop(): boolean {
  return useMediaQuery('(min-width: 1024px)', true);
}

/** Clamp that also copes with a NaN out of `Number(searchParam)`. */
export function clampIndex(value: number, length: number): number {
  if (!Number.isFinite(value) || length <= 0) return 0;
  return Math.min(Math.max(Math.trunc(value), 0), length - 1);
}
