// Learned roaster behavior, aggregated from every finished roast the user has uploaded
// (actual_roasts.parsed). Purely deterministic — medians and averages, no model involved —
// so the same profile can ground the plan curve, the plan chat, and roast evaluations
// without drift. Complements shared/learn.js, which learns pace from worksheet planActual
// entries; this learns from real telemetry.

const median = (values) => {
  const sorted = values.filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
  if (!sorted.length) return null;
  const mid = Math.floor(sorted.length / 2);
  const value = sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
  return Math.round(value * 10) / 10;
};

function avgRor(curve, fromS, toS) {
  const pts = (curve ?? []).filter((p) => p.t >= fromS && p.t <= toS);
  if (pts.length < 2) return null;
  const first = pts[0];
  const last = pts[pts.length - 1];
  if (last.t <= first.t) return null;
  return ((last.bt - first.bt) / (last.t - first.t)) * 60;
}

/**
 * @param {object[]} parsedRoasts array of parseAlog() outputs (actual_roasts.parsed)
 * @returns compact profile of how this user's machine actually behaves, or {n: 0}.
 */
export function computeRoasterProfile(parsedRoasts) {
  const roasts = (parsedRoasts ?? []).filter((p) => p && Array.isArray(p.curve));
  if (!roasts.length) return { n: 0 };

  const milestone = (p, key) => p.milestones?.find((m) => m.key === key) ?? null;
  const collect = (fn) => roasts.map(fn);

  const chargeTemps = collect((p) => p.curve.find((pt) => pt.t >= 0)?.bt);
  const tpTimes = collect((p) => p.turningPoint?.timeS);
  const tpTemps = collect((p) => p.turningPoint?.tempC);
  const yellowTimes = collect((p) => milestone(p, "yellow")?.timeS);
  const yellowTemps = collect((p) => milestone(p, "yellow")?.tempC);
  const fcTimes = collect((p) => milestone(p, "fc")?.timeS);
  const fcTemps = collect((p) => milestone(p, "fc")?.tempC);
  const dropTimes = collect((p) => milestone(p, "drop")?.timeS);
  const dropTemps = collect((p) => milestone(p, "drop")?.tempC);
  const dtrs = collect((p) => p.derived?.dtrPct);
  const losses = collect((p) => p.roast?.weightLossPct);

  const dryingRor = [];
  const maillardRor = [];
  const developmentRor = [];
  for (const p of roasts) {
    const yellow = milestone(p, "yellow");
    const fc = milestone(p, "fc");
    const drop = milestone(p, "drop");
    if (yellow) dryingRor.push(avgRor(p.curve, 60, yellow.timeS));
    if (yellow && fc) maillardRor.push(avgRor(p.curve, yellow.timeS, fc.timeS));
    if (fc && drop) developmentRor.push(avgRor(p.curve, fc.timeS, drop.timeS));
  }

  return {
    n: roasts.length,
    medians: {
      chargeTempC: median(chargeTemps),
      // Turning-point time is the practical "thermal lag" of the machine: how long charged
      // energy takes to reverse the probe dip. Deep/late TPs mean slow heat response.
      turningPointS: median(tpTimes),
      turningPointTempC: median(tpTemps),
      yellowS: median(yellowTimes),
      yellowTempC: median(yellowTemps),
      firstCrackS: median(fcTimes),
      firstCrackTempC: median(fcTemps),
      dropS: median(dropTimes),
      dropTempC: median(dropTemps),
      dtrPct: median(dtrs),
      weightLossPct: median(losses),
    },
    rorCPerMin: {
      drying: median(dryingRor),
      maillard: median(maillardRor),
      development: median(developmentRor),
    },
  };
}
