// Formatting + maintenance-status helpers. The status logic follows the
// spreadsheet idea: a service is "due" when its computed next-service date
// (service date + interval) approaches/passes today, OR when the car's current
// odometer approaches/passes the computed next-service km.

import { prefs } from "../prefs.js";
import { t } from "../i18n/index.js";

export function formatDate(value) {
  if (!value) return "—";
  const d = new Date(value);
  if (isNaN(d)) return "—";

  const day = String(d.getDate()).padStart(2, "0");
  const month = String(d.getMonth() + 1).padStart(2, "0");
  const monthName = d.toLocaleDateString(prefs.locale || undefined, { month: "short" });
  const year = d.getFullYear();

  switch (prefs.dateFormat) {
    case "DMY_NUM":
      return `${day}-${month}-${year}`;
    case "DMY":
      return `${day} ${monthName} ${year}`;
    case "MDY":
      return `${monthName} ${day}, ${year}`;
    case "YMD":
    default:
      return `${year}-${month}-${day}`;
  }
}

// A date somebody may only half know. A car's build date is often "2015", or
// "March 2015", and no more than that — the plate carries a month, the papers a
// day, a grey import neither. The value is an ISO 8601 reduced-precision date:
// "2015", "2015-03" or "2015-03-10", and each prints to exactly its own
// precision. Filling the missing parts in with 01 would show the reader two
// numbers nobody supplied.
//
// Split by string rather than run through a Date: "2015-03" parses as UTC
// midnight and reads back in local time, which west of Greenwich hands back
// February. Anything that isn't a reduced-precision date — a full timestamp
// from an older record — falls through to formatDate, which is where it was
// being rendered before.
export function formatPartialDate(value) {
  if (!value) return "—";
  const parts = /^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.exec(String(value).trim());
  if (!parts) return formatDate(value);
  const [, year, month, day] = parts;
  if (day) return formatDate(value);
  if (!month) return year;

  // Month and year. The month's name where the user's format spells months out,
  // its number where the format is numeric, and always in the order the rest of
  // the app puts them in.
  const monthName = new Date(Date.UTC(Number(year), Number(month) - 1, 1)).toLocaleDateString(
    prefs.locale || undefined,
    { month: "short", timeZone: "UTC" }
  );
  switch (prefs.dateFormat) {
    case "DMY_NUM":
      return `${month}-${year}`;
    case "DMY":
    case "MDY":
      return `${monthName} ${year}`;
    case "YMD":
    default:
      return `${year}-${month}`;
  }
}

// A timestamp rather than a date: the date in the user's chosen format plus the
// clock time in their region's convention. For the places where freshness is the
// whole point — a live reading pulled from a manufacturer service means little
// without the minute it was taken.
export function formatDateTime(value) {
  if (!value) return "—";
  const d = new Date(value);
  if (isNaN(d)) return "—";
  return `${formatDate(value)} ${formatTime(d)}`;
}

// The clock alone. "auto" leaves the reading to the region, which is what every
// time in the app said before there was a setting; the other two are for the
// people whose region and habit disagree — plenty of Poles read 12-hour clocks
// and plenty of Americans read 24-hour ones, and the region picker also decides
// how money and numbers are grouped, so it is the wrong lever to reach for.
//
// hourCycle rather than hour12: with hour12:false the en-US formatter prints
// midnight as 24:00.
export function formatTime(value) {
  if (!value) return "—";
  const d = value instanceof Date ? value : new Date(value);
  if (isNaN(d)) return "—";
  const opts = { hour: "2-digit", minute: "2-digit" };
  if (prefs.timeFormat === "24") opts.hourCycle = "h23";
  else if (prefs.timeFormat === "12") opts.hourCycle = "h12";
  return d.toLocaleTimeString(prefs.locale || undefined, opts);
}

// Every number we render goes through here so the grouping separator follows
// the user's chosen region rather than the browser's own locale — otherwise the
// odometer disagrees with the dates and costs beside it.
function num(value) {
  return Number(value).toLocaleString(prefs.locale || undefined);
}

// 0 prints as "0 km", not "—": a car picked up new has an odometer reading, and
// blanking it hides the very number the first service interval counts from.
// Nothing stores a placeholder 0 for the intervals — the server defaults those
// (applyCarDefaults) — so a zero reaching here is a real reading.
export function formatKm(value) {
  if (value == null || value === "") return "—";
  return num(value) + " km";
}

