/**
 * Column type inference (spec §05 → Profiling; docs/architecture/06 §2).
 *
 * Portable core: no browser APIs, no React — usable in Node and tests. Given the
 * values of a single column, decide which of four display types it holds:
 * `number`, `boolean`, `date`, or `string`. This is a **display hint** only —
 * nothing downstream coerces values from it, and Vega-Lite does its own type
 * handling at render time — so the rules favour being simple and predictable
 * over being clever.
 *
 * The algorithm (arch 06 §2):
 *   1. Drop empties (null/undefined/whitespace-only string) — empty cells carry
 *      no type signal.
 *   2. An all-empty (or zero-length) column is `string` — no evidence otherwise.
 *   3. Run the checks in precedence order **boolean → number → date → string**,
 *      narrowest evidence to widest; the first for which *every* present value
 *      matches wins. One stray value knocks the column down to the next candidate.
 *
 * Date detection guards with a shape regex *before* trusting `Date.parse`, which
 * on some engines accepts `"42"` or `"March"` and would swallow number/string
 * columns. `0`/`1` are numbers, never booleans.
 */

export type ColumnType = 'number' | 'string' | 'date' | 'boolean';

/** Empty cells (null/undefined/whitespace-only string) carry no type signal. */
export const isEmpty = (v: unknown): boolean =>
  v === null || v === undefined || (typeof v === 'string' && v.trim() === '');

/** Native numbers pass when finite; strings must parse to a finite, non-NaN number. */
export const isNumeric = (v: unknown): boolean => {
  if (typeof v === 'number') return Number.isFinite(v);
  if (typeof v !== 'string') return false;
  const t = v.trim();
  if (t === '') return false; // reject blank so Number("") === 0 can't sneak through
  const n = Number(t);
  return !Number.isNaN(n) && Number.isFinite(n);
};

/** Native booleans pass; otherwise the trimmed, lower-cased string is exactly true/false. */
const isBoolean = (v: unknown): boolean => {
  if (typeof v === 'boolean') return true;
  if (typeof v !== 'string') return false;
  const t = v.trim().toLowerCase();
  return t === 'true' || t === 'false';
};

/**
 * Shape guard for dates: a leading `YYYY-MM-DD`/`YYYY/MM/DD`, or `M/D/YYYY`.
 * Required *before* `Date.parse` — see the module note above.
 */
const DATE_SHAPE = /^\d{4}[-/]\d{2}[-/]\d{2}|^\d{1,2}\/\d{1,2}\/\d{4}/;
const isDate = (v: unknown): boolean => {
  if (typeof v !== 'string') return false;
  const t = v.trim();
  return DATE_SHAPE.test(t) && !Number.isNaN(Date.parse(t));
};

/**
 * Infer one of four column types from a sample of column values. Empty cells are
 * ignored; an all-empty (or zero-length) column is `string`. Precedence:
 * boolean → number → date → string.
 */
export function inferColumnType(values: readonly unknown[]): ColumnType {
  const present = values.filter((v) => !isEmpty(v));
  if (present.length === 0) return 'string';

  if (present.every(isBoolean)) return 'boolean';
  if (present.every(isNumeric)) return 'number';
  if (present.every(isDate)) return 'date';
  return 'string';
}
