/**
 * Conservative pantry-awareness for shopping list generation.
 *
 * Matching strategy:
 * 1. Reliable path: if both the shopping item and a pantry item reference the same
 *    normalized `ingredientId`, they're the same ingredient.
 * 2. Fallback: normalize free-text `rawName` (case/whitespace/simple plural) and match exactly.
 *    No fuzzy/AI matching — when normalization doesn't produce an exact match, treat as unmatched.
 *
 * Quantity handling: only subtract pantry quantity from the needed quantity when both
 * are parseable numbers in the same (normalized) unit. Otherwise leave the needed quantity
 * untouched and just flag `inPantry` so the user can decide for themselves.
 */

import { extractIngredientQuantity } from "./extract-ingredient-quantity";
import { resolveIngredientKey, type IngredientAliasIndex } from "./ingredient-match";

export type PantrySourceItem = {
  ingredientId: string | null;
  rawName: string;
  quantity: string | null;
  unit: string | null;
};

export type ShoppingSourceItem = {
  rawName: string;
  quantity?: string;
  unit?: string;
  ingredientId?: string | null;
};

export type PantryAdjustedItem = {
  rawName: string;
  quantity?: string;
  unit?: string;
  inPantry: boolean;
};

export function normalizeName(name: string): string {
  const trimmed = name.toLowerCase().trim().replace(/\s+/g, " ");
  // Simple plural trim — strip a single trailing "s" for words longer than 3 chars.
  return trimmed.length > 3 && trimmed.endsWith("s") ? trimmed.slice(0, -1) : trimmed;
}

export function normalizeUnit(unit: string | null | undefined): string {
  return (unit ?? "").toLowerCase().trim();
}

export function formatQuantity(n: number): string {
  return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/\.?0+$/, "");
}

/**
 * Reduces (or flags) each shopping item's needed quantity based on what's already in the pantry.
 * Never removes an item outright when the match is uncertain — always returns one entry per input item.
 */
export function applyPantryToItems(
  items: ShoppingSourceItem[],
  pantry: PantrySourceItem[],
  aliasIndex?: IngredientAliasIndex
): PantryAdjustedItem[] {
  const pantryByIngredientId = new Map<string, PantrySourceItem[]>();
  const pantryByName = new Map<string, PantrySourceItem[]>();

  for (const p of pantry) {
    if (p.ingredientId) {
      const list = pantryByIngredientId.get(p.ingredientId) ?? [];
      list.push(p);
      pantryByIngredientId.set(p.ingredientId, list);
    }
    // Index under both the plain normalized name and its alias-resolved
    // canonical key (e.g. "sel fin" also indexes under salt's canonical
    // id) — a shopping item written as "sel" then still finds it.
    for (const key of new Set([normalizeName(p.rawName), aliasIndex ? resolveIngredientKey(p.rawName, aliasIndex) : null])) {
      if (!key) continue;
      const list = pantryByName.get(key) ?? [];
      list.push(p);
      pantryByName.set(key, list);
    }
  }

  return items.map((item) => {
    let matches: PantrySourceItem[] | undefined;

    if (item.ingredientId && pantryByIngredientId.has(item.ingredientId)) {
      matches = pantryByIngredientId.get(item.ingredientId);
    } else {
      matches =
        pantryByName.get(normalizeName(item.rawName)) ??
        (aliasIndex ? pantryByName.get(resolveIngredientKey(item.rawName, aliasIndex)) : undefined);
    }

    if (!matches || matches.length === 0) {
      return { rawName: item.rawName, quantity: item.quantity, unit: item.unit, inPantry: false };
    }

    const neededQty = item.quantity ? parseFloat(item.quantity) : NaN;
    const neededUnit = normalizeUnit(item.unit);

    if (!isNaN(neededQty) && neededQty > 0) {
      // Only sum pantry entries whose unit matches the needed unit (including both-empty).
      const compatible = matches.filter((m) => normalizeUnit(m.unit) === neededUnit);
      const parseableQuantities = compatible
        .map((m) => (m.quantity ? parseFloat(m.quantity) : NaN))
        .filter((n) => !isNaN(n));

      if (compatible.length > 0 && parseableQuantities.length === compatible.length) {
        const havingQty = parseableQuantities.reduce((sum, n) => sum + n, 0);
        const remaining = neededQty - havingQty;
        if (remaining <= 0) {
          // Fully covered — still surfaced in the list (flagged), not silently dropped.
          return { rawName: item.rawName, quantity: undefined, unit: item.unit, inPantry: true };
        }
        return { rawName: item.rawName, quantity: formatQuantity(remaining), unit: item.unit, inPantry: true };
      }
    }

    // Matched the ingredient, but quantities/units aren't reliably comparable — flag only.
    return { rawName: item.rawName, quantity: item.quantity, unit: item.unit, inPantry: true };
  });
}

export type MergeSourceIngredient = {
  rawName: string;
  quantity: string | null;
  unit: string | null;
  ingredientId: string | null;
};

export type MergedIngredient = {
  rawName: string;
  quantity?: string;
  unit?: string;
  ingredientId: string | null;
};

/**
 * Merges ingredients across recipes (e.g. an entire week's meal plan) so a shopping
 * list doesn't show the same ingredient once per recipe. Groups by normalized
 * ingredientId (if present) or normalized name+unit, then SUMS quantities across the
 * group — the earlier version of this logic kept only the first occurrence and
 * silently dropped every other recipe's requirement, which under-shopped whenever the
 * same ingredient appeared in more than one recipe.
 *
 * Items with the same name but incompatible/unparseable units are kept as separate
 * line items rather than guessed at — never invent a converted total.
 */
export function mergeIngredients(ingredients: MergeSourceIngredient[]): MergedIngredient[] {
  const groups = new Map<string, { rawName: string; ingredientId: string | null; unit: string | null; entries: (string | null)[] }>();

  for (const ing of ingredients) {
    // Clean up ingredients whose stored rawName still has a quantity baked into it
    // (older AI-generated recipes, before that was fixed at the source) BEFORE grouping —
    // otherwise "1 avocat" and a cleanly-named "avocat" from another recipe would
    // normalize to different group keys and never merge.
    const cleaned = extractIngredientQuantity(ing.rawName, ing.quantity ?? undefined, ing.unit ?? undefined);
    const unitKey = normalizeUnit(cleaned.unit);
    const groupKey = ing.ingredientId
      ? `id:${ing.ingredientId}:${unitKey}`
      : `name:${normalizeName(cleaned.rawName)}:${unitKey}`;

    let group = groups.get(groupKey);
    if (!group) {
      group = { rawName: cleaned.rawName, ingredientId: ing.ingredientId, unit: cleaned.unit ?? null, entries: [] };
      groups.set(groupKey, group);
    }
    group.entries.push(cleaned.quantity ?? null);
  }

  return Array.from(groups.values()).map((group) => {
    const parsed = group.entries.map((q) => (q ? parseFloat(q) : NaN));
    const allParseable = parsed.length > 0 && parsed.every((n) => !isNaN(n));

    if (allParseable) {
      const total = parsed.reduce((sum, n) => sum + n, 0);
      return { rawName: group.rawName, quantity: formatQuantity(total), unit: group.unit ?? undefined, ingredientId: group.ingredientId };
    }

    // Unparseable or mixed with/without quantity — can't sum reliably, don't guess.
    // Fall back to no quantity shown rather than an incorrect total.
    return { rawName: group.rawName, quantity: undefined, unit: group.unit ?? undefined, ingredientId: group.ingredientId };
  });
}