const DAY = 24 * 60 * 60 * 1000;
const KM_SOON = 1000; // within 1000 km of due => "soon"

// daysUntil returns whole days from today to the given date (negative = past).
export function daysUntil(value) {
  if (!value) return null;
  const target = new Date(value);
  if (isNaN(target)) return null;
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  target.setHours(0, 0, 0, 0);
  return Math.round((target - today) / DAY);
}

// Severity ranking so we can pick the worst of the date/km signals.
const RANK = { unknown: 0, ok: 1, soon: 2, overdue: 3 };
// DriverVault status language: On track (green) / Due soon (amber) / Action
// needed (red). Uses the shared badge recipes from style.css so tints flip in
// dark mode automatically.
const STYLE = {
  unknown: "dh-badge dh-badge-neutral",
  ok: "dh-badge dh-badge-success",
  soon: "dh-badge dh-badge-warning",
  overdue: "dh-badge dh-badge-danger",
};

// dateSignal classifies the next-due date relative to today.
function dateSignal(nextServiceDate) {
  const days = daysUntil(nextServiceDate);
  if (days == null) return { key: "unknown", label: t("status.noData") };
  if (days < 0) return { key: "overdue", days, label: t("status.serviceOverdueDays", { days: Math.abs(days) }) };
  if (days <= 30) return { key: "soon", days, label: t("status.dueInDays", { days }) };
  return { key: "ok", days, label: t("status.okDays", { days }) };
}

// kmSignal classifies the current odometer against the next-due km. Both values
// are tested for absence rather than truthiness: 0 km is where a new car starts,
// and reading that as "no data" strands the km signal until the first drive.
function kmSignal(currentKm, nextServiceKm) {
  if (currentKm == null || nextServiceKm == null) return { key: "unknown", label: t("status.noKm") };
  const remaining = nextServiceKm - currentKm;
  if (remaining < 0) return { key: "overdue", remaining, label: t("status.serviceOverdueKm", { km: num(Math.abs(remaining)) }) };
  if (remaining <= KM_SOON) return { key: "soon", remaining, label: t("status.inKm", { km: num(remaining) }) };
  return { key: "ok", remaining, label: t("status.kmLeft", { km: num(remaining) }) };
}

// serviceStatus combines the date- and km-based signals, returning the worse of
// the two for the badge. `latest` is the most recent service record (with
// nextServiceDate/nextServiceKm); `car` carries the current odometer.
// formatLiters / formatMoney / formatConsumption render the fuel figures. The
// server sends null for anything it could not derive (a window with a missed
// fill, a first-ever tank), which reads as "—" rather than a misleading zero.
export function formatLiters(value) {
  if (value == null || value === "") return "—";
  return Number(value).toFixed(2) + " L";
}

// Amounts are stored as plain numbers; the user's currency setting only decides
// how they are displayed. Nothing is converted — a figure entered as 40 reads as
// 40 in whichever currency is selected.
export function formatMoney(value) {
  if (value == null || value === "") return "—";
  // No explicit fraction digits: currency style already pins them to the
  // currency's minor unit, which keeps the 2 decimals the fuel figures were
  // written for while still rendering yen without phantom sen.
  return Number(value).toLocaleString(prefs.locale || undefined, {
    style: "currency",
    currency: prefs.currency || "USD",
  });
}

// One decimal: the interesting differences between tanks live in tenths, and
// rounding to whole litres collapses a best of 6.8 and a worst of 7.0 into the
// same number.
export function formatConsumption(value) {
  if (value == null) return "—";
  return Number(value).toFixed(1) + " L/100km";
}

export function formatKmPerLiter(value) {
  if (value == null) return "—";
  return Number(value).toFixed(2) + " km/L";
}

// The charging equivalents of the three above. Energy keeps two decimals like
// litres — a 7.35 kWh top-up is a real number off a charge point — and the
// consumption figure one, for the same reason tanks do.
export function formatKwh(value) {
  if (value == null || value === "") return "—";
  return Number(value).toFixed(2) + " kWh";
}

export function formatConsumptionKwh(value) {
  if (value == null) return "—";
  return Number(value).toFixed(1) + " kWh/100km";
}

export function formatKmPerKwh(value) {
  if (value == null) return "—";
  return Number(value).toFixed(2) + " km/kWh";
}

