// Pure helpers for the cross-subject metrics plot interactions. No React, no I/O.

// Sort a Recharts tooltip payload by numeric value descending, dropping entries
// whose value is null/undefined. Returns a NEW array (does not mutate input).
export function sortPayloadByValueDesc(payload) {
  if (!Array.isArray(payload)) return [];
  return payload
    .filter((p) => p && p.value != null)
    .sort((a, b) => b.value - a.value);
}

// Toggle an id's membership in a Set, returning a NEW Set.
export function toggleInSet(set, id) {
  const next = new Set(set);
  if (next.has(id)) next.delete(id);
  else next.add(id);
  return next;
}

// Add (hidden=true) or remove (hidden=false) a list of ids from a hidden-set.
// Returns a NEW Set.
export function setIdsHidden(set, ids, hidden) {
  const next = new Set(set);
  for (const id of ids) {
    if (hidden) next.add(id);
    else next.delete(id);
  }
  return next;
}

// Tri-state for a group checkbox given which subject ids are hidden.
// 'all' = all visible, 'none' = all hidden, 'some' = mixed.
export function groupVisibilityState(subjectIds, hiddenSet) {
  if (subjectIds.length === 0) return 'none';
  const hiddenCount = subjectIds.filter((id) => hiddenSet.has(id)).length;
  if (hiddenCount === 0) return 'all';
  if (hiddenCount === subjectIds.length) return 'none';
  return 'some';
}

// Group chart "lines" by their .group field, preserving first-seen group order
// and line order within each group. Returns [{ group, subjects: [line...] }].
export function groupLines(lines) {
  const order = [];
  const byGroup = new Map();
  for (const line of lines) {
    const g = line.group ?? '—';
    if (!byGroup.has(g)) { byGroup.set(g, []); order.push(g); }
    byGroup.get(g).push(line);
  }
  return order.map((group) => ({ group, subjects: byGroup.get(group) }));
}

// ── Plot config (de)serialization ───────────────────────────────────────────────
// Translate between ExperimentAnalysisCharts React state and the stored JSONB blob.
// readPlotConfig is defensive: it never throws and fills safe defaults for missing
// or malformed input (unknown keys are ignored).

// Symmetric with readChart so a serialize→read round-trip is lossless.
function snapshotChart(s = {}) {
  const str = (v) => (typeof v === 'string' ? v : '');
  return {
    height:         Number.isFinite(s.height) ? s.height : undefined,
    yMin:           str(s.yMin),
    yMax:           str(s.yMax),
    xZoomMin:       str(s.xZoomMin),
    xZoomMax:       str(s.xZoomMax),
    xFieldOverride: typeof s.xFieldOverride === 'string' ? s.xFieldOverride : null,
  };
}

export function serializePlotConfig(state = {}) {
  const { xField, groupBy, tickStep, metric, hiddenSubjects, sidebarOpen, counts, rate } = state;
  return {
    xField,
    groupBy,
    tickStep,
    metric,
    hiddenSubjects: [...(hiddenSubjects ?? [])].sort(),
    sidebarOpen: !!sidebarOpen,
    counts: snapshotChart(counts),
    rate:   snapshotChart(rate),
  };
}

function readChart(c) {
  const o = c && typeof c === 'object' && !Array.isArray(c) ? c : {};
  const str = (v) => (typeof v === 'string' ? v : '');
  return {
    height:         Number.isFinite(o.height) ? o.height : undefined,
    yMin:           str(o.yMin),
    yMax:           str(o.yMax),
    xZoomMin:       str(o.xZoomMin),
    xZoomMax:       str(o.xZoomMax),
    xFieldOverride: typeof o.xFieldOverride === 'string' ? o.xFieldOverride : null,
  };
}

export function readPlotConfig(raw) {
  const c = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
  return {
    xField:   typeof c.xField === 'string' ? c.xField : undefined,
    groupBy:  typeof c.groupBy === 'string' ? c.groupBy : undefined,
    tickStep: Number.isFinite(c.tickStep) ? c.tickStep : undefined,
    metric:   typeof c.metric === 'string' ? c.metric : undefined,
    hiddenSubjects: Array.isArray(c.hiddenSubjects) ? c.hiddenSubjects.filter((x) => typeof x === 'string') : [],
    sidebarOpen: c.sidebarOpen === true,
    counts: readChart(c.counts),
    rate:   readChart(c.rate),
  };
}
