import yaml from 'js-yaml'
import fs from 'fs/promises'
import path from 'path'
import readline from 'readline'

const PACKS_SRC = path.resolve('packs_src')
const RECIPES_DIR = path.join(PACKS_SRC, 'recettes-alchimiques')
const FAUNA_DIR = path.join(PACKS_SRC, 'faune-flore-mineraux')

const STOP_OUTILS = new Set([
  'chaudron', 'abri', 'filtre', 'parchemin', 'pilon', 'mortier',
  'bille de plomb', 'bâton', 'craie', 'sac',
])
const STOP_VAGUE = new Set(['cette matière', 'cette poudre', 'cette mixture'])
const STRIP_ADJ = [
  /\s+(lent|rapide|gros|grosse|petit|petite|frais|fraîche|vieux|vieille|jeune|doux|douce|sec|sèche)\s*$/i,
]

const MOTS_NOMBRES = {
  un: 1, une: 1, demi: 0.5, demie: 0.5,
  deux: 2, trois: 3, quatre: 4, cinq: 5,
  six: 6, sept: 7, huit: 8, neuf: 9, dix: 10,
}

const UNITES = [
  'brin', 'pépin', 'écaille', 'mesure', 'doigt', 'déhacoudre',
  'livre', 'pinte', 'poignée',
]

/* ---------- helpers ---------- */

function lowerNoAccent(s) {
  return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim()
}

function stripEntities(s) {
  return s
    .replace(/&rsquo;/g, "'").replace(/&lsquo;/g, "'")
    .replace(/&amp;/g, '&').replace(/&quot;/g, '"')
    .replace(/&lt;/g, '<').replace(/&gt;/g, '>')
    .replace(/&eacute;/g, 'é').replace(/&egrave;/g, 'è')
    .replace(/&ecirc;/g, 'ê').replace(/&euml;/g, 'ë')
    .replace(/&agrave;/g, 'à').replace(/&acirc;/g, 'â')
    .replace(/&ocirc;/g, 'ô').replace(/&icirc;/g, 'î')
    .replace(/&ucirc;/g, 'û').replace(/&ugrave;/g, 'ù')
    .replace(/&ccedil;/g, 'ç').replace(/&Eacute;/g, 'É')
    .replace(/&nbsp;/g, ' ')
    .replace(/\u2019/g, "'")  // curly right quote → ASCII apostrophe
    .replace(/\u2018/g, "'")  // curly left quote → ASCII apostrophe
    .replace(/\s+/g, ' ').trim()
}

function ucFirst(s) {
  return s.charAt(0).toUpperCase() + s.slice(1)
}

/* ---------- compendium map ---------- */

async function buildCompendiumMap() {
  const map = {}
  const files = await fs.readdir(FAUNA_DIR)
  for (const f of files) {
    if (!f.endsWith('.yml')) continue
    const content = await fs.readFile(path.join(FAUNA_DIR, f), 'utf8')
    const data = yaml.load(content)
    const key = lowerNoAccent(data.name)
    if (key) map[key] = { type: data.type, name: data.name }
  }
  return map
}

function singularizeWord(w) {
  // French plural: most words +s, words ending in -eau/-eu +x, -al→-aux etc.
  // We just handle the common cases for our dataset
  return w.replace(/aux$/, 'al').replace(/eaux$/, 'eau').replace(/[sx]$/, '')
}

function lookUp(name, map) {
  const exact = (n) => map[lowerNoAccent(n)]
  if (exact(name)) return exact(name).type
  // Try singularizing each word independently
  const words = name.split(/\s+/)
  for (let i = 0; i < words.length; i++) {
    const variant = [...words]
    variant[i] = singularizeWord(variant[i])
    const joined = variant.join(' ')
    if (joined !== name && exact(joined)) return exact(joined).type
  }
  // Try singularizing all words
  const allSingular = words.map(singularizeWord).join(' ')
  if (allSingular !== name && exact(allSingular)) return exact(allSingular).type
  return 'autre'
}

/* ---------- strong text extraction with merge ---------- */

function extractStrongTexts(html) {
  if (!html) return []
  const strongs = []
  const re = /<strong>(.*?)<\/strong>/gi
  let m
  while ((m = re.exec(html)) !== null) {
    strongs.push({
      text: m[1],
      start: m.index,
      end: m.index + m[0].length,
    })
  }

  // Merge consecutive strongs: merge only if between is empty/whitespace (< 3 chars, no HTML)
  const merged = []
  for (let i = 0; i < strongs.length; i++) {
    const curr = strongs[i]
    let j = i + 1
    let combine = stripEntities(curr.text)

    while (j < strongs.length) {
      const between = html.substring(strongs[j - 1].end, strongs[j].start)
      const betweenClean = between.replace(/\s/g, '')
      // Merge only if between is pure whitespace or empty, and short
      if (betweenClean.length === 0 && between.length < 3) {
        combine += ' ' + stripEntities(strongs[j].text)
        j++
      } else {
        break
      }
    }

    merged.push(combine)
    i = j - 1
  }

  return merged
}