// Document renewal badge, driven by the server's expiry assessment so the client
// never re-derives the date maths.
const EXPIRY_STYLE = {
  no_expiry: "dh-badge dh-badge-neutral",
  valid: "dh-badge dh-badge-success",
  expiring_soon: "dh-badge dh-badge-warning",
  expired: "dh-badge dh-badge-danger",
};

export function expiryStatus(doc) {
  const state = doc?.expiry?.state || "no_expiry";
  const days = doc?.expiry?.daysUntilExpiry;
  let label;
  switch (state) {
    case "expired":
      label = t("status.expiredAgo", { days: Math.abs(days) });
      break;
    case "expiring_soon":
      label = days === 0 ? t("status.expiresToday") : t("status.renewInDays", { days });
      break;
    case "valid":
      label = t("status.validDays", { days });
      break;
    default:
      label = t("status.noExpiry");
  }
  return { key: state, label, classes: EXPIRY_STYLE[state] || EXPIRY_STYLE.no_expiry };
}

// Reminder badge. The server has already picked the worse of the date and
// odometer signals; this only chooses the wording, preferring whichever trigger
// is actually driving the status.
const REMINDER_STYLE = {
  done: "dh-badge dh-badge-neutral",
  no_trigger: "dh-badge dh-badge-neutral",
  upcoming: "dh-badge dh-badge-success",
  due_soon: "dh-badge dh-badge-warning",
  overdue: "dh-badge dh-badge-danger",
};

export function reminderStatus(rem) {
  const state = rem?.status || "no_trigger";
  const days = rem?.daysLeft;
  const km = rem?.kmLeft;

  let label;
  if (state === "done") label = t("status.done");
  else if (state === "no_trigger") label = t("status.noTrigger");
  else if (state === "overdue") {
    const parts = [];
    if (days != null && days < 0) parts.push(t("status.days", { days: Math.abs(days) }));
    if (km != null && km < 0) parts.push(t("status.km", { km: num(Math.abs(km)) }));
    label = parts.length ? t("status.overdueBy", { parts: parts.join(" · ") }) : t("status.overdue");
  } else {
    // Lead with the trigger that is closest to firing.
    const parts = [];
    if (days != null && days >= 0) parts.push(days === 0 ? t("status.today") : t("status.days", { days }));
    if (km != null && km >= 0) parts.push(t("status.km", { km: num(km) }));
    label = parts.length ? t("status.dueIn", { parts: parts.join(" · ") }) : t("status.upcoming");
  }
  return { key: state, label, classes: REMINDER_STYLE[state] || REMINDER_STYLE.no_trigger };
}

export function serviceStatus(latest, car = null) {
  const date = dateSignal(latest?.nextServiceDate);
  const km = kmSignal(car?.currentKm, latest?.nextServiceKm);

  const worse = RANK[km.key] > RANK[date.key] ? km : date;

  let label;
  if (date.key === "unknown" && km.key === "unknown") label = t("status.noData");
  // With one signal to go on, that signal's own sentence says it best.
  else if (date.key === "unknown") label = km.label;
  else if (km.key === "unknown") label = date.label;
  else label = bothSignals(date, km, worse.key);

  return { key: worse.key, label, classes: STYLE[worse.key], date, km };
}

// bothSignals words a badge that has a due date AND an odometer target. A
// service falls due on whichever arrives first, so "OK · 354d" on its own left
// out half of what the badge is watching: the distance still to run belongs
// beside the days. One headline for the severity, then each trigger as a bare
// quantity - the shape reminderStatus already uses, it having had the
// two-trigger problem first.
function bothSignals(date, km, key) {
  const parts = [];
  if (key === "overdue") {
    // Only what has actually passed. The other trigger is not late, and its
    // comfortable remainder under an "Overdue" headline would read as one.
    if (date.key === "overdue") parts.push(t("status.days", { days: Math.abs(date.days) }));
    if (km.key === "overdue") parts.push(t("status.km", { km: num(Math.abs(km.remaining)) }));
    return t("status.serviceOverdueBy", { parts: parts.join(" · ") });
  }
  parts.push(t("status.days", { days: date.days }));
  parts.push(t("status.km", { km: num(km.remaining) }));
  return t(key === "soon" ? "status.dueIn" : "status.okIn", { parts: parts.join(" · ") });
}
