(function attachParser(global: any) {
  type ItemType = "portion" | "piece" | "weight" | "plate" | "unknown";
  type Portion = "full" | "half";

  type MenuItem = {
    id?: string;
    name: string;
    price: number;
    item_type?: ItemType;
    item_name_marathi?: string | null;
    aliases?: string[];
    half_price?: number | null;
  };

  type ParsedItem = {
    item_name: string;
    order_type: ItemType;
    portion?: Portion | "piece";
    quantity: number;
    portion_breakup?: Array<{ portion: Portion; qty: number }>;
    packing_note: string;
    special_instruction: string;
    item_price: number;
    menu_full_rate: number;
    final_rate: number;
    customer_mentioned_rate: number | null;
    line_total: number;
    source_text: string;
    confidence: "high" | "medium" | "low";
    confidence_score: number;
    confidence_reason: string;
    warnings: string[];
  };

  type ParsedOrder = {
    customer_name: string;
    customer_phone: string;
    address: string;
    items: ParsedItem[];
    order_total: number;
    warnings: string[];
    raw_text: string;
  };

  type AliasGroup = {
    canonical: string;
    aliases: string[];
  };

  type AliasRule = {
    alias: string;
    canonical: string;
    price: number;
    item: MenuItem;
  };

  const defaultMenuItems: MenuItem[] = [
    { name: "Thalpeet", price: 65, item_type: "portion", aliases: ["thalpeet", "thalipeeth", "thalipith", "thalipit", "थालीपीठ", "थालिपीठ"] },
    { name: "पिठलं", price: 65, item_type: "portion", aliases: ["pithala", "pithla", "pitla", "pithale"] },
    { name: "फोडणीचे वरण", price: 40, item_type: "portion", aliases: ["varan", "fodniche varan", "fodnicha varan", "fodani varan"] },
    { name: "साधा भात", price: 30, item_type: "portion", aliases: ["sadha bhat", "sada bhat", "bhat", "rice", "भात"] },
    { name: "ज्वारी भाकरी", price: 22, item_type: "piece", aliases: ["bhakri", "jowar bhakri", "jawar bhakri", "भाकरी"] },
    { name: "घडीची पोळी", price: 10, item_type: "piece", aliases: ["poli", "polya", "chapati", "roti", "folding bread", "पोळी", "चपाती"] },
    { name: "पीयूष", price: 70, item_type: "piece", aliases: ["piyush", "piyus"] }
  ];

  const builtInAliasGroups: AliasGroup[] = [
    { canonical: "samosa", aliases: ["samosa", "samose", "samosas", "समोसा", "समोसे"] },
    { canonical: "poli", aliases: ["poli", "polya", "chapati", "roti", "पोळी", "पोळ्या", "चपाती"] },
    { canonical: "gulab jamun", aliases: ["gulab jamun", "jamun", "गुलाब जामुन"] },
    { canonical: "modak", aliases: ["modak", "मोदक"] },
    { canonical: "vada", aliases: ["vada", "wada", "वडा"] },
    { canonical: "cutlet", aliases: ["cutlet", "कटलेट"] },
    { canonical: "bhat", aliases: ["bhat", "sadha bhat", "sada bhat", "rice", "भात", "साधा भात"] }
  ];

  const quantityWords: Record<string, number> = {
    "0": 0,
    "०": 0,
    zero: 0,
    "1": 1,
    "१": 1,
    one: 1,
    ek: 1,
    "एक": 1,
    "2": 2,
    "२": 2,
    two: 2,
    don: 2,
    do: 2,
    "दोन": 2,
    "3": 3,
    "३": 3,
    three: 3,
    teen: 3,
    "तीन": 3,
    "4": 4,
    "४": 4,
    four: 4,
    char: 4,
    "चार": 4,
    "5": 5,
    "५": 5,
    five: 5,
    pach: 5,
    paach: 5,
    "पाच": 5,
    "6": 6,
    "६": 6,
    six: 6,
    saha: 6,
    "7": 7,
    "७": 7,
    seven: 7,
    saat: 7,
    "8": 8,
    "८": 8,
    eight: 8,
    aath: 8,
    "9": 9,
    "९": 9,
    nine: 9,
    nau: 9,
    "10": 10,
    "१०": 10,
    ten: 10,
    daha: 10,
    half: 0.5,
    adha: 0.5,
    ardha: 0.5,
    "अर्धा": 0.5,
    "अर्धी": 0.5,
    dedh: 1.5,
    "दीड": 1.5
  };

  const pieceItemPattern = /(samosa|samose|samosas|poli|polya|chapati|chapatis|roti|rotis|gulab\s*jamun|jamun|modak|vada|wada|cutlet|भाकरी|पोळी|पोळ्या|चपाती|समोसा|समोसे|मोदक|वडा|कटलेट)/i;
  const portionItemPattern = /(bhat|rice|varan|pithala|pithla|pitla|bhaji|amti|dal|भात|वरण|पिठलं|भाजी|आमटी)/i;
  const rateMarkerPattern = /(₹|rs\.?|inr|rate|price|\/-|रु\.?|रुपये|per\s+plate)/i;
  const packingInstructionPattern = /(separate|seperate|seprate|separately|vegla|vegale|alag|pack\s+separately|वेगळं|वेगळे|वेगवेगळे|अलग)/i;
  const halfPattern = /\b(half|haf|hafe|adha|ardha)\b|हाफ|अर्धा|अर्धी|अर्धे/i;
  const fullPattern = /\b(full|ful)\b|फुल|पूर्ण/i;

  const boilerplateWords = [
    "कृपया",
    "आणि",
    "पाठवा",
    "प्लेट",
    "plate",
    "please",
    "pls",
    "and",
    "send",
    "bheja",
    "bhejna",
    "dya",
    "द्या"
  ];

  function loadAliasGroups(): AliasGroup[] {
    const fromGlobal = global.RamaItemAliases?.itemAliases;
    if (fromGlobal) return [...fromGlobal, ...builtInAliasGroups];

    if (typeof require !== "undefined") {
      try {
        return [...require("./data/itemAliases.ts").itemAliases, ...builtInAliasGroups];
      } catch {
        return builtInAliasGroups;
      }
    }

    return builtInAliasGroups;
  }

  const itemAliases: AliasGroup[] = loadAliasGroups();

  function normalizeSpaces(value: string): string {
    return String(value || "").replace(/\s+/g, " ").trim();
  }

  function toAsciiDigits(value: string): string {
    const devanagari = "०१२३४५६७८९";
    const mojibake = "à¥¦à¥§à¥¨à¥©à¥ªà¥«à¥¬à¥­à¥®à¥¯";
    return String(value || "")
      .replace(/[०-९]/g, (digit) => String(devanagari.indexOf(digit)))
      .replace(/[à¥¦-à¥¯]/g, (digit) => String(mojibake.indexOf(digit)));
  }

  function normalizeForParsing(value: string): string {
    return normalizeSpaces(toAsciiDigits(value)
      .replace(/[−–—]/g, "-")
      .replace(/([0-9])\s*(full|ful|half|haf|hafe)\b/gi, "$1 $2")
      .replace(/\b(full|ful|half|haf|hafe)\s*([0-9])/gi, "$1 $2")
      .replace(/\s*\+\s*/g, " + "));
  }

  function escapeRegex(value: string): string {
    return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }

  function hasCanonical(menu: MenuItem[], name: string): boolean {
    return menu.some((item) => normalizeSpaces(item.name).toLowerCase() === normalizeSpaces(name).toLowerCase());
  }

  function findMenuItem(menu: MenuItem[], name: string): MenuItem | undefined {
    return menu.find((item) => normalizeSpaces(item.name).toLowerCase() === normalizeSpaces(name).toLowerCase());
  }

  function canonicalizeItemName(input: string, menu: MenuItem[] = defaultMenuItems): string | null {
    const clean = normalizeSpaces(input).toLowerCase();
    if (!clean) return null;

    for (const item of menu) {
      const candidates = [item.name, item.item_name_marathi, ...(item.aliases || [])].filter(Boolean);
      if (candidates.some((candidate) => normalizeSpaces(String(candidate)).toLowerCase() === clean)) return item.name;
    }

    for (const group of itemAliases) {
      const aliases = [group.canonical, ...group.aliases];
      if (aliases.some((alias) => normalizeSpaces(alias).toLowerCase() === clean)) {
        const direct = findMenuItem(menu, group.canonical);
        if (direct) return direct.name;
        const matchedMenu = menu.find((item) => {
          const candidates = [item.name, item.item_name_marathi, ...(item.aliases || [])].filter(Boolean);
          return candidates.some((candidate) => aliases.map((alias) => normalizeSpaces(alias).toLowerCase()).includes(normalizeSpaces(String(candidate)).toLowerCase()));
        });
        return matchedMenu?.name || group.canonical;
      }
    }

    return null;
  }

  function classifyItemType(name: string, menuItem?: MenuItem): ItemType {
    if (menuItem?.item_type) return menuItem.item_type;
    const haystack = `${name || ""} ${menuItem?.item_name_marathi || ""} ${(menuItem?.aliases || []).join(" ")}`;
    if (pieceItemPattern.test(haystack)) return "piece";
    if (portionItemPattern.test(haystack)) return "portion";
    return "portion";
  }

  function makeAliasRules(menu: MenuItem[] = defaultMenuItems): AliasRule[] {
    const ruleByAlias = new Map<string, AliasRule>();

    function add(alias: string, item: MenuItem) {
      const key = normalizeSpaces(alias).toLowerCase();
      if (!key) return;
      const current = ruleByAlias.get(key);
      if (!current || key.length > current.alias.length) {
        ruleByAlias.set(key, {
          alias: normalizeSpaces(alias),
          canonical: item.name,
          price: Number(item.price) || 0,
          item
        });
      }
    }

    for (const item of menu) {
      add(item.name, item);
      if (item.item_name_marathi) add(item.item_name_marathi, item);
      for (const alias of item.aliases || []) add(alias, item);
    }

    for (const group of itemAliases) {
      const canonical = canonicalizeItemName(group.canonical, menu);
      const item = canonical ? findMenuItem(menu, canonical) : null;
      if (!item) continue;
      add(group.canonical, item);
      for (const alias of group.aliases) add(alias, item);
    }

    return Array.from(ruleByAlias.values()).sort((a, b) => b.alias.length - a.alias.length);
  }

  function hasRateMarker(text: string): boolean {
    return rateMarkerPattern.test(String(text || ""));
  }

  function extractCustomerMentionedRate(text: string): number | null {
    const clean = normalizeForParsing(text);
    if (!hasRateMarker(clean)) return null;
    const markerBefore = clean.match(/(?:₹|rs\.?|inr|rate|price|रु\.?|रुपये)\s*([0-9]+(?:\.[0-9]+)?)/i);
    if (markerBefore) return Number(markerBefore[1]);
    const markerAfter = clean.match(/([0-9]+(?:\.[0-9]+)?)\s*(?:\/-|रु\.?|रुपये|rs\.?|inr|per\s+plate)/i);
    return markerAfter ? Number(markerAfter[1]) : null;
  }

  function wordToNumber(word: string): number | null {
    const clean = normalizeForParsing(word).toLowerCase();
    if (clean in quantityWords) return quantityWords[clean];
    const number = Number(clean);
    return Number.isFinite(number) ? number : null;
  }

  function parseNumberToken(token: string): number | null {
    const clean = normalizeForParsing(token).toLowerCase();
    if (clean in quantityWords) return quantityWords[clean];

    const fraction = clean.match(/^([0-9]+)\s*\/\s*([0-9]+)$/);
    if (fraction && Number(fraction[2])) return Number(fraction[1]) / Number(fraction[2]);

    const numeric = clean.match(/^[0-9]+(?:\.[0-9]+)?$/);
    return numeric ? Number(clean) : null;
  }

  function decimalHalfBreakup(value: number): Array<{ portion: Portion; qty: number }> {
    if (!Number.isFinite(value) || Math.abs((value * 10) % 10) !== 5) return [];
    const fullQty = Math.floor(value);
    const breakup: Array<{ portion: Portion; qty: number }> = [];
    if (fullQty > 0) breakup.push({ portion: "full", qty: fullQty });
    breakup.push({ portion: "half", qty: 1 });
    return breakup;
  }

  function firstDecimalHalfValue(text: string): number | null {
    const match = normalizeForParsing(text).match(/(?:^|[\s(-])((?:[0-9]+)?\.5)(?=$|[\s),])/);
    if (!match) return null;
    const value = Number(match[1].startsWith(".") ? `0${match[1]}` : match[1]);
    return Number.isFinite(value) ? value : null;
  }

  function stripPackingWords(text: string): string {
    return normalizeSpaces(String(text || "").replace(packingInstructionPattern, " ").replace(/\bdya\b|\bdena\b|देना|द्या/gi, " "));
  }

  function compactPortionText(text: string): string {
    return normalizeForParsing(text)
      .replace(/\b1\s*\/\s*2\b/g, " half ")
      .replace(/\bful\b/gi, " full ")
      .replace(/\bhaf(e)?\b/gi, " half ")
      .replace(/\badha\b|\bardha\b/gi, " half ")
      .replace(/अर्धा|अर्धी|अर्धे|हाफ/g, " half ")
      .replace(/फुल|पूर्ण/g, " full ")
      .replace(/\bदीड\b/gi, " 1.5 ")
      .replace(/\bdedh\b/gi, " 1.5 ");
  }

  function normalizeQuantityText(text: string, options: { itemType?: ItemType } = {}) {
    const itemType = options.itemType || "unknown";
    const source = normalizeForParsing(text);
    const withoutPacking = stripPackingWords(source);
    const clean = compactPortionText(withoutPacking);
    const customerMentionedRate = extractCustomerMentionedRate(source);
    const rateSafeText = hasRateMarker(clean)
      ? clean
          .replace(/(?:₹|rs\.?|inr|rate|price|रु\.?|रुपये)\s*[0-9]+(?:\.[0-9]+)?/gi, " ")
          .replace(/[0-9]+(?:\.[0-9]+)?\s*(?:\/-|रु\.?|रुपये|rs\.?|inr|per\s+plate)/gi, " ")
      : clean;

    if (itemType === "piece") {
      const numberMatch = rateSafeText.match(/(?:^|[\s(-])([0-9]+(?:\.[0-9]+)?)(?=$|[\s),.])/);
      const wordMatch = rateSafeText.match(/\b(one|two|three|four|five|six|seven|eight|nine|ten|ek|don|do|teen|char|pach|paach|saha|saat|aath|nau|daha|एक|दोन|तीन|चार|पाच)\b/i);
      const marathiWordMatch = rateSafeText.match(/(एक|दोन|तीन|चार|पाच)/);
      const quantity = numberMatch ? Number(numberMatch[1]) : wordMatch ? wordToNumber(wordMatch[1]) : marathiWordMatch ? wordToNumber(marathiWordMatch[1]) : null;
      return {
        quantity: quantity || 1,
        unit_type: "piece",
        is_rate: Boolean(customerMentionedRate && !quantity),
        customer_mentioned_rate: customerMentionedRate,
        normalized_text: quantity ? `${quantity} piece` : "1 piece"
      };
    }

    const portionBreakup: Array<{ portion: Portion; qty: number }> = [];
    const numericPlusHalf = rateSafeText.match(/\b([0-9]+(?:\.[0-9]+)?)\s*\+\s*half\b/i);
    if (numericPlusHalf) {
      portionBreakup.push({ portion: "full", qty: Number(numericPlusHalf[1]) || 1 }, { portion: "half", qty: 1 });
    }
    const tokenPattern = /([0-9]+(?:\.[0-9]+)?|one|two|three|ek|don|do|teen|एक|दोन|तीन)?\s*(full|half)\b|(full|half)\s*([0-9]+(?:\.[0-9]+)?|one|two|three|ek|don|do|teen|एक|दोन|तीन)?/gi;
    let match: RegExpExecArray | null;
    if (!numericPlusHalf) {
      while ((match = tokenPattern.exec(rateSafeText))) {
        const qtyToken = match[1] || match[4] || "1";
        const portion = (match[2] || match[3]).toLowerCase() === "half" ? "half" : "full";
        portionBreakup.push({ portion, qty: parseNumberToken(qtyToken) || 1 });
      }
    }

    if (!portionBreakup.length && halfPattern.test(rateSafeText)) portionBreakup.push({ portion: "half", qty: 1 });
    if (!portionBreakup.length && fullPattern.test(rateSafeText)) portionBreakup.push({ portion: "full", qty: 1 });

    if (!portionBreakup.length) {
      const decimalValue = firstDecimalHalfValue(rateSafeText);
      if (decimalValue !== null) portionBreakup.push(...decimalHalfBreakup(decimalValue));
    }

    if (!portionBreakup.length) {
      const fraction = rateSafeText.match(/(?:^|[\s(-])([0-9]+)\s*\/\s*([0-9]+)(?=$|[\s),.])/);
      if (fraction && Number(fraction[1]) === 1 && Number(fraction[2]) === 2) {
        portionBreakup.push({ portion: "half", qty: 1 });
      }
    }

    if (!portionBreakup.length) {
      const numeric = rateSafeText.match(/(?:^|[\s(-])([0-9]+(?:\.[0-9]+)?)(?=$|[\s),.])/);
      const word = rateSafeText.match(/\b(one|two|three|ek|don|do|teen|एक|दोन|तीन)\b/i);
      const value = numeric ? Number(numeric[1]) : word ? wordToNumber(word[1]) : null;
      const decimalBreakup = value !== null ? decimalHalfBreakup(value) : [];
      if (decimalBreakup.length) {
        portionBreakup.push(...decimalBreakup);
      } else if (value) {
        portionBreakup.push({ portion: "full", qty: value });
      }
    }

    const quantity = portionBreakup.reduce((total, part) => total + (part.portion === "half" ? 0.5 : 1) * part.qty, 0) || 1;
    return {
      quantity,
      unit_type: "portion",
      portion_breakup: portionBreakup,
      is_rate: Boolean(customerMentionedRate && !portionBreakup.length),
      customer_mentioned_rate: customerMentionedRate,
      normalized_text: portionBreakup.map((part) => `${part.qty} ${part.portion}`).join(" + ") || "1 full"
    };
  }

  function parseQuantity(value: string): number | null {
    const clean = normalizeForParsing(value.replace(/,/g, "")).toLowerCase();
    if (clean in quantityWords) return quantityWords[clean];

    const mixed = clean.match(/^([0-9]+(?:\.[0-9]+)?)\s*\+\s*([0-9]+)\s*\/\s*([0-9]+)$/);
    if (mixed) return Number(mixed[1]) + Number(mixed[2]) / Number(mixed[3]);

    const fraction = clean.match(/^([0-9]+)\s*\/\s*([0-9]+)$/);
    if (fraction) return Number(fraction[1]) / Number(fraction[2]);

    const numeric = clean.match(/^[0-9]+(?:\.[0-9]+)?$/);
    if (numeric) return Number(clean);

    return null;
  }

  function firstQuantity(text: string): number | null {
    const quantityPattern = /([0-9]+(?:\.[0-9]+)?\s*\+\s*[0-9]+\s*\/\s*[0-9]+|[0-9]+\s*\/\s*[0-9]+|[0-9]+(?:\.[0-9]+)?|अर्धा|अर्धी|एक|दोन|तीन|half|ek|one|don|do|two|teen|three|dedh|दीड)/i;
    const match = normalizeForParsing(text).match(quantityPattern);
    return match ? parseQuantity(match[1]) : null;
  }

  function confidenceForParsedItem(itemType: ItemType, normalized: any, source: string) {
    if (itemType === "piece" && normalized.quantity > 1 && !normalized.is_rate) {
      return { score: 0.95, confidence: "high" as const, reason: "High confidence: known item + numeric quantity" };
    }
    if (normalized.portion_breakup?.length > 1) {
      return { score: 0.9, confidence: "high" as const, reason: "High confidence: mixed full and half portions detected" };
    }
    if (normalized.portion_breakup?.length === 1) {
      return { score: 0.82, confidence: "high" as const, reason: "High confidence: portion quantity detected" };
    }
    if (hasRateMarker(source)) {
      return { score: 0.72, confidence: "medium" as const, reason: "Medium confidence: customer mentioned a possible rate; menu rate protected" };
    }
    return { score: 0.68, confidence: "low" as const, reason: "Low confidence: number could be rate or quantity" };
  }

  function detectInstruction(text: string): string {
    const bits: string[] = [];
    const bracketMatches = Array.from(text.matchAll(/[(（]([^()（）]+)[)）]/g)).map((match) => match[1]);
    const searchable = `${text} ${bracketMatches.join(" ")}`.toLowerCase();

    if (packingInstructionPattern.test(searchable)) bits.push("separate");
    if (/कमी\s*तिखट|less\s*spicy|mild/.test(searchable)) bits.push("कमी तिखट");
    if (/जास्त\s*तिखट|extra\s*spicy|spicy/.test(searchable)) bits.push("जास्त तिखट");
    if (/कमी\s*तेल|less\s*oil/.test(searchable)) bits.push("कमी तेल");
    if (/जैन|jain/.test(searchable)) bits.push("जैन");

    for (const raw of bracketMatches) {
      const clean = normalizeSpaces(raw);
      if (!packingInstructionPattern.test(clean) && !/कमी\s*तिखट|less\s*spicy|mild|जास्त\s*तिखट|extra\s*spicy|spicy|कमी\s*तेल|less\s*oil|जैन|jain/i.test(clean)) {
        bits.push(clean);
      }
    }

    return Array.from(new Set(bits)).join(", ");
  }

  function extractPackingNote(text: string): string {
    return packingInstructionPattern.test(String(text || "")) ? "Pack full and half separately" : "";
  }

  function extractPhone(text: string): string {
    const phoneMatch = String(text || "").match(/(?:\+?91[-\s]?)?([6-9]\d{9})/);
    return phoneMatch ? phoneMatch[1] : "";
  }

  function stripPhone(text: string): string {
    return normalizeSpaces(String(text || "").replace(/(?:\+?91[-\s]?)?[6-9]\d{9}/g, " "));
  }

  function hasAddressCue(text: string): boolean {
    return /(\d+\s*[,/-]|[A-Za-z]\d|\d+[A-Za-z]|,|society|apt|apartment|flat|wing|bavadhan|pune|road|nagar|colony|shagun|dsk|building|floor|सोसायटी|पुणे|रोड|नगर)/i.test(text);
  }

  function splitAddressAndName(line: string) {
    const match = normalizeSpaces(line).match(/\b(Mr|Mrs|Miss|Ms|Dr|Shri|Shree|Smt)\.?\s+(.+)$/i);
    if (!match) return { addressPart: line, namePart: "" };
    return {
      addressPart: normalizeSpaces(line.slice(0, match.index)),
      namePart: `${match[1].replace(/\.$/, "")}. ${match[2]}`
    };
  }

  function titleCaseWords(value: string): string {
    return String(value || "").replace(/\b([A-Za-z])([A-Za-z']*)\b/g, (_match, first, rest) => first.toUpperCase() + rest.toLowerCase());
  }

  function formatCustomerName(value: string): string {
    const clean = normalizeSpaces(value);
    if (!clean || /[\u0900-\u097f]/.test(clean)) return clean;
    const honorifics: Record<string, string> = { mr: "Mr.", mrs: "Mrs.", miss: "Miss", ms: "Ms.", dr: "Dr.", shri: "Shri", shree: "Shree", smt: "Smt." };
    const match = clean.match(/^(mr|mrs|miss|ms|dr|shri|shree|smt)\.?\s+(.+)$/i);
    return match ? `${honorifics[match[1].toLowerCase()]} ${titleCaseWords(match[2])}` : titleCaseWords(clean);
  }

  function formatAddressLine(value: string): string {
    const clean = normalizeSpaces(value);
    if (!clean || /[\u0900-\u097f]/.test(clean)) return clean;
    if (clean.includes(",")) return clean.split(",").map((part) => titleCaseWords(part.trim())).filter(Boolean).join(", ");
    let formatted = titleCaseWords(clean);
    formatted = formatted.replace(/^([A-Za-z]?\d+[A-Za-z0-9/-]*)\s+/, "$1, ");
    formatted = formatted.replace(/\b(Viva\s+Hallmark)\s+(Patil\s+Nagar)\b/i, "$1, $2");
    formatted = formatted.replace(/\s+(Bavdhan|Bavadhan|Bavdhan Budruk|Pune)$/i, ", $1");
    formatted = formatted.replace(/,\s*,/g, ",");
    return formatted;
  }

  function stripParsedNoise(text: string): string {
    let clean = stripPhone(text);
    for (const word of boilerplateWords) {
      clean = clean.replace(new RegExp(escapeRegex(word), "gi"), " ");
    }
    clean = clean.replace(/[(（][^()（）]+[)）]/g, " ");
    clean = clean.replace(packingInstructionPattern, " ");
    clean = clean.replace(/[-:+]/g, " ");
    clean = clean.replace(/\b\d+(?:\.\d+)?\b/g, " ");
    clean = clean.replace(/\d+\s*\/\s*\d+/g, " ");
    clean = clean.replace(/अर्धा|अर्धी|एक|दोन|तीन|half|ek|one|don|do|two|teen|three|full|ful/gi, " ");
    return normalizeSpaces(clean);
  }

  function isBoilerplateLine(text: string): boolean {
    const clean = stripParsedNoise(text).toLowerCase();
    return !clean || boilerplateWords.some((word) => clean === word.toLowerCase());
  }

  function looksLikeUnknownFoodLine(text: string): boolean {
    if (!firstQuantity(text)) return false;
    if (hasAddressCue(text)) return false;
    const clean = stripParsedNoise(text);
    return clean.length > 0 && /[\p{L}]/u.test(clean);
  }

  function findAliasHits(line: string, rules: AliasRule[]) {
    const hits: Array<{ start: number; end: number; rule: AliasRule }> = [];
    const lowerLine = line.toLowerCase();

    for (const rule of rules) {
      const alias = rule.alias.toLowerCase();
      let index = lowerLine.indexOf(alias);
      while (index !== -1) {
        hits.push({ start: index, end: index + alias.length, rule });
        index = lowerLine.indexOf(alias, index + alias.length);
      }
    }

    return hits
      .sort((a, b) => (a.start === b.start ? b.end - b.start - (a.end - a.start) : a.start - b.start))
      .filter((hit, index, all) => {
        return !all.some((other, otherIndex) => {
          if (otherIndex === index) return false;
          const overlaps = other.start <= hit.start && other.end >= hit.end;
          const longer = other.end - other.start > hit.end - hit.start;
          return overlaps && longer;
        });
      })
      .sort((a, b) => a.start - b.start);
  }

  function lineTotalForPortions(portionBreakup: Array<{ portion: Portion; qty: number }>, item: MenuItem): number {
    return Number(portionBreakup.reduce((total, part) => {
      const rate = part.portion === "half" ? Number(item.half_price ?? item.price / 2) : Number(item.price);
      return total + (Number(part.qty) || 0) * rate;
    }, 0).toFixed(2));
  }

  function normalizeItemKey(value: string): string {
    return normalizeSpaces(value).toLowerCase();
  }

  function evidenceScore(item: ParsedItem): number {
    let score = 0;
    if (Number(item.quantity) !== 1) score += 5;
    if (item.source_text && /(^|[^\w])\d+(?:\.\d+)?\s+[^\d]/i.test(item.source_text)) score += 2;
    if (item.portion_breakup?.length) score += 1;
    score += item.confidence_score || 0;
    return score;
  }

  function dedupeParsedItems(items: ParsedItem[]): ParsedItem[] {
    const byKey = new Map<string, ParsedItem>();
    for (const item of items) {
      const sourceBucket = normalizeSpaces(item.source_text).toLowerCase() || "single-message";
      const key = `${normalizeItemKey(item.item_name)}|${item.portion || "full"}|${sourceBucket}`;
      const current = byKey.get(key);
      if (!current || evidenceScore(item) > evidenceScore(current)) {
        byKey.set(key, item);
      }
    }
    return Array.from(byKey.values()).map((item) => {
      const line_total = item.portion_breakup?.length
        ? Number(item.portion_breakup.reduce((total, part) => {
            const rate = part.portion === "half" ? Number(item.menu_full_rate || 0) / 2 : Number(item.menu_full_rate || item.final_rate || 0);
            return total + Number(part.qty || 0) * rate;
          }, 0).toFixed(2))
        : Number((Number(item.quantity || 0) * Number(item.final_rate || item.item_price || 0)).toFixed(2));
      return { ...item, line_total };
    });
  }

  function removeConsumedSpans(line: string, spans: Array<{ start: number; end: number }>): string {
    let result = line;
    for (const span of [...spans].sort((a, b) => b.start - a.start)) {
      result = `${result.slice(0, span.start)} ${result.slice(span.end)}`;
    }
    return normalizeSpaces(result.replace(/\s+/g, " "));
  }

  const immediateQuantityToken = String.raw`(?:1\s*\/\s*2|[0-9\u0966-\u096f]+(?:\.[0-9\u0966-\u096f]+)?|\.[0-9\u0966-\u096f]+|one|two|three|four|five|six|seven|eight|nine|ten|ek|don|do|teen|char|pach|paach|dedh|half|haf|hafe|adha|ardha|full|ful|एक|दोन|तीन|चार|पाच|दीड|अर्धा|अर्धी|अर्धे|हाफ|फुल)`;
  const immediateRateToken = String.raw`(?:(?:₹|rs\.?|inr|rate|price|रु\.?|रुपये)\s*[0-9\u0966-\u096f]+(?:\.[0-9\u0966-\u096f]+)?|[0-9\u0966-\u096f]+(?:\.[0-9\u0966-\u096f]+)?\s*(?:\/-|रु\.?|रुपये|rs\.?|inr|per\s+plate))`;

  function immediateQuantityCue(text: string): string {
    const clean = normalizeForParsing(text);
    const mixedPortion = clean.match(/^\s*(?:[-:]\s*)?[0-9\u0966-\u096f]+(?:\.[0-9\u0966-\u096f]+)?\s*(?:full|ful)?\s*(?:\+|and)\s*(?:1\s*\/\s*2|half|haf|hafe|adha|ardha|अर्धा|अर्धी|अर्धे|हाफ)/i);
    if (mixedPortion) return mixedPortion[0];
    const match = clean.match(new RegExp(
      String.raw`^\s*(?:[-:]\s*)?(?:${immediateRateToken}\s*)?(?:${immediateQuantityToken})(?:\s*(?:\+|and)\s*(?:${immediateQuantityToken}))?(?:\s*(?:full|ful|half|haf|hafe|adha|ardha|फुल|हाफ|अर्धा|अर्धी|अर्धे))?`,
      "i"
    ));
    return match ? match[0] : "";
  }

  function immediatePortionCue(text: string): string {
    const clean = normalizeForParsing(text);
    const match = clean.match(/^\s*(?:[-:]\s*)?(?:half|haf|hafe|adha|ardha|full|ful|1\s*\/\s*2|अर्धा|अर्धी|अर्धे|हाफ|फुल)\b/i);
    return match ? match[0] : "";
  }

  function immediateConsumedSuffixLength(text: string, leadingQuantity: boolean, hasNextItem: boolean): number {
    const source = String(text || "");
    const token = String.raw`(?:1\s*\/\s*2|[0-9\u0966-\u096f]+(?:\.[0-9\u0966-\u096f]+)?|\.[0-9\u0966-\u096f]+|one|two|three|four|five|six|seven|eight|nine|ten|ek|don|do|teen|char|pach|paach|dedh|half|haf|hafe|adha|ardha|full|ful|एक|दोन|तीन|चार|पाच|दीड|अर्धा|अर्धी|अर्धे|हाफ|फुल)`;
    const portion = String.raw`(?:half|haf|hafe|adha|ardha|full|ful|1\s*\/\s*2|अर्धा|अर्धी|अर्धे|हाफ|फुल)`;
    const rate = String.raw`(?:(?:₹|rs\.?|inr|rate|price|रु\.?|रुपये)\s*[0-9\u0966-\u096f]+(?:\.[0-9\u0966-\u096f]+)?|[0-9\u0966-\u096f]+(?:\.[0-9\u0966-\u096f]+)?\s*(?:\/-|रु\.?|रुपये|rs\.?|inr|per\s+plate))`;
    const pattern = leadingQuantity && hasNextItem
      ? new RegExp(String.raw`^\s*(?:[-:]\s*)?${portion}\b`, "i")
      : new RegExp(String.raw`^\s*(?:[-:]\s*)?(?:${rate}\s*)?(?:${token})(?:\s*(?:\+|and)\s*(?:${token}))?(?:\s*${portion})?`, "i");
    const match = source.match(pattern);
    return match ? match[0].length : 0;
  }

  function parseOrder(rawText: string, menu: MenuItem[] = defaultMenuItems): ParsedOrder {
    const warnings: string[] = [];
    const items: ParsedItem[] = [];
    const unmatchedLines: string[] = [];
    const rules = makeAliasRules(menu);
    const phone = extractPhone(rawText);
    const lines = rawText
      .split(/\r?\n/)
      .map((line) => normalizeSpaces(stripPhone(line)))
      .filter(Boolean);

    for (const line of lines) {
      const hits = findAliasHits(line, rules);
      if (!hits.length) {
        unmatchedLines.push(line);
        if (looksLikeUnknownFoodLine(line)) warnings.push(`Unknown item: ${line}`);
        continue;
      }

      const consumedSpans: Array<{ start: number; end: number }> = [];
      for (let index = 0; index < hits.length; index += 1) {
        const hit = hits[index];
        if (consumedSpans.some((span) => hit.start >= span.start && hit.end <= span.end)) continue;
        const next = hits[index + 1];
        const segment = line.slice(hit.start, next ? next.start : line.length);
        const quantityText = segment.slice(hit.rule.alias.length);
        const leadingQuantityText = line.slice(Math.max(0, hit.start - 18), hit.start);
        const leadingQuantityMatch = leadingQuantityText.match(new RegExp(String.raw`(?:^|\s)(${immediateQuantityToken})\s*$`, "i"));
        const consumedStart = leadingQuantityMatch ? hit.start - leadingQuantityMatch[0].length : hit.start;
        const consumedSuffixLength = immediateConsumedSuffixLength(quantityText, Boolean(leadingQuantityMatch), Boolean(next));
        const consumedEnd = hit.end + consumedSuffixLength;
        consumedSpans.push({ start: Math.max(0, consumedStart), end: consumedEnd });
        const itemType = classifyItemType(hit.rule.canonical, hit.rule.item);
        const trailingCue = leadingQuantityMatch
          ? immediatePortionCue(quantityText)
          : immediateQuantityCue(quantityText);
        const quantityCueText = normalizeSpaces(`${leadingQuantityMatch?.[1] || ""} ${trailingCue}`);
        const hasQuantityCue = /[0-9०-९]|half|ful|full|adha|ardha|अर्धा|अर्धी|फुल|दीड|dedh|ek|one|don|do|two|teen|three|एक|दोन|तीन|चार|पाच/i.test(quantityCueText);
        const canDefaultQuantity = hasAddressCue(quantityText) || Boolean(splitAddressAndName(quantityText).namePart);
        if (!hasQuantityCue && !hasRateMarker(quantityCueText) && !canDefaultQuantity) {
          warnings.push(`Quantity unclear for ${hit.rule.canonical}`);
          continue;
        }
        const normalized = hasQuantityCue || hasRateMarker(quantityCueText)
          ? normalizeQuantityText(quantityCueText, { itemType })
          : {
              quantity: 1,
              unit_type: itemType,
              portion_breakup: itemType === "piece" ? undefined : [{ portion: "full" as Portion, qty: 1 }],
              is_rate: false,
              customer_mentioned_rate: null,
              normalized_text: itemType === "piece" ? "1 piece" : "1 full"
            };
        const rawQuantity = Number(normalized.quantity ?? firstQuantity(quantityCueText) ?? 1);
        const customerMentionedRate = normalized.customer_mentioned_rate ?? null;
        const confidence = confidenceForParsedItem(itemType, normalized, quantityCueText || segment);
        const special_instruction = detectInstruction(segment);
        const packing_note = extractPackingNote(segment);
        const portionBreakup = itemType === "piece" ? undefined : normalized.portion_breakup;
        const quantity = portionBreakup?.length === 1 && Number(portionBreakup[0].qty) > 1
          ? Number(portionBreakup[0].qty)
          : rawQuantity;
        const item_price = itemType === "piece"
          ? hit.rule.price
          : portionBreakup?.length === 1 && portionBreakup[0].portion === "half"
            ? Number(hit.rule.item.half_price ?? hit.rule.price / 2)
            : hit.rule.price;
        const line_total = itemType === "piece"
          ? Number((quantity * hit.rule.price).toFixed(2))
          : portionBreakup?.length
            ? lineTotalForPortions(portionBreakup, hit.rule.item)
            : Number((quantity * hit.rule.price).toFixed(2));
        const itemWarnings = customerMentionedRate !== null
          ? [`Customer mentioned rate ${customerMentionedRate}; menu rate was used for billing.`]
          : [];

        items.push({
          item_name: hit.rule.canonical,
          order_type: itemType,
          portion: itemType === "piece" ? "piece" : portionBreakup?.[0]?.portion || "full",
          quantity,
          portion_breakup: portionBreakup,
          packing_note,
          special_instruction,
          item_price,
          menu_full_rate: hit.rule.price,
          final_rate: item_price,
          customer_mentioned_rate: customerMentionedRate,
          line_total,
          source_text: line.slice(Math.max(0, consumedStart), consumedEnd || hit.end),
          confidence: confidence.confidence,
          confidence_score: confidence.score,
          confidence_reason: confidence.reason,
          warnings: itemWarnings
        });
      }

      const leftover = removeConsumedSpans(line, consumedSpans);
      if (leftover && !isBoilerplateLine(leftover)) unmatchedLines.push(leftover);
    }

    const addressLines: string[] = [];
    const nameLines: string[] = [];
    for (const line of unmatchedLines) {
      const split = splitAddressAndName(line);
      if (split.addressPart && hasAddressCue(split.addressPart)) addressLines.push(split.addressPart);
      if (split.namePart) nameLines.push(split.namePart);
      if (!split.namePart && !hasAddressCue(line) && !looksLikeUnknownFoodLine(line) && !isBoilerplateLine(line)) nameLines.push(line);
    }
    const customer_name = nameLines.length ? formatCustomerName(nameLines[nameLines.length - 1]) : "";
    const address = addressLines.map(formatAddressLine).join(", ");
    if (!address) warnings.push("Address missing");

    const dedupedItems = dedupeParsedItems(items);
    const order_total = Number(dedupedItems.reduce((total, item) => total + item.line_total, 0).toFixed(2));

    return {
      customer_name,
      customer_phone: phone,
      address,
      items: dedupedItems,
      order_total,
      warnings: Array.from(new Set(warnings)),
      raw_text: rawText
    };
  }

  function splitIncomingMessages(messageText: string): string[] {
    return String(messageText || "")
      .split(/\n\s*---\s*\n/)
      .map((part) => part.trim())
      .filter(Boolean);
  }

  function processIncomingWhatsAppMessage(
    messageText: string,
    customerPhone: string = "",
    customerName: string = "",
    menu: MenuItem[] = defaultMenuItems
  ): ParsedOrder {
    const firstMessage = splitIncomingMessages(messageText)[0] || "";
    const parsed = parseOrder(firstMessage, menu);
    const name = normalizeSpaces(customerName) || parsed.customer_name;
    const phone = extractPhone(customerPhone) || parsed.customer_phone || normalizeSpaces(customerPhone);
    const warnings = [...parsed.warnings];

    if (!name) warnings.push("Name missing");
    if (!phone) warnings.push("Mobile missing");
    if (!parsed.items.length && firstMessage.trim()) warnings.push("Unknown item or unclear order");

    return {
      ...parsed,
      customer_name: name,
      customer_phone: phone,
      warnings: Array.from(new Set(warnings))
    };
  }

  const api = {
    canonicalizeItemName,
    classifyItemType,
    defaultMenuItems,
    extractPhone,
    itemAliases,
    makeAliasRules,
    normalizeQuantityText,
    parseOrder,
    parseQuantity,
    processIncomingWhatsAppMessage,
    splitIncomingMessages
  };

  global.RamaParser = api;
  if (typeof module !== "undefined" && module.exports) {
    module.exports = api;
  }
})(typeof globalThis !== "undefined" ? globalThis : window);
