import { getSupabaseAdmin } from '@/lib/supabase';
import { getClaudeClient, CLAUDE_MODEL } from '@/lib/claude';
import { ProductMapping, ReferencePrice, Supplier } from '@/types';

export interface MatchResult {
  matched_product_name: string;
  matched_product_id: string | null;
  negotiated_price: number | null;
  match_method: 'exact' | 'keyword' | 'claude' | 'unmatched';
}

// ===== CACHES =====
let mappingsCache: Map<string, ProductMapping> = new Map();
let referencePricesCache: ReferencePrice[] = [];

export async function loadCaches(supplier?: Supplier, analysisMonth?: string) {
  const supabase = getSupabaseAdmin();

  let mappingsQuery = supabase.from('product_mappings').select('*');
  if (supplier) mappingsQuery = mappingsQuery.eq('supplier', supplier);
  const { data: mappings } = await mappingsQuery;

  mappingsCache = new Map();
  for (const m of mappings ?? []) {
    mappingsCache.set(`${m.supplier}:${m.code_article}`, m as ProductMapping);
  }

  if (analysisMonth) {
    const [year, month] = analysisMonth.split('-').map(Number);
    const startOfMonth = `${year}-${String(month).padStart(2, '0')}-01`;
    const endOfMonth = new Date(year, month, 0).toISOString().split('T')[0];

    let pricesQuery = supabase
      .from('reference_prices')
      .select('*')
      .lte('valid_from', endOfMonth)
      .or(`valid_to.is.null,valid_to.gte.${startOfMonth}`);
    if (supplier) pricesQuery = pricesQuery.eq('supplier', supplier);
    const { data: prices } = await pricesQuery;

    const priceMap = new Map<string, ReferencePrice>();
    for (const p of (prices ?? []) as ReferencePrice[]) {
      const key = `${p.supplier}:${p.product_name.toLowerCase()}`;
      const existing = priceMap.get(key);
      if (!existing || p.valid_from > existing.valid_from) {
        priceMap.set(key, p);
      }
    }
    referencePricesCache = Array.from(priceMap.values());
    console.log(`[CACHE] Loaded ${mappingsCache.size} mappings, ${referencePricesCache.length} reference prices (date-aware: ${analysisMonth})`);
  } else {
    let pricesQuery = supabase
      .from('reference_prices')
      .select('*')
      .is('valid_to', null);
    if (supplier) pricesQuery = pricesQuery.eq('supplier', supplier);
    const { data: prices } = await pricesQuery;
    referencePricesCache = (prices ?? []) as ReferencePrice[];
    console.log(`[CACHE] Loaded ${mappingsCache.size} mappings, ${referencePricesCache.length} reference prices (no date filter)`);
  }
}

// ===== PART A: STRUCTURED PRODUCT COMPONENT PARSER =====

interface ProductComponents {
  gamme: string | null;       // Pure, Motion, Insio, Silk, Styletto, Omega AI, etc.
  niveau: string | null;      // 3, 5, 7 (Signia/ReSound) or 12, 16, 20, 24 (Starkey) or 4.2, 4.7
  generation: string | null;  // IX, AX, X, NX, PX, AI, or null
  forme: string | null;       // C&G, 312, CIC, IIC, ITC, ITE, mRIC, RIC RT, BTE, etc.
  sousType: string | null;    // P, SP, M, BCT, S, T, M/P/SP, or null
  isPack: boolean;
  isCros: boolean;
}

// Known product families by supplier
const GAMMES: Record<string, RegExp> = {
  // Signia
  'Pure': /\bPURE\b/i,
  'Motion': /\bMOTION\b/i,
  'Insio': /\bINSIO\b/i,
  'Silk': /\bSILK\b/i,
  'Styletto': /\bSTYLETTO\b/i,
  'Intuis': /\bINTUIS\b/i,
  'Active Pro': /\bACTIVE\s*PRO\b/i,
  // Starkey
  'Omega AI': /\bOMEGA\s*AI\b/i,
  'Genesis AI': /\bGENESIS\s*AI\b/i,
  'G Series AI': /\bG\s*SERIES\s*AI\b/i,
  'Evolv AI': /\bEVOLV\s*AI\b/i,
  'Edge AI': /\bEDGE\s*AI\b/i,
  'Signature Series': /\bSIGNATURE\s*SERIES\b/i,
  'Instant Fit AI': /\bINSTANT\s*FIT\s*AI\b/i,
  // ReSound
  'Nexia': /\bNEXIA\b/i,
  'Omnia': /\bOMNIA\b/i,
  'Enzo Q': /\bENZO\s*Q\b/i,
  'Enzo IA': /\bENZO\s*IA\b/i,
  'Vivia': /\bVIVIA\b/i,
  'Vibrant': /\bVIBRANT\b/i,
  'Savi': /\bSAVI\b/i,
  'Wing': /\bWING\b/i,
};

