// Pure helpers for exporting daily-parameter data as a CSV matrix. The export is a
// subject-series matrix: subjects are always columns, the chosen X coordinate
// (Date / # Days Reach / a daily field) provides the rows, and one Data
// parameter supplies the cell values.
// No React, no I/O — mirrors the crossSubjectChart.js pure-helper pattern.

// Read a single parameter's raw value from one daily status.
// Returns null when the value is absent. Note: 0 and '' are returned as-is
// (real values); only genuinely-missing lookups become null.
export function extractValue(status, param) {
  if (!status || !param) return null;
  switch (param.kind) {
    case 'total':
      return status.analysis_summary?.total ?? null;
    case 'success_rate':
      return status.analysis_summary?.success_rate ?? null;
    case 'category':
      return status.analysis_summary?.counts?.[param.category] ?? null;
    case 'builtin':
      return status[param.statusKey] ?? null;
    case 'custom':
      return status.custom_fields?.[param.fieldId] ?? null;
    default:
      return null;
  }
}

// Build the grouped, ordered list of exportable parameters.
// Session metrics (total, success_rate, then each dynamic count category found
// in the data) come first; then every field defined in the daily template,
// including inactive ones (historical data may exist).
export function listExportableParams(dailyTemplate, statuses) {
  const params = [
    { id: '__total__', label: 'Total attempts', group: 'metrics', kind: 'total' },
    { id: '__success_rate__', label: 'Success rate', group: 'metrics', kind: 'success_rate' },
  ];

  const cats = new Set();
  for (const s of statuses ?? []) {
    const counts = s?.analysis_summary?.counts;
    if (counts && typeof counts === 'object') {
      for (const k of Object.keys(counts)) cats.add(k);
    }
  }
  for (const cat of [...cats].sort()) {
    params.push({ id: `__cat__${cat}`, label: cat, group: 'metrics', kind: 'category', category: cat });
  }

  for (const f of dailyTemplate ?? []) {
    if (f.builtin) {
      params.push({ id: `b:${f.fieldId}`, label: f.label, group: 'daily', kind: 'builtin', statusKey: f.key });
    } else {
      params.push({ id: `c:${f.fieldId}`, label: f.label, group: 'daily', kind: 'custom', fieldId: f.fieldId });
    }
  }

  return params;
}

// Numeric value of a daily field on one status (mirrors the metrics plot's helper).
export function numericFieldVal(status, field) {
  if (!field) return null;
  const raw = field.builtin ? status?.[field.key] : status?.custom_fields?.[field.fieldId];
  const n = parseFloat(raw);
  return Number.isNaN(n) ? null : n;
}

// Ordered row members for the chosen X coordinate.
// xField: '__date__' | '__days__' | <daily fieldId>
export function listSampleMembers(statuses, xField, dailyTemplate) {
  const list = statuses ?? [];
  if (xField === '__date__') {
    const keys = new Set();
    for (const s of list) if (s?.date) keys.add(dateKey(s.date));
    return [...keys].sort().map((k) => ({ id: k, label: k, sampleValue: k }));
  }
  if (xField === '__days__') {
    const counts = new Map();
    for (const s of list) {
      if (s?.animal_id == null || !s?.date) continue;
      counts.set(s.animal_id, (counts.get(s.animal_id) ?? 0) + 1);
    }
    const max = counts.size ? Math.max(...counts.values()) : 0;
    return Array.from({ length: max }, (_, i) => ({ id: String(i + 1), label: String(i + 1), sampleValue: i + 1 }));
  }
  const field = (dailyTemplate ?? []).find((f) => f.fieldId === xField);
  const vals = new Set();
  for (const s of list) {
    const v = numericFieldVal(s, field);
    if (v !== null) vals.add(v);
  }
  return [...vals].sort((a, b) => a - b).map((v) => ({ id: String(v), label: String(v), sampleValue: v }));
}

// animalId -> Map(sampleValue -> status). First status per (subject, sampleValue) wins,
// scanning each subject's statuses in ascending date order.
export function buildSampleIndex(statuses, xField, dailyTemplate) {
  const bySubject = new Map();
  for (const s of statuses ?? []) {
    if (s?.animal_id == null || !s?.date) continue;
    if (!bySubject.has(s.animal_id)) bySubject.set(s.animal_id, []);
    bySubject.get(s.animal_id).push(s);
  }
  const field = xField !== '__date__' && xField !== '__days__'
    ? (dailyTemplate ?? []).find((f) => f.fieldId === xField)
    : null;

  const index = new Map();
  for (const [animalId, subjStatuses] of bySubject) {
    const ordered = [...subjStatuses].sort((a, b) => dateKey(a.date).localeCompare(dateKey(b.date)));
    const m = new Map();
    ordered.forEach((s, i) => {
      let key;
      if (xField === '__date__') key = dateKey(s.date);
      else if (xField === '__days__') key = i + 1;
      else { const v = numericFieldVal(s, field); if (v === null) return; key = v; }
      if (!m.has(key)) m.set(key, s);
    });
    index.set(animalId, m);
  }
  return index;
}

