// Pure (DB-free) scheduling math for dynamic PM. The PM service feeds it plain objects so it can be
// unit-tested in isolation. The core idea: a PM's next-due is the EARLIEST of every enabled signal —
// the calendar interval (optionally compressed by an age/wear curve) and a projected usage threshold.

const DAY_MS = 86400000;

function toDate(dateStr) {
  return new Date(`${String(dateStr).slice(0, 10)}T00:00:00`);
}

function dayString(date) {
  return date.toISOString().slice(0, 10);
}

function addDays(dateStr, days) {
  const d = toDate(dateStr);
  d.setDate(d.getDate() + Math.round(days));
  return dayString(d);
}

function daysBetween(aStr, bStr) {
  return (toDate(bStr).getTime() - toDate(aStr).getTime()) / DAY_MS;
}

// Convert a calendar frequency to an approximate number of days (months ≈ 30, year = 365) so the
// lifespan factor can scale it smoothly. Mirrors the unit cases in pm.addInterval.
function intervalToDays(value, unit) {
  const v = Math.max(1, Number(value) || 1);
  switch (unit) {
    case 'days': return v;
    case 'weeks': return v * 7;
    case 'quarters': return v * 91;
    case 'semiannual': return v * 182;
    case 'years': return v * 365;
    case 'months':
    default: return v * 30;
  }
}

function clamp(n, min, max) {
  return Math.min(max, Math.max(min, n));
}

// Wear curve: returns an interval multiplier in [minFactor, 1]. New equipment (ageFraction 0) keeps the
// full interval (1.0); at/after end of estimated life (ageFraction ≥ 1) it shrinks to minFactor. The
// exponent shapes the knee — exponent 1 is linear, higher exponents hold the base cadence longer then
// drop sharply near end of life.
function lifespanFactor(ageFraction, { minFactor = 0.5, exponent = 2 } = {}) {
  const frac = clamp(Number(ageFraction) || 0, 0, 1);
  const floor = clamp(Number(minFactor) || 0, 0.05, 1);
  const exp = Math.max(0.1, Number(exponent) || 1);
  return 1 - (1 - floor) * Math.pow(frac, exp);
}

// Age fraction of an asset's estimated life at a given date (0 = brand new, 1 = at end of life).
function ageFraction(inServiceDate, usefulLifeMonths, asOfStr) {
  const months = Number(usefulLifeMonths);
  if (!inServiceDate || !months || months <= 0) return 0;
  const elapsedDays = Math.max(0, daysBetween(inServiceDate, asOfStr));
  const lifeDays = months * 30;
  return clamp(elapsedDays / lifeDays, 0, 1);
}

// Usage per day inferred from a meter's readings (chronological [{ reading, reading_date }]). Uses the
// span between the earliest and latest reading; needs ≥2 readings over a positive number of days.
function usageRatePerDay(readings) {
  if (!Array.isArray(readings) || readings.length < 2) return null;
  const sorted = [...readings].sort((a, b) => toDate(a.reading_date) - toDate(b.reading_date));
  const first = sorted[0];
  const last = sorted[sorted.length - 1];
  const days = daysBetween(first.reading_date, last.reading_date);
  const delta = Number(last.reading) - Number(first.reading);
  if (days <= 0 || delta <= 0) return null;
  return delta / days;
}

// Projected date a meter reaches its threshold (due_at_reading). Already crossed → today; otherwise
// today + remaining/ratePerDay. Returns null when there is no threshold or no usable rate.
function projectUsageDue(meter, readings, todayStr) {
  if (!meter || meter.due_at_reading == null) return null;
  const current = meter.last_reading == null ? meter.baseline_reading : meter.last_reading;
  if (current != null && Number(current) >= Number(meter.due_at_reading)) return todayStr;
  const rate = usageRatePerDay(readings);
  if (!rate || rate <= 0) return null;
  const remaining = Number(meter.due_at_reading) - Number(current || 0);
  return addDays(todayStr, Math.ceil(remaining / rate));
}

function earliest(dates) {
  const valid = dates.filter(Boolean).sort();
  return valid.length ? valid[0] : null;
}

// Compose the next-due date from every enabled signal and return the winner plus the per-signal
// breakdown (so the UI can explain WHY a date was chosen).
//   schedule: { lastDate, frequency_value, frequency_unit, lifespan_adjust }
//   asset:    { in_service_date, useful_life_months }
//   meters:   [{ ...meter, readings: [...] }]
//   settings: { usage_enabled, curve_enabled, min_factor, curve_exponent }
function computeNextDue(schedule, asset, meters, settings, today) {
  const lastDate = schedule.lastDate || today;
  const baseDays = intervalToDays(schedule.frequency_value, schedule.frequency_unit);

  let factor = 1;
  if (settings.curve_enabled && schedule.lifespan_adjust) {
    const frac = ageFraction(asset.in_service_date, asset.useful_life_months, lastDate);
    factor = lifespanFactor(frac, { minFactor: settings.min_factor, exponent: settings.curve_exponent });
  }
  const calendarDue = addDays(lastDate, baseDays * factor);

  let usageDue = null;
  if (settings.usage_enabled && Array.isArray(meters)) {
    const projections = meters
      .filter((m) => Number(m.usage_interval) > 0)
      .map((m) => projectUsageDue(m, m.readings || [], today));
    usageDue = earliest(projections);
  }

  const lifespanDue = factor < 1 ? calendarDue : null; // surfaced separately for transparency
  const nextDue = earliest([calendarDue, usageDue]);
  return {
    next_due_date: nextDue,
    signals: { calendar: calendarDue, usage: usageDue, lifespan: lifespanDue, factor },
    driver: nextDue === usageDue && usageDue ? 'usage' : (factor < 1 ? 'lifespan' : 'calendar')
  };
}

module.exports = {
  addDays,
  ageFraction,
  computeNextDue,
  daysBetween,
  intervalToDays,
  lifespanFactor,
  projectUsageDue,
  usageRatePerDay
};