// Colors and other noise to strip
const NOISE_WORDS = /\b(BLK|GPH|SLH|DKC|SB|RGD|SNW|MOC|TRD|TBLU|BLKG|SNWG|BGG|PRL|BG|FGD|GNT|DBR|GR|SILVER|BEIGE|CARAMEL|GRAPHITE|CHESTNUT|ESPRESSO|CHAMPAGNE|STERLING|TECH\s*BLACK|COS|WHITE|GRAPHITE\s*GRAY|SNW\s*RGD|SNW\s*SLV|BLK\s*GPH|BLK\s*SLV|BLK\s*BLKG|BG\s*BGG|COS\s*RGD)\b/gi;
const PREFIX_STRIP = /^(HA-N |HA |S_ITE-N |S_ITE |C_ITE |STARKEY |KIT |SET )/i;
const SUFFIX_STRIP = /\s*(ENVOI_DEPOT|ACCEPTATION_DEPOT|ENVOI|DEPOT)\s*$/i;

function parseProductComponents(text: string): ProductComponents {
  // Clean the text
  let clean = text
    .replace(PREFIX_STRIP, '')
    .replace(SUFFIX_STRIP, '')
    .replace(NOISE_WORDS, '')
    .replace(/\s*[RL]\s*$/i, '') // trailing R/L (side indicator)
    .replace(/\s*\([^)]*\)\s*/g, (m) => {
      // Keep (pack)/(unité), strip everything else like (T3), (T), (N)
      if (/pack|unit/i.test(m)) return m;
      return ' ';
    })
    .replace(/\s+/g, ' ')
    .trim();

  const result: ProductComponents = {
    gamme: null,
    niveau: null,
    generation: null,
    forme: null,
    sousType: null,
    isPack: /\b(KIT|SET|PACK)\b/i.test(text) || /\(pack\)/i.test(text),
    isCros: /\bCROS\b/i.test(text),
  };

  // Extract gamme (longest match first)
  const gammeEntries = Object.entries(GAMMES).sort((a, b) => b[0].length - a[0].length);
  for (const [name, regex] of gammeEntries) {
    if (regex.test(clean)) {
      result.gamme = name;
      clean = clean.replace(regex, ' ').trim();
      break;
    }
  }

  // Extract niveau + generation for Signia/ReSound style: "7IX", "5AX", "3X", "3NX", "3PX"
  const signiaLevel = clean.match(/\b(\d+)\s*(IX|AX|NX|PX)\b/i) || clean.match(/\b(\d+)\s*(X)\b/i);
  if (signiaLevel) {
    result.niveau = signiaLevel[1];
    result.generation = signiaLevel[2].toUpperCase();
    clean = clean.replace(signiaLevel[0], ' ').trim();
  }

  // Extract niveau for Starkey style: standalone number like "24", "16", "2400"
  if (!result.niveau) {
    const starkeyLevel = clean.match(/\b(1200|1600|2000|2400|12|16|20|24)\b/);
    if (starkeyLevel) {
      result.niveau = starkeyLevel[1];
      result.generation = 'AI'; // Starkey products are all "AI"
      clean = clean.replace(starkeyLevel[0], ' ').trim();
    }
  }

  // Extract standalone generation (no numeric level) — for CROS and Active Pro
  if (!result.niveau && !result.generation) {
    const standaloneGen = clean.match(/\b(IX|AX|NX|PX)\b/i);
    if (standaloneGen) {
      result.generation = standaloneGen[1].toUpperCase();
      clean = clean.replace(standaloneGen[0], ' ').trim();
    } else if (/\bX\b/.test(clean) && !/\bEU\b/i.test(clean)) {
      // Standalone "X" — careful not to match "EU" or other false positives
      result.generation = 'X';
      clean = clean.replace(/\bX\b/, ' ').trim();
    }
  }

  // Extract niveau for Intuis style: "4.2", "4.7"
  if (!result.niveau) {
    const intuisLevel = clean.match(/\b(4\.\d)\b/);
    if (intuisLevel) {
      result.niveau = intuisLevel[1];
      clean = clean.replace(intuisLevel[0], ' ').trim();
    }
  }

  // Extract niveau for ReSound style: "9-60", "7-61", "5-88"
  if (!result.niveau) {
    const resoundLevel = clean.match(/\b(\d+)\s*[-]\s*(\d+)\b/);
    if (resoundLevel) {
      result.niveau = resoundLevel[1]; // "9" from "9-60"
      result.forme = resoundLevel[0].replace(/\s/g, ''); // "9-60"
      clean = clean.replace(resoundLevel[0], ' ').trim();
    }
  }

  // Extract forme (form factor)
  if (/\bBCT\b/i.test(clean)) { result.forme = 'BCT'; result.sousType = 'BCT'; }
  if (/\bC&G\b|C&GO\b/i.test(clean) || /\bC G\b/i.test(clean)) {
    result.forme = (result.forme ? result.forme + ' ' : '') + 'C&G';
  }
  if (/\b312\b/.test(clean) && !result.forme?.includes('-')) result.forme = '312';
  if (/\bmRIC\b/i.test(clean)) result.forme = 'mRIC R';
  if (/\bRIC\s*RT\b/i.test(clean)) result.forme = 'RIC RT';
  if (/\bRIC\s*312\b/i.test(clean)) result.forme = 'RIC 312';
  if (/\bCIC\b/i.test(clean) && !result.forme) result.forme = 'CIC';
  if (/\bIIC\b/i.test(clean) && !result.forme) result.forme = 'IIC';
  if (/\bITC\b/i.test(clean) && !result.forme) result.forme = 'ITC R';
  if (/\bITE\b/i.test(clean) && !result.forme) result.forme = 'ITE R';
  if (/\bBTE\b/i.test(clean) && !result.forme) result.forme = 'BTE';

  // Extract sous-type (P, SP, M, S, T)
  if (/\bM\/P\/SP\b/i.test(clean)) result.sousType = 'M/P/SP';
  else if (/\bSP\b/i.test(clean) && !result.sousType) result.sousType = 'M/P/SP';
  else if (/\bP\b/i.test(clean) && result.gamme === 'Motion') result.sousType = 'M/P/SP';
  else if (/\bM\b/i.test(clean) && result.gamme === 'Motion') result.sousType = 'M/P/SP';
  if (/\bT\b/i.test(clean) && result.gamme === 'Pure' && /C&G/i.test(clean)) result.sousType = 'T';
  if (/\bS\b/i.test(clean) && result.gamme === 'Styletto') result.sousType = 'S';

  return result;
}