function xFieldLabel(xField, dailyTemplate) {
  if (xField === '__date__') return 'Date';
  if (xField === '__days__') return '# Days Reach (ordinal)';
  return (dailyTemplate ?? []).find((f) => f.fieldId === xField)?.label ?? '';
}

// Subjects as columns, X-field values as rows, one Data value per cell.
// Returns the { context, corner, groupAxis, columns, rows } shape toCSV consumes.
export function buildSubjectSeriesMatrix({ xField, dataParam, groupField }, { statuses, animals, dailyTemplate }) {
  const columns = listSubjectMembers(animals, groupField);
  const rowMembers = listSampleMembers(statuses, xField, dailyTemplate);
  const index = buildSampleIndex(statuses, xField, dailyTemplate);

  const cellValue = (sampleValue, animalId) => {
    const status = index.get(animalId)?.get(sampleValue);
    if (!status || !dataParam) return '';
    const v = extractValue(status, dataParam);
    return hasValue(v) ? v : '';
  };

  let rows = rowMembers.map((rm) => ({
    member: rm,
    values: columns.map((c) => cellValue(rm.sampleValue, c.animalId)),
  }));
  rows = rows.filter((r) => r.values.some((v) => v !== '')); // drop entirely-blank sample rows

  const grouped = !!groupField && groupField !== '__none__';
  return {
    corner: xFieldLabel(xField, dailyTemplate),
    context: { label: 'Data', value: dataParam?.label ?? '' },
    groupAxis: grouped ? 'col' : null,
    columns,
    rows,
  };
}

// Resolve a subject's group value for a given group field id.
// null  -> no grouping (field is falsy or '__none__')
// '—'   -> grouping is on but this subject has no value
export function subjectGroupValue(animal, groupField) {
  if (!groupField || groupField === '__none__') return null;
  if (groupField === '__name__') return animal?.animal_name ?? '—';
  if (groupField === '__id__') return animal?.animal_id_string ?? '—';
  const v = animal?.subject_info?.[groupField];
  return v === null || v === undefined || v === '' ? '—' : v;
}

export function listSubjectMembers(animals, groupField) {
  const list = [...(animals ?? [])];
  const nameCounts = new Map();
  for (const a of list) {
    const n = a?.animal_name ?? '';
    nameCounts.set(n, (nameCounts.get(n) ?? 0) + 1);
  }
  const members = list.map((a) => {
    const name = a?.animal_name ?? '';
    const label = nameCounts.get(name) > 1 ? `${name} (${a?.animal_id_string ?? ''})` : name;
    return { id: a?.id, label, animalId: a?.id, group: subjectGroupValue(a, groupField) };
  });
  const grouped = !!groupField && groupField !== '__none__';
  members.sort((x, y) => {
    if (grouped) {
      const g = String(x.group ?? '').localeCompare(String(y.group ?? ''));
      if (g !== 0) return g;
    }
    return String(x.label).localeCompare(String(y.label));
  });
  return members;
}

// A cell has a value unless it is null, undefined, or the empty string.
// (0 and false are real values.)
function hasValue(v) {
  return v !== null && v !== undefined && v !== '';
}

function dateKey(date) {
  return String(date).slice(0, 10); // ISO datetime or date → YYYY-MM-DD
}

function escapeCsvCell(value) {
  const s = value === null || value === undefined ? '' : String(value);
  return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}

// Serialize a matrix to an RFC-4180-ish CSV string (LF line endings):
//   Data,<metric>
//   (blank line)
//   [Group,<group per subject column>]    -- only when groupAxis === 'col'
//   <corner>[,Group?],<column headers...>
//   <row label>[,group?],<values...>
export function toCSV(matrix) {
  const lines = [];
  lines.push([matrix.context.label, matrix.context.value].map(escapeCsvCell).join(','));
  lines.push('');

  if (matrix.groupAxis === 'col') {
    const groupRow = ['Group', ...matrix.columns.map((c) => c.group ?? '—')];
    lines.push(groupRow.map(escapeCsvCell).join(','));
  }

  // toCSV is a general serializer; the subject-series builder never emits groupAxis
  // === 'row' (only 'col' or null), but the branch is retained for that generality.
  const header = matrix.groupAxis === 'row'
    ? ['Group', matrix.corner, ...matrix.columns.map((c) => c.label)]
    : [matrix.corner, ...matrix.columns.map((c) => c.label)];
  lines.push(header.map(escapeCsvCell).join(','));

  for (const r of matrix.rows) {
    const lead = matrix.groupAxis === 'row'
      ? [r.member.group ?? '—', r.member.label]
      : [r.member.label];
    lines.push([...lead, ...r.values].map(escapeCsvCell).join(','));
  }

  return lines.join('\n');
}

// Build a download filename from the experiment title and parameter label.
export function csvFilename(title, label) {
  const slug = (s) => String(s ?? '').replace(/[^a-z0-9]+/gi, '-').replace(/^-+|-+$/g, '');
  const base = [slug(title), slug(label)].filter(Boolean).join('-');
  return `${base || 'export'}.csv`;
}