/* ---------- ingredient parsing ---------- */

// de / d' / d\u2019 (handles "de truc", "d'truc", "d' truc", "d\u2019truc", "d\u2019 truc")
const DE_PATTERN = "d(?:e\\s+|'|\\u2019)\\s*"

function parseIngredient(text, compendiumMap) {
  let cleaned = text.trim()
    .replace(new RegExp(`^${DE_PATTERN}`, 'i'), '')  // strip leading "de/d'/d\u2019"
    .replace(/\*$/, '')          // strip trailing asterisk
    .replace(/\s+/g, ' ')
    .trim()

  // Skip — too vague
  if (STOP_VAGUE.has(cleaned.toLowerCase())) return null

  // Skip — starts with d' (continuation, not standalone)
  if (/^d(?:e\s+|'|\u2019)\s*/i.test(cleaned)) return null

  const isTool = [...STOP_OUTILS].some(w => cleaned.toLowerCase().includes(w))

  const unitPattern = `(?:${UNITES.join('|')})s?`
  const dePattern = DE_PATTERN
  const wordNumPattern = `(?:un|une|demi|demie|deux|trois|quatre|cinq|six|sept|huit|neuf|dix)`

  // ---- Pattern A: digits + unit ----
  let re = new RegExp(
    `^(\\d+(?:\\.\\d+)?)\\s+(demi[ -])?(${unitPattern})\\s+${dePattern}(.+)$`, 'i')
  let m = cleaned.match(re)
  if (m) {
    const qty = parseFloat(m[1]) + (m[2] ? 0.5 : 0)
    const unit = m[3].replace(/s$/, '')
    let name = m[4].trim().replace(/^autres?\s+/, '').trim()
    if (STOP_VAGUE.has(name.toLowerCase())) return null
    const type = lookUp(name, compendiumMap)
    return {
      confidence: type !== 'autre' ? 'high' : 'medium',
      ingredient: { nom: ucFirst(name), type, quantite: qty, unite: unit, consommable: !isTool },
      isTool,
    }
  }

  // ---- Pattern B: word-number + unit ----
  re = new RegExp(
    `^(${wordNumPattern})\\s+(demi[ -])?(${unitPattern})\\s+${dePattern}(.+)$`, 'i')
  m = cleaned.match(re)
  if (m) {
    const qty = (MOTS_NOMBRES[m[1].toLowerCase()] ?? 1) + (m[2] ? 0.5 : 0)
    const unit = m[3].replace(/s$/, '')
    let name = m[4].trim().replace(/^autres?\s+/, '').trim()
    if (STOP_VAGUE.has(name.toLowerCase())) return null
    const type = lookUp(name, compendiumMap)
    return {
      confidence: type !== 'autre' ? 'high' : 'medium',
      ingredient: { nom: ucFirst(name), type, quantite: qty, unite: unit, consommable: !isTool },
      isTool,
    }
  }

  // ---- Pattern C: "même quantité de X" -> reference to previous ----
  m = cleaned.match(new RegExp(`^m(?:ê|e)me\\s+quantit(?:é|e)\\s+${dePattern}(.+)$`, 'i'))
  if (m) {
    let name = m[1].trim()
    if (STOP_VAGUE.has(name.toLowerCase())) return null
    const type = lookUp(name, compendiumMap)
    return {
      confidence: 'low',
      ingredient: { nom: ucFirst(name), type, quantite: 1, unite: 'unité', consommable: !isTool },
      isTool,
    }
  }

  // ---- Pattern D: standalone name in compendium (no quantity) ----
  let direct = cleaned.replace(new RegExp(`^${dePattern}`, 'i'), '').trim()
  if (direct.length > 2 && !STOP_VAGUE.has(direct.toLowerCase())) {
    let base = direct
    for (const rx of STRIP_ADJ) base = base.replace(rx, '')
    base = base.trim()
    const type = lookUp(base, compendiumMap)
    if (type !== 'autre') {
      return {
        confidence: 'low',
        ingredient: { nom: ucFirst(base), type, quantite: 1, unite: 'unité', consommable: !isTool },
        isTool,
      }
    }
  }

  // ---- Pattern E: "un/une" + name (no unit) ----
  m = cleaned.match(/^(un|une)\s+(.+)$/i)
  if (m) {
    let name = m[2].trim()
    if (STOP_VAGUE.has(name.toLowerCase())) return null
    for (const rx of STRIP_ADJ) name = name.replace(rx, '')
    name = name.trim()
    if (isTool) {
      return {
        confidence: 'dubious',
        ingredient: { nom: ucFirst(name), type: 'autre', quantite: 1, unite: 'unité', consommable: false },
        isTool: true,
      }
    }
    const type = lookUp(name, compendiumMap)
    return {
      confidence: type !== 'autre' ? 'low' : 'very_low',
      ingredient: { nom: ucFirst(name), type, quantite: 1, unite: 'unité', consommable: true },
      isTool: false,
    }
  }

  return null
}

/* ---------- prompt ---------- */

function createPrompt() {
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
  return {
    question: (q) => new Promise(resolve => rl.question(q, resolve)),
    close: () => rl.close(),
  }
}

const CONF_LABEL = {
  high: '',
  medium: ' [?]',
  low: ' [?]',
  very_low: ' [??]',
  dubious: ' [OUTIL?]',
}

async function promptIngredient(recipeName, item, prompt) {
  const ing = item.ingredient
  const confidence = item.confidence
  const tag = CONF_LABEL[confidence]

  while (true) {
    console.log(
      `\n  ${recipeName}${tag} → "${ing.nom}"  (${ing.quantite} ${ing.unite}, ${ing.type}, conso:${ing.consommable})`)

    const options = ['(y) Oui', '(n) Non']
    if (confidence !== 'dubious') options.push('(t) Outil')
    options.push('(e) Éditer')

    const line = (await prompt.question(`    ${options.join('  › ')}  `)).trim().toLowerCase()

    if (line === '' || line === 'y') return { action: 'accept', ingredient: ing }
    if (line === 'n') return { action: 'reject' }
    if (line === 't') {
      ing.consommable = false
      ing.type = 'autre'
      return { action: 'accept', ingredient: ing }
    }
    if (line === 'e') {
      return { action: 'accept', ingredient: await editIngredient(ing, prompt) }
    }
    console.log('  Invalide. Choisis y/n/e/t')
  }
}

async function editIngredient(ing, prompt) {
  const nom = await prompt.question(`    Nom [${ing.nom}] : `)
  const quantite = await prompt.question(`    Quantité [${ing.quantite}] : `)
  const unite = await prompt.question(`    Unité [${ing.unite}] : `)
  const type = await prompt.question(`    Type [${ing.type}] (herbe/ingredient/plante/objet/autre) : `)
  const conso = await prompt.question(`    Consommable [${ing.consommable ? 'o' : 'n'}] (o/n) : `)

  return {
    nom: nom.trim() || ing.nom,
    quantite: parseFloat(quantite) || ing.quantite,
    unite: unite.trim() || ing.unite,
    type: ['herbe', 'ingredient', 'plante', 'objet', 'autre'].includes(type.trim()) ? type.trim() : ing.type,
    consommable: conso.trim() === 'o' ? true : conso.trim() === 'n' ? false : ing.consommable,
  }
}

/* ---------- process one recipe ---------- */

async function processRecipe(filePath, compendiumMap, opts) {
  const { yes, dryRun } = opts
  const content = await fs.readFile(filePath, 'utf8')
  const data = yaml.load(content)

  // Skip if already has resultat (both fields complete)
  if (data.system?.resultat) {
    const res = tryParseResultat(data.system.resultat)
    if (res && res.type) {
      return { modified: false, reason: 'déjà rempli' }
    }
  }

  // If ingredients exist but resultat is missing, just add resultat
  if (data.system?.ingredients) {
    const parsed = tryParseIngredients(data.system.ingredients)
    if (parsed && parsed.length > 0) {
      data.system.resultat = JSON.stringify(guessResultat(data))
      if (!dryRun) {
        await fs.copyFile(filePath, filePath + '.bak')
        const output = yaml.dump(data, { lineWidth: 300, noRefs: true })
        await fs.writeFile(filePath, output, 'utf8')
        return { modified: true, count: parsed.length, file: path.basename(filePath), note: 'resultat ajouté' }
      } else {
        return { modified: true, count: parsed.length, file: path.basename(filePath), dryRun: true, note: 'resultat ajouté' }
      }
    }
  }

  const html = data.system?.manipulation || ''
  const texts = extractStrongTexts(html)

  const candidates = []
  for (const t of texts) {
    const parsed = parseIngredient(t, compendiumMap)
    if (parsed) candidates.push(parsed)
  }

  if (candidates.length === 0) {
    return { modified: false, reason: 'aucun ingrédient trouvé' }
  }

  const prompt = createPrompt()
  const accepted = []

  for (const item of candidates) {
    if (yes || item.confidence === 'high') {
      accepted.push(item.ingredient)
      continue
    }
    const result = await promptIngredient(data.name, item, prompt)
    if (result.action === 'accept') {
      accepted.push(result.ingredient)
    }
  }
  prompt.close()

  if (accepted.length === 0) {
    return { modified: false, reason: 'tout refusé' }
  }

  // Dedup by normalized name
  const seen = new Set()
  const deduped = []
  for (const ing of accepted) {
    const key = lowerNoAccent(ing.nom)
    if (!seen.has(key)) {
      seen.add(key)
      deduped.push(ing)
    }
  }

  data.system.ingredients = JSON.stringify(deduped)
  data.system.resultat = JSON.stringify(guessResultat(data))

  if (!dryRun) {
    await fs.copyFile(filePath, filePath + '.bak')
    const output = yaml.dump(data, { lineWidth: 300, noRefs: true })
    await fs.writeFile(filePath, output, 'utf8')
    return { modified: true, count: deduped.length, file: path.basename(filePath) }
  } else {
    return {
      modified: true, count: deduped.length,
      file: path.basename(filePath),
      candidates: deduped,
      dryRun: true,
    }
  }
}

function tryParseIngredients(str) {
  try {
    const p = JSON.parse(str)
    return Array.isArray(p) && p.length > 0 ? p : null
  } catch {
    return null
  }
}

function tryParseResultat(str) {
  try {
    const p = JSON.parse(str)
    return p && p.type ? p : null
  } catch {
    return null
  }
}

function guessResultat(data) {
  const name = data.name || ''
  let type = 'objet'
  if (/potion|elixir|liqueur|teinture|huile|lait|remède|baume|onguent/i.test(name)) type = 'potion'
  else if (/cristal|gemme|sable|poudre|bitume/i.test(name)) type = 'objet'
  else if (/gelée/i.test(name)) type = 'nourritureboisson'
  return { type, name, img: data.img || '', quantite: 1 }
}

/* ---------- main ---------- */

async function main() {
  const yes = process.argv.includes('--yes') || process.argv.includes('-y')
  const dryRun = process.argv.includes('--dry-run') || process.argv.includes('-n')

  console.log(`Extraction d'ingrédients des recettes alchimiques`)
  if (dryRun) console.log('  Mode DRY-RUN — aucun fichier modifié')
  if (yes) console.log('  Mode AUTO — pas de confirmation interactive')
  console.log('')

  console.log('Construction de l\'index des ingrédients connus...')
  const compendiumMap = await buildCompendiumMap()
  console.log(`  ${Object.keys(compendiumMap).length} entrées (herbes, ingrédients, plantes)`)

  const files = (await fs.readdir(RECIPES_DIR)).filter(f => f.endsWith('.yml'))
  console.log(`  ${files.length} recettes trouvées\n`)

  const results = { success: 0, skipped: 0, errors: [] }

  for (const f of files.sort()) {
    const filePath = path.join(RECIPES_DIR, f)
    try {
      const result = await processRecipe(filePath, compendiumMap, { yes, dryRun })
      if (result.modified) {
        if (result.dryRun) {
          console.log(`  [${f.padEnd(65)}] ${result.count} ingrédient(s)`)
          for (const c of result.candidates) {
            console.log(`      - ${c.quantite} ${c.unite} ${c.nom} (${c.type})`)
          }
        } else {
          console.log(`  OK ${f} → ${result.count} ingrédient(s)`)
        }
        results.success++
      } else {
        console.log(`  -- ${f} → ${result.reason}`)
        results.skipped++
      }
    } catch (e) {
      console.error(`  !! ${f} → ERREUR: ${e.message}`)
      results.errors.push({ file: f, error: e.message })
    }
  }

  console.log(`\n=== Résultat ===`)
  console.log(`  Modifiées :  ${results.success}`)
  console.log(`  Ignorées :   ${results.skipped}`)
  console.log(`  Erreurs :    ${results.errors.length}`)
  for (const e of results.errors) {
    console.log(`    • ${e.file}: ${e.error}`)
  }
}

main().catch(e => {
  console.error('FATAL:', e)
  process.exit(1)
})