// Parse reference price name using the same logic
function parseRefComponents(productName: string): ProductComponents {
  let text = productName;

  const result: ProductComponents = {
    gamme: null,
    niveau: null,
    generation: null,
    forme: null,
    sousType: null,
    isPack: /\(pack\)/i.test(text),
    isCros: /\bCROS\b/i.test(text),
  };

  // Extract gamme
  const gammeEntries = Object.entries(GAMMES).sort((a, b) => b[0].length - a[0].length);
  for (const [name, regex] of gammeEntries) {
    if (regex.test(text)) {
      result.gamme = name;
      text = text.replace(regex, ' ').trim();
      break;
    }
  }

  // Extract niveau + generation: "7 IX", "3 AX", "7X", "3 Nx", "3 Px", "7 X", "3 X"
  const levelGen = text.match(/\b(\d+)\s*(IX|AX|NX|PX|iX)\b/i) || text.match(/\b(\d+)\s*(X)\b/i);
  if (levelGen) {
    result.niveau = levelGen[1];
    result.generation = levelGen[2].toUpperCase();
  }

  // Starkey levels: "24", "16", "2400"
  if (!result.niveau) {
    const stk = text.match(/\b(1200|1600|2000|2400|12|16|20|24)\b/);
    if (stk) {
      result.niveau = stk[1];
      result.generation = 'AI';
    }
  }

  // Standalone generation (CROS, Active Pro)
  if (!result.niveau && !result.generation) {
    const standaloneGen = text.match(/\b(IX|AX|NX|PX|iX)\b/i);
    if (standaloneGen) {
      result.generation = standaloneGen[1].toUpperCase();
    } else if (/\b(?<!\d)X\b/.test(text)) {
      result.generation = 'X';
    }
  }

  // Intuis: "4.2", "4.7"
  if (!result.niveau) {
    const intuis = text.match(/\b(4\.\d)\b/);
    if (intuis) result.niveau = intuis[1];
  }

  // ReSound levels: "9-60", "5-61"
  if (!result.niveau) {
    const rs = text.match(/\b(\d+)\s*[-]\s*(\d+)\b/);
    if (rs) {
      result.niveau = rs[1];
      result.forme = rs[0].replace(/\s/g, '');
    }
  }

  // Forme
  if (/\bBCT\b/i.test(text)) { result.forme = 'BCT'; result.sousType = 'BCT'; }
  if (/\bC&G\b|C&Go\b/i.test(text)) result.forme = (result.forme ? result.forme + ' ' : '') + 'C&G';
  if (/\b312\b/.test(text) && !result.forme?.includes('-')) result.forme = '312';
  if (/\bmRIC\b/i.test(text)) result.forme = 'mRIC R';
  if (/\bRIC\s*RT\b/i.test(text)) result.forme = 'RIC RT';
  if (/\bRIC\s*312\b/i.test(text)) result.forme = 'RIC 312';
  if (/\bCIC\b/i.test(text) && !result.forme) result.forme = 'CIC';
  if (/\bIIC\b/i.test(text) && !result.forme) result.forme = 'IIC';
  if (/\bITC\b/i.test(text) && !result.forme) result.forme = 'ITC R';
  if (/\bITE\b/i.test(text) && !result.forme) result.forme = 'ITE R';
  if (/\bBTE\b/i.test(text) && !result.forme) result.forme = 'BTE';
  if (/\bPower\b/i.test(text) && !result.forme) result.forme = 'BTE';

  // Sous-type
  if (/\bM\/P\/SP\b/i.test(text)) result.sousType = 'M/P/SP';
  else if (/\bSP\b/i.test(text) && result.gamme === 'Motion') result.sousType = 'M/P/SP';
  else if (/\bP\b/i.test(text) && result.gamme === 'Motion') result.sousType = 'M/P/SP';

  return result;
}

// ===== RESOUND CODE ARTICLE PARSER =====
// ReSound codes follow: [GammePrefix][Niveau][Forme]-[Suffix]
// e.g. NX960S-DRWC → Nexia 9-60 R, VI560S-DRWC → Vivia 5-60 R

const RESOUND_PREFIXES: Record<string, string> = {
  NX: 'Nexia',
  VI: 'Vivia',
  VB: 'Vibrant',
  SA: 'Savi',
  EQ: 'Enzo Q',
  EI: 'Enzo IA',
  RT: 'Omnia',
  RU: 'Omnia',
  RE: 'ReSound',
  CX: 'Custom',
  WN: 'Wing',
};

