/**
 * Pure-function utilities shared by:
 *   - the Phase-0 benchmark harness (computes aggregate accuracy)
 *   - DesktopTool runtime (decides whether a coord landed in-target)
 *   - audit-log readers (computes hit-rate over time windows)
 *
 * No I/O, no provider calls, no state. Easy to unit-test.
 *
 * @typedef {import('./types.js').Point} Point
 * @typedef {import('./types.js').Rect}  Rect
 */

/** Parse the canonical "x=<int>, y=<int>" answer shape. */
const COORD_PATTERN = /x\s*=\s*(-?\d+)\D+y\s*=\s*(-?\d+)/i;

/**
 * Extract pixel coords from a model's free-text answer. Returns null
 * if no coord pair found — caller decides whether to fall back to
 * tool_calls or re-prompt.
 *
 * @param {string} answer
 * @returns {Point | null}
 */
export function parseCoords(answer) {
  if (typeof answer !== 'string' || !answer) return null;
  const m = COORD_PATTERN.exec(answer);
  if (!m) return null;
  const x = Number(m[1]);
  const y = Number(m[2]);
  if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
  return { x, y };
}

/** Euclidean distance between two points. */
export function distance(a, b) {
  if (!isPoint(a) || !isPoint(b)) return NaN;
  const dx = a.x - b.x;
  const dy = a.y - b.y;
  return Math.sqrt(dx * dx + dy * dy);
}

/** Center of a rect, as a Point. */
export function rectCenter(r) {
  return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
}

/**
 * Does the point fall inside (or on the boundary of) the rect?
 * Used by the bench to decide HIT vs MISS.
 */
export function pointInRect(p, r) {
  if (!isPoint(p) || !isRect(r)) return false;
  return p.x >= r.x && p.x <= r.x + r.width
      && p.y >= r.y && p.y <= r.y + r.height;
}

/**
 * Aggregate stats over an array of per-trial results. Used by the
 * bench to summarise a sweep (e.g. "config=low, n=12 trials, hit-rate=92%").
 *
 * @param {{ err?: number, hit?: boolean, elapsedMs?: number, tokens?: number }[]} trials
 * @returns {{
 *   n: number,
 *   hitRate: number,
 *   meanErrPx: number,
 *   stdevErrPx: number,
 *   meanLatencyMs: number,
 *   p50LatencyMs: number,
 *   p95LatencyMs: number,
 *   meanTokens: number,
 * }}
 */
export function summarise(trials) {
  if (!Array.isArray(trials) || trials.length === 0) {
    return zeroSummary();
  }
  const errs    = trials.map(t => t.err).filter(Number.isFinite);
  const lats    = trials.map(t => t.elapsedMs).filter(Number.isFinite);
  const toks    = trials.map(t => t.tokens).filter(Number.isFinite);
  const hits    = trials.filter(t => t.hit === true).length;
  return {
    n: trials.length,
    hitRate:        trials.length ? hits / trials.length : 0,
    meanErrPx:      mean(errs),
    stdevErrPx:     stdev(errs),
    meanLatencyMs:  mean(lats),
    p50LatencyMs:   percentile(lats, 0.5),
    p95LatencyMs:   percentile(lats, 0.95),
    meanTokens:     mean(toks),
  };
}

// ── helpers (deliberately not exported — implementation detail) ───────────────

function isPoint(p) {
  return p && typeof p === 'object' && Number.isFinite(p.x) && Number.isFinite(p.y);
}
function isRect(r) {
  return r && typeof r === 'object'
    && Number.isFinite(r.x) && Number.isFinite(r.y)
    && Number.isFinite(r.width) && Number.isFinite(r.height);
}
function mean(xs) {
  return xs.length ? xs.reduce((s, v) => s + v, 0) / xs.length : 0;
}
function stdev(xs) {
  if (xs.length < 2) return 0;
  const m = mean(xs);
  const v = xs.reduce((s, x) => s + (x - m) ** 2, 0) / (xs.length - 1);
  return Math.sqrt(v);
}
function percentile(xs, p) {
  if (!xs.length) return 0;
  const sorted = [...xs].sort((a, b) => a - b);
  const idx = Math.min(sorted.length - 1, Math.max(0, Math.round(p * (sorted.length - 1))));
  return sorted[idx];
}
function zeroSummary() {
  return { n: 0, hitRate: 0, meanErrPx: 0, stdevErrPx: 0,
    meanLatencyMs: 0, p50LatencyMs: 0, p95LatencyMs: 0, meanTokens: 0 };
}
