import { formatQuantity } from "./fractions";

/**
 * Display-time unit conversion for recipe ingredient quantities.
 *
 * Recipes are authored in whatever units the author used. This module
 * converts weight/volume units to the viewing user's `unitPref` at render
 * time only — stored recipe data is never rewritten. Units that aren't in
 * the table below (counts, "to taste", "a pinch", "clove", cups/tsp/tbsp,
 * etc.) are rendered unconverted, since they're either unit-less or
 * genuinely ambiguous / shared across both measurement systems in cooking
 * convention.
 */

export type UnitPref = "metric" | "imperial";

type UnitKind = "weight" | "volume";

type UnitEntry = {
  system: UnitPref;
  kind: UnitKind;
  /** Conversion factor to the kind's base unit (grams for weight, ml for volume). */
  factor: number;
};

// Only units we actually want to convert live here. Anything else (cup,
// tbsp, tsp, "clove", "pinch", "to taste", ...) is intentionally omitted so
// it renders unconverted.
const UNIT_TABLE: Record<string, UnitEntry> = {
  // metric weight
  g: { system: "metric", kind: "weight", factor: 1 },
  gram: { system: "metric", kind: "weight", factor: 1 },
  grams: { system: "metric", kind: "weight", factor: 1 },
  kg: { system: "metric", kind: "weight", factor: 1000 },
  kilogram: { system: "metric", kind: "weight", factor: 1000 },
  kilograms: { system: "metric", kind: "weight", factor: 1000 },

  // imperial weight
  oz: { system: "imperial", kind: "weight", factor: 28.3495 },
  ounce: { system: "imperial", kind: "weight", factor: 28.3495 },
  ounces: { system: "imperial", kind: "weight", factor: 28.3495 },
  lb: { system: "imperial", kind: "weight", factor: 453.592 },
  lbs: { system: "imperial", kind: "weight", factor: 453.592 },
  pound: { system: "imperial", kind: "weight", factor: 453.592 },
  pounds: { system: "imperial", kind: "weight", factor: 453.592 },

  // metric volume
  ml: { system: "metric", kind: "volume", factor: 1 },
  milliliter: { system: "metric", kind: "volume", factor: 1 },
  milliliters: { system: "metric", kind: "volume", factor: 1 },
  millilitre: { system: "metric", kind: "volume", factor: 1 },
  millilitres: { system: "metric", kind: "volume", factor: 1 },
  l: { system: "metric", kind: "volume", factor: 1000 },
  liter: { system: "metric", kind: "volume", factor: 1000 },
  liters: { system: "metric", kind: "volume", factor: 1000 },
  litre: { system: "metric", kind: "volume", factor: 1000 },
  litres: { system: "metric", kind: "volume", factor: 1000 },

  // imperial volume (deliberately excludes cup/tbsp/tsp — see note above)
  "fl oz": { system: "imperial", kind: "volume", factor: 29.5735 },
  "fl. oz": { system: "imperial", kind: "volume", factor: 29.5735 },
  floz: { system: "imperial", kind: "volume", factor: 29.5735 },
  "fluid ounce": { system: "imperial", kind: "volume", factor: 29.5735 },
  "fluid ounces": { system: "imperial", kind: "volume", factor: 29.5735 },
  qt: { system: "imperial", kind: "volume", factor: 946.353 },
  quart: { system: "imperial", kind: "volume", factor: 946.353 },
  quarts: { system: "imperial", kind: "volume", factor: 946.353 },
  pt: { system: "imperial", kind: "volume", factor: 473.176 },
  pint: { system: "imperial", kind: "volume", factor: 473.176 },
  pints: { system: "imperial", kind: "volume", factor: 473.176 },
  gal: { system: "imperial", kind: "volume", factor: 3785.41 },
  gallon: { system: "imperial", kind: "volume", factor: 3785.41 },
  gallons: { system: "imperial", kind: "volume", factor: 3785.41 },
};

function normalizeUnit(unit: string): string {
  return unit.trim().toLowerCase().replace(/\.$/, "");
}

function roundTo(value: number, nearest: number): number {
  return Math.round(value / nearest) * nearest;
}

/** Formats a metric quantity (g/kg/ml/l), trimming to at most 2 decimals. */
function formatMetricNumber(value: number): string {
  const rounded = Math.round(value * 100) / 100;
  return Number.isInteger(rounded) ? String(rounded) : String(parseFloat(rounded.toFixed(2)));
}

/**
 * Converts a numeric quantity + unit string to the target unit system,
 * rounded to sensible cooking precision. Returns null when the unit isn't
 * in the conversion table, or is already in the target system (nothing to
 * convert).
 */
export function convertUnit(
  value: number,
  unit: string,
  targetPref: UnitPref
): { value: string; unit: string } | null {
  if (!isFinite(value) || value <= 0) return null;
  const entry = UNIT_TABLE[normalizeUnit(unit)];
  if (!entry || entry.system === targetPref) return null;

  const base = value * entry.factor;

  if (entry.kind === "weight") {
    if (targetPref === "metric") {
      if (base >= 1000) return { value: formatMetricNumber(roundTo(base / 1000, 0.1)), unit: "kg" };
      return { value: formatMetricNumber(roundTo(base, base < 20 ? 1 : 5)), unit: "g" };
    }
    const oz = base / 28.3495;
    if (oz >= 16) return { value: formatQuantity(roundTo(oz / 16, 0.1)), unit: "lb" };
    return { value: formatQuantity(roundTo(oz * 4, 1) / 4), unit: "oz" };
  }

  // volume
  if (targetPref === "metric") {
    if (base >= 1000) return { value: formatMetricNumber(roundTo(base / 1000, 0.1)), unit: "l" };
    return { value: formatMetricNumber(roundTo(base, base < 50 ? 5 : 10)), unit: "ml" };
  }
  const flOz = base / 29.5735;
  if (flOz >= 32) return { value: formatQuantity(roundTo(flOz / 32, 0.25)), unit: "qt" };
  return { value: formatQuantity(roundTo(flOz * 2, 1) / 2), unit: "fl oz" };
}

/**
 * Builds the full display string for an ingredient's quantity + unit,
 * applying an optional serving-scale factor and, when the unit differs
 * from the viewer's `unitPref`, appending an approximate converted value —
 * e.g. "200 g (~7 oz)". The original authored value is always shown first
 * so it's never lost or hidden.
 */
export function formatIngredientQuantity(
  rawQuantity: string | null | undefined,
  unit: string | null | undefined,
  targetPref: UnitPref,
  scale: { base: number; desired: number } = { base: 1, desired: 1 }
): string {
  if (!rawQuantity) return unit ?? "";
  const parsed = parseFloat(rawQuantity);
  if (isNaN(parsed) || parsed === 0) return unit ?? "";

  const scaleFactor = scale.base === 0 ? 1 : scale.desired / scale.base;
  const scaledValue = parsed * scaleFactor;
  const originalText = `${formatQuantity(scaledValue)}${unit ? ` ${unit}` : ""}`;

  if (!unit) return originalText;

  const converted = convertUnit(scaledValue, unit, targetPref);
  if (!converted) return originalText;

  return `${originalText} (~${converted.value} ${converted.unit})`;
}