interface ResoundParsed {
  gamme: string;
  niveau: string;
  forme: string; // e.g. "9-60", "7-61", "9-98"
  formeR: boolean; // rechargeable (60S suffix)
}

function parseResoundCode(text: string): ResoundParsed | null {
  // Take just the main code part before any comma (e.g. "NX960S-DRWC,RIE,SPK SIL" → "NX960S-DRWC")
  const mainCode = text.split(',')[0].trim();

  // Try to match the pattern: [2-letter prefix][1 digit niveau][2-3 digit forme/CIC/ITC][optional S]-suffix
  const m = mainCode.match(/^([A-Z]{2})(\d)(CIC|IIC|ITC|ITE|\d{2,3})(S?)(?:-.*)?$/i);
  if (!m) return null;

  const prefix = m[1].toUpperCase();
  const niveau = m[2];
  const formeRaw = m[3];
  const isRechargeable = m[4] === 'S';

  const gamme = RESOUND_PREFIXES[prefix];
  if (!gamme) return null;

  // Handle CIC/IIC/ITC/ITE forme (e.g. NX9CIC-HP)
  const isIntraCode = /^(CIC|IIC|ITC|ITE)$/i.test(formeRaw);
  const forme = isIntraCode ? formeRaw.toUpperCase() : `${niveau}-${formeRaw}`;

  // Also check suffix for "C" (charging) — e.g. -DRWC vs -DRW
  const suffixMatch = mainCode.match(/-([A-Z]+)$/i);
  const suffixHasC = suffixMatch ? /C$/i.test(suffixMatch[1]) : false;
  const isRechargeableFromSuffix = isRechargeable || suffixHasC;

  return {
    gamme,
    niveau,
    forme,
    formeR: isRechargeableFromSuffix,
  };
}

// Match a ReSound parsed code to reference prices structurally
function matchResoundStructural(supplier: Supplier, codeArticle: string, description: string): MatchResult | null {
  if (supplier !== 'resound') return null;

  // Try code_article first, then description (ReSound often has numeric code_article with real code in description)
  let parsed = parseResoundCode(codeArticle);
  if (!parsed) parsed = parseResoundCode(description);
  if (!parsed) return null;

  console.log(`[RESOUND PARSE] ${codeArticle} → gamme=${parsed.gamme}, niveau=${parsed.niveau}, forme=${parsed.forme}, R=${parsed.formeR}`);

  // Filter to human-readable names only (skip code-format like "VI960S-DRWC", "NX4CIC", "RE961-DRWC")
  const supplierPrices = referencePricesCache.filter((p) => {
    if (p.supplier !== 'resound') return false;
    // Skip products that look like raw codes (2-3 letters + digits + dash)
    if (/^[A-Z]{2,3}\d.*-/i.test(p.product_name)) return false;
    if (/^[A-Z]{2}\d+(CIC|IIC|ITC|ITE)/i.test(p.product_name)) return false;
    return true;
  });

  // CIC/ITC/ITE from code or description
  const isIntraCode = /^(CIC|IIC|ITC|ITE)$/i.test(parsed.forme);
  const isCIC = isIntraCode ? /CIC/i.test(parsed.forme) : /\bCIC\b/i.test(description);
  const isITC = isIntraCode ? /ITC/i.test(parsed.forme) : /\bITC\b/i.test(description);
  const isITE = isIntraCode ? /ITE/i.test(parsed.forme) : /\bITE\b/i.test(description);
  const isIIC = isIntraCode ? /IIC/i.test(parsed.forme) : /\bIIC\b/i.test(description);
  const isIntra = isCIC || isITC || isITE || isIIC;

  let bestMatch: ReferencePrice | null = null;
  let bestScore = 0;

  for (const ref of supplierPrices) {
    const refName = ref.product_name;

    // Check gamme matches
    if (!new RegExp(`\\b${parsed.gamme}\\b`, 'i').test(refName)) continue;

    // Check niveau matches - ref names like "Nexia 9-60 R" or "Vivia 5-61 / 5-62"
    const refLevelMatch = refName.match(/\b(\d+)\s*[-]\s*(\d+)/);
    const refIntraMatch = refName.match(/\b(\d+)\s+(CIC|IIC|ITC|ITE)/i);

    let score = 0;

    if (isIntra && refIntraMatch) {
      // Intra matching: niveau must match
      if (refIntraMatch[1] !== parsed.niveau) continue;
      const refIntraType = refIntraMatch[2].toUpperCase();
      if (isCIC && refIntraType === 'CIC') score = 100;
      else if (isIIC && (refIntraType === 'IIC' || refIntraType === 'CIC')) score = 100;
      else if (isITC && (refIntraType === 'ITC' || refIntraType === 'ITE')) score = 100;
      else if (isITE && (refIntraType === 'ITE' || refIntraType === 'ITC')) score = 100;
      else continue;
    } else if (!isIntra && refLevelMatch) {
      // Standard matching: niveau AND forme digits must match
      if (refLevelMatch[1] !== parsed.niveau) continue;

      // Forme matching: 60 matches "60", 61 matches "61/62", 77 matches "77/88", 98 matches "98/88"
      const refForme = refLevelMatch[2];
      const descForme = parsed.forme.split('-')[1]; // "60" from "9-60"

      // Check for slash alternatives: "61 / 62" or "77 / 88" or "98 / 88"
      const refAlternatives = refName.match(/(\d+)\s*\/\s*(?:\d+-)?(\d+)/);
      const matchesForme = refForme === descForme ||
        (refAlternatives && (refAlternatives[1] === descForme || refAlternatives[2] === descForme));

      if (!matchesForme) continue;

      // Rechargeable preference: prefer R↔R and non-R↔non-R
      // "R" in ref name (but not RIE/RIC) indicates rechargeable
      const refNameClean = refName.replace(/\bRIE\b/gi, '').replace(/\bRIC\b/gi, '');
      const refIsR = /\b\d+-\d+\s+R\b/.test(refNameClean) || /\bR\s*$/.test(refNameClean);
      if (parsed.formeR && refIsR) score = 105; // prefer R variant
      else if (!parsed.formeR && !refIsR) score = 105; // prefer non-R variant
      else score = 100; // acceptable but not preferred
    } else {
      continue;
    }

    if (score > bestScore) {
      bestScore = score;
      bestMatch = ref;
    }
  }

  if (!bestMatch || bestScore < 100) return null;

  console.log(`[RESOUND MATCH] ${codeArticle} → "${bestMatch.product_name}" (${bestMatch.price_ht}€)`);

  return {
    matched_product_name: bestMatch.product_name,
    matched_product_id: bestMatch.id,
    negotiated_price: bestMatch.price_ht,
    match_method: 'keyword',
  };
}

// ===== PART B: STRICT STRUCTURAL MATCHING =====

function structuralMatch(desc: ProductComponents, ref: ProductComponents): number {
  // Returns score 0-100. Only 100 = valid keyword match.

  // CROS must match exactly
  if (desc.isCros !== ref.isCros) return 0;

  // Pack must match
  if (desc.isPack !== ref.isPack) return 0;

  // Gamme MUST match (mandatory)
  if (!desc.gamme || !ref.gamme) return 0;
  if (desc.gamme.toLowerCase() !== ref.gamme.toLowerCase()) return 0;

  let score = 30; // gamme matched

  // Niveau matching
  if (desc.niveau && ref.niveau) {
    if (desc.niveau !== ref.niveau) return 0;
    score += 30; // niveau matched
  } else if (!desc.niveau && !ref.niveau) {
    // No niveau on either (CROS, Active Pro) — OK if generation matches
    score += 30;
  } else {
    // One has niveau, other doesn't — mismatch
    return 0;
  }

  // Generation MUST match strictly — never partial credit
  if (desc.generation && ref.generation) {
    if (desc.generation !== ref.generation) return 0;
    score += 20;
  } else if (!desc.generation && !ref.generation) {
    score += 20; // neither has gen — OK
  } else {
    // One has generation, other doesn't → REJECT (prevents IX matching non-IX)
    return 0;
  }

  // Forme: C&G, 312, CIC, mRIC, etc.
  const descForme = (desc.forme || '').toLowerCase().replace(/\s+/g, '');
  const refForme = (ref.forme || '').toLowerCase().replace(/\s+/g, '');
  if (descForme && refForme) {
    const descHasCG = /c&g|c&go/i.test(descForme);
    const refHasCG = /c&g|c&go/i.test(refForme);
    const descHas312 = descForme.includes('312');
    const refHas312 = refForme.includes('312');
    const descHasBCT = descForme.includes('bct');
    const refHasBCT = refForme.includes('bct');

    if (descHasCG !== refHasCG) return 0;
    if (descHas312 !== refHas312) return 0;
    if (descHasBCT !== refHasBCT) return 0;

    score += 20;
  } else if (descForme || refForme) {
    const hasCG = (descForme || refForme).includes('c&g');
    if (hasCG) return 0;
    // For Insio/Silk intras: CIC/IIC in desc is implicit (all Insio are CIC/IIC)
    // So CIC in desc matching a ref without forme is OK
    const implicitIntra = desc.gamme && /^(Insio|Silk)$/i.test(desc.gamme) && /^(cic|iic)$/i.test(descForme);
    if (implicitIntra) {
      score += 20; // CIC is implicit for Insio/Silk
    } else {
      score += 10;
    }
  } else {
    score += 20; // neither has forme — OK
  }

  return Math.min(score, 100);
}

// Level 1: Exact lookup in product_mappings
function matchExact(supplier: Supplier, codeArticle: string): MatchResult | null {
  const key = `${supplier}:${codeArticle}`;
  const mapping = mappingsCache.get(key);
  if (!mapping || !mapping.matched_product_name) return null;

  // Try by ID first
  let refPrice = mapping.matched_product_id
    ? referencePricesCache.find((p) => p.id === mapping.matched_product_id)
    : null;

  // Fallback: search by product name (case-insensitive)
  if (!refPrice) {
    refPrice = referencePricesCache.find(
      (p) =>
        p.supplier === supplier &&
        p.product_name.toLowerCase() === mapping.matched_product_name!.toLowerCase()
    );
  }

  // If still no price found but mapping has a name, return it with null price
  // The caller (matchProduct) will decide whether to fall through
  return {
    matched_product_name: mapping.matched_product_name,
    matched_product_id: refPrice?.id ?? mapping.matched_product_id,
    negotiated_price: refPrice?.price_ht ?? null,
    match_method: 'exact',
  };
}

// Level 2: Structural keyword matching — ONLY accepts score = 100
function matchKeywordStructural(supplier: Supplier, description: string, codeArticle?: string): MatchResult | null {
  // For ReSound: try code article parsing first (more reliable than description parsing)
  if (supplier === 'resound' && codeArticle) {
    const resoundMatch = matchResoundStructural(supplier, codeArticle, description);
    if (resoundMatch) return resoundMatch;
  }

  const descComponents = parseProductComponents(description);

  // Must have at least gamme + niveau to attempt matching
  if (!descComponents.gamme || !descComponents.niveau) return null;

  let bestMatch: ReferencePrice | null = null;
  let bestScore = 0;

  for (const refPrice of referencePricesCache) {
    if (refPrice.supplier !== supplier) continue;

    const refComponents = parseRefComponents(refPrice.product_name);
    const score = structuralMatch(descComponents, refComponents);

    if (score > bestScore) {
      bestScore = score;
      bestMatch = refPrice;
    }
  }

  // STRICT: only accept score = 100 (all components matched perfectly)
  if (!bestMatch || bestScore < 100) return null;

  return {
    matched_product_name: bestMatch.product_name,
    matched_product_id: bestMatch.id,
    negotiated_price: bestMatch.price_ht,
    match_method: 'keyword',
  };
}

// Level 3: Claude API matching
async function matchClaude(
  supplier: Supplier,
  codeArticle: string,
  description: string
): Promise<MatchResult | null> {
  const supplierPrices = referencePricesCache.filter((p) => p.supplier === supplier);
  if (supplierPrices.length === 0) return null;

  const priceList = supplierPrices
    .map((p) => `- "${p.product_name}" (${p.category}, ${p.price_ht}€ HT)`)
    .join('\n');

  // Build supplier-specific decoding rules for Claude
  let supplierRules = '';
  if (supplier === 'resound') {
    supplierRules = `
DÉCODAGE CODES ARTICLES RESOUND (source: Instructions Phacet) :
Les codes ReSound suivent le format [PréfixeGamme][Niveau][Forme][S?]-[Suffixe]
Préfixes gammes : NX=Nexia, VI=Vivia, VB=Vibrant, SA=Savi, EQ=Enzo Q, EI=Enzo IA, RT=Omnia, RU=Omnia, WN=Wing, RE=ReSound, CX=Custom
Le chiffre APRÈS le préfixe = NIVEAU (NX9=Nexia 9, VI5=Vivia 5, SA4=Savi 4)
Les 2-3 chiffres/lettres suivants = FORME :
  60S = RIC rechargeable → "-60 R" en base
  61 = RIC → "-61 / -62" en base
  62 = RIC bobine T → "-61 / -62" en base
  77 = BTE → "-77 / -88 R" en base
  88 = BTE puissant → "-77 / -88 R" en base
  98 = BTE très puissant → "-98 / -88" en base
  CIC = intra CIC → "CIC" en base
  ITC = intra → "ITE R" en base
Mappings exacts prioritaires :
  NX960S → Nexia 9-60 R, NX560S → Nexia 5-60 R, NX761 → Nexia 7-61/7-62
  VI960S → Vivia 9-60 R, VI560S → Vivia 5-60 R, VI760S → Vivia 7-60 R
  EQ998 → Enzo Q 9-98/9-88, VB561 → Vibrant 5-61, VB5CIC → Vibrant 5-10/5-30
  RU960 → Omnia 9-60 R, RT961 → Omnia 9-61 R
  VI160S → Vivia 1 n'existe PAS dans notre base → null
IMPORTANT : Certains produits existent en base avec leur code technique ET leur nom humain. Toujours préférer le nom humain.
`;
  } else if (supplier === 'signia') {
    supplierRules = `
DÉCODAGE DESCRIPTIONS SIGNIA (source: Instructions Phacet) :
Format EDI : [Préfixe] [Gamme] [Forme] [SousType] [NiveauGénération] [Couleurs]
Préfixes à ignorer : HA, HA-N, S_ITE, C_ITE, KIT, SET
Familles : PR=Appareils, AC=Accessoire, CONT=Contribution, EMB=Embout, PD/PT=Port, REPA=Réparation, PO=Fidélité, ECO=Taxe, PUB=Pub
Niveaux+Générations : "7IX"→niveau 7 gen IX, "5AX"→niveau 5 gen AX, "3X"→niveau 3 gen X, "3NX"→niveau 3 gen NX, "3PX"→niveau 3 gen PX
Formes : "C&G"="C&Go", P/SP/M dans Motion→"M/P/SP", BCT=BCT
Acronymes : BTE=Behind The Ear, CIC=Completely In Canal, CIC W/NW=CIC Wireless/Non-Wireless, IIC NW=Invisible In Canal, ITC R=In The Canal Rechargeable, MRIC R=Micro Receiver In Canal Rechargeable, RIC RT=Receiver In Canal Rechargeable Telecoil
Couleurs à ignorer : BLK, GPH, SLH, DKC, SB, RGD, SNW, MOC, TRD, TBLU, BLKG, SNWG, BGG, PRL, BG, SILVER, BEIGE, CARAMEL, GRAPHITE, CHESTNUT, etc.
Matcher en priorité par : 1) Libellé, 2) Famille, 3) Prix
`;
  } else if (supplier === 'starkey') {
    supplierRules = `
DÉCODAGE DESCRIPTIONS STARKEY :
Format EDI : STARKEY [Gamme] AI [Niveau] [Forme] [Couleur]
Gammes : Omega AI, Genesis AI, G Series AI, Evolv AI, Edge AI, Signature Series, Instant Fit AI
Niveaux : 12 (entrée), 16, 20, 24 (haut de gamme)
Formes : mRIC R (Micro Receiver In Canal Rechargeable), RIC RT (Receiver In Canal Rechargeable Telecoil), RIC 312, CIC (Completely In Canal), IIC (Invisible In Canal), ITC R (In The Canal Rechargeable), ITE R (In The Ear Rechargeable), BTE (Behind The Ear)
Couleurs à ignorer : SILVER, BEIGE, CARAMEL, GRAPHITE, CHESTNUT, ESPRESSO, NW, etc.
`;
  }

  const claude = getClaudeClient();

  try {
    const response = await claude.messages.create({
      model: CLAUDE_MODEL,
      max_tokens: 500,
      messages: [
        {
          role: 'user',
          content: `Tu es un expert en audioprothèses. Fais correspondre le produit facturé avec le produit dans notre base tarifaire.

RÈGLES STRICTES :
- Le NIVEAU technologique DOIT correspondre exactement : 3/5/7 (Signia/ReSound) ou 12/16/20/24 (Starkey).
- La FORME DOIT correspondre : C&G, 312, CIC, IIC, ITC, ITE, mRIC, RIC RT, BTE, BCT, etc.
- Un produit CROS ne matche qu'un produit CROS.
- Ignore les couleurs.
- Si tu n'es pas sûr à 80%+ → réponds null. MIEUX VAUT null QU'UN FAUX MATCH.
- Si le produit exact n'existe pas dans la liste, retourne null. Un faux match est PIRE qu'un non-match.
- Ne force jamais un match approximatif. Le niveau ET la forme doivent correspondre exactement.
${supplierRules}
Produit facturé :
- Code article : ${codeArticle}
- Description : ${description}
- Fournisseur : ${supplier}

Liste des produits dans notre base tarifaire ${supplier} :
${priceList}

Réponds UNIQUEMENT avec un JSON (pas de markdown) :
{"product_name": "nom exact du produit dans la liste", "confidence": 0-100, "rationale": "explication courte"}

Si aucun produit ne correspond ou confiance < 80 :
{"product_name": null, "confidence": 0, "rationale": "raison"}`,
        },
      ],
    });

    const text = response.content[0].type === 'text' ? response.content[0].text : '';
    // Strip markdown fences if present (```json ... ```)
    const cleanText = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '').trim();
    let parsed;
    try {
      parsed = JSON.parse(cleanText);
    } catch {
      console.warn(`[MATCH L3] JSON parse failed for ${codeArticle}, retrying...`);
      // Retry once
      const retry = await claude.messages.create({
        model: CLAUDE_MODEL,
        max_tokens: 500,
        messages: [
          {
            role: 'user',
            content: `Réponds UNIQUEMENT avec un JSON valide (sans markdown, sans backticks).
Fais correspondre ce produit : "${description}" (code: ${codeArticle}, fournisseur: ${supplier})
avec un de ces produits : ${supplierPrices.slice(0, 30).map(p => p.product_name).join(', ')}
Format: {"product_name": "nom exact ou null", "confidence": 0-100, "rationale": "raison"}`,
          },
        ],
      });
      const retryText = retry.content[0].type === 'text' ? retry.content[0].text : '';
      const retryClean = retryText.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '').trim();
      try {
        parsed = JSON.parse(retryClean);
      } catch {
        console.error(`[MATCH L3] JSON parse failed twice for ${codeArticle}, returning null`);
        return null;
      }
    }

    if (!parsed.product_name || parsed.confidence < 80) return null;

    const refPrice = supplierPrices.find(
      (p) => p.product_name.toLowerCase() === parsed.product_name.toLowerCase()
    );
    if (!refPrice) return null;

    // Final guards
    const descHasCros = /\bCROS\b/i.test(description);
    const refHasCros = /\bCROS\b/i.test(refPrice.product_name);
    if (descHasCros !== refHasCros) return null;

    const DEVICE_KW = /\b(OMEGA|SIGNATURE|GENESIS|EVOLV|PURE|INSIO|MOTION|STYLETTO|SILK|NEXIA|ENZO|LIVIO|PICASSO|MUSE|INTUIS)\b/i;
    if (DEVICE_KW.test(description) && refPrice.price_ht < 50) return null;

    // Generation guard: reject if EDI generation (IX/AX/X/NX/PX) doesn't match ref
    const descGen = description.match(/\b\d*(IX|AX|NX|PX)\b/i) || description.match(/\b\d*(X)\b/i);
    const refGen = refPrice.product_name.match(/\b\d*\s*(IX|AX|NX|PX|iX)\b/i) || refPrice.product_name.match(/\b(X)\b/);
    if (descGen && refGen) {
      const dg = descGen[1].toUpperCase();
      const rg = refGen[1].toUpperCase();
      if (dg !== rg) {
        console.log(`[MATCH L3 GUARD] ${codeArticle} — generation mismatch: EDI=${dg}, ref=${rg} → reject`);
        return null;
      }
    }
    // If EDI has a generation but ref doesn't (or vice versa), reject
    if (descGen && !refGen) {
      console.log(`[MATCH L3 GUARD] ${codeArticle} — EDI has gen=${descGen[1]} but ref has none → reject`);
      return null;
    }
    if (!descGen && refGen) {
      console.log(`[MATCH L3 GUARD] ${codeArticle} — ref has gen=${refGen[1]} but EDI has none → reject`);
      return null;
    }

    return {
      matched_product_name: refPrice.product_name,
      matched_product_id: refPrice.id,
      negotiated_price: refPrice.price_ht,
      match_method: 'claude',
    };
  } catch (error) {
    console.error(`[MATCH L3 ERROR] Claude API failed for ${codeArticle}:`, error instanceof Error ? error.message : error);
    return null;
  }
}

// Save mapping to Supabase for progressive learning
async function saveMapping(
  supplier: Supplier,
  codeArticle: string,
  description: string,
  result: MatchResult
) {
  const supabase = getSupabaseAdmin();

  const { data: existing } = await supabase
    .from('product_mappings')
    .select('id')
    .eq('supplier', supplier)
    .eq('code_article', codeArticle)
    .limit(1);

  if (existing && existing.length > 0) {
    const { data } = await supabase
      .from('product_mappings')
      .update({
        matched_product_name: result.matched_product_name,
        matched_product_id: result.matched_product_id,
        confidence: result.match_method,
        description_edi: description,
      })
      .eq('id', existing[0].id)
      .select()
      .single();

    if (data) {
      mappingsCache.set(`${supplier}:${codeArticle}`, data as ProductMapping);
    }
  } else {
    const { data } = await supabase
      .from('product_mappings')
      .insert({
        supplier,
        code_article: codeArticle,
        description_edi: description,
        matched_product_name: result.matched_product_name,
        matched_product_id: result.matched_product_id,
        confidence: result.match_method,
        validated: false,
      })
      .select()
      .single();

    if (data) {
      mappingsCache.set(`${supplier}:${codeArticle}`, data as ProductMapping);
    }
  }
}

// Batch Claude API calls — process up to 10 in parallel
async function matchClaudeBatch(
  supplier: Supplier,
  items: { codeArticle: string; description: string }[]
): Promise<Map<string, MatchResult | null>> {
  const results = new Map<string, MatchResult | null>();
  const BATCH_SIZE = 10;

  for (let i = 0; i < items.length; i += BATCH_SIZE) {
    const batch = items.slice(i, i + BATCH_SIZE);
    const promises = batch.map(async (item) => {
      const result = await matchClaude(supplier, item.codeArticle, item.description);
      return { key: item.codeArticle, result };
    });
    const batchResults = await Promise.all(promises);
    for (const { key, result } of batchResults) {
      results.set(key, result);
    }
  }

  return results;
}

// ===== MAIN MATCHING FUNCTION =====

export async function matchProduct(
  supplier: Supplier,
  codeArticle: string,
  description: string,
  invoicedPrice?: number
): Promise<MatchResult> {
  // Level 1: Exact mapping lookup (free, instant)
  const exact = matchExact(supplier, codeArticle);
  if (exact && exact.negotiated_price !== null) {
    // Sanity check: reject wildly implausible prices
    if (invoicedPrice && invoicedPrice > 100 && exact.negotiated_price > 0) {
      const ratio = exact.negotiated_price / invoicedPrice;
      if (ratio < 0.15 || ratio > 10) {
        console.log(`[MATCH L1] ${codeArticle} — prix implausible (ratio=${ratio.toFixed(2)}), skip exact`);
      } else {
        console.log(`[MATCH L1] ${codeArticle} → "${exact.matched_product_name}" (exact)`);
        return exact;
      }
    } else {
      console.log(`[MATCH L1] ${codeArticle} → "${exact.matched_product_name}" (exact)`);
      return exact;
    }
  }
  if (exact && exact.negotiated_price === null) {
    console.log(`[MATCH L1] ${codeArticle} — mapping exists but no price, continue`);
  }

  // Level 2: Structural keyword matching (score must be 100/100)
  const keyword = matchKeywordStructural(supplier, description, codeArticle);
  if (keyword) {
    console.log(`[MATCH L2] ${codeArticle} → "${keyword.matched_product_name}" (keyword structural)`);
    await saveMapping(supplier, codeArticle, description, keyword);
    return keyword;
  }
  console.log(`[MATCH L2] ${codeArticle} — no structural match, falling to Claude API`);

  // Level 3: Claude API (saves result for future exact lookups)
  const claudeMatch = await matchClaude(supplier, codeArticle, description);
  if (claudeMatch) {
    console.log(`[MATCH L3] ${codeArticle} → "${claudeMatch.matched_product_name}" (claude API)`);
    await saveMapping(supplier, codeArticle, description, claudeMatch);
    return claudeMatch;
  }
  console.log(`[MATCH L3] ${codeArticle} — Claude API returned no match → UNMATCHED`);

  // Unmatched
  return {
    matched_product_name: description,
    matched_product_id: null,
    negotiated_price: null,
    match_method: 'unmatched',
  };
}

// Export for batch processing in analyze route
export { matchClaudeBatch };
