// lib/municipality.js — Résolveur de municipalités : mappe un nom de ville TEXTE LIBRE (delivery.city, tickets)
// vers la table canonique `municipalite` de F (1 185 municipalités QC : id, nom, code, administration/MRC).
// But : regrouper proprement par municipalité (les 21 orthographes de « Sainte-Clotilde » → 1 seul groupe).
// Lecture seule. Cache en mémoire. Détection campings (lieu-dit) via table d'alias extensible.
let mysql; try { mysql = require('mysql2/promise') } catch { /* optionnel */ }
const cfg = require('./config')
const { json, parseBody } = require('./helpers')

let _pool = null
function pool () { if (!mysql) return null; if (!_pool) _pool = mysql.createPool({ host: cfg.LEGACY_DB_HOST, user: cfg.LEGACY_DB_USER, password: cfg.LEGACY_DB_PASS, database: cfg.LEGACY_DB_NAME, connectionLimit: 2, waitForConnections: true }); return _pool }

// ── Normalisation ──
function decodeEntities (s) { return String(s == null ? '' : s).replace(/&#0*39;|&apos;/gi, "'").replace(/&amp;/gi, '&').replace(/&eacute;|&egrave;|&ecirc;/gi, 'e').replace(/&agrave;|&acirc;/gi, 'a').replace(/&#(\d+);/g, (_, n) => { try { return String.fromCodePoint(+n) } catch (e) { return ' ' } }) }
function deburr (s) { return decodeEntities(s).normalize('NFD').replace(/[̀-ͯ]/g, '') }
const STOP = new Set(['de', 'des', 'du', 'la', 'le', 'les', 'd', 'l', 'aux', 'au', 'sur', 'a'])
const NOISE = new Set(['camping', 'domaine', 'terrain', 'parc', 'roulotte', 'site', 'lot']) // mots-bruit (campings/lieux-dits) retirés pour le matching
function tokens (s) {
  let x = ' ' + deburr(s).toLowerCase().replace(/\(.*?\)/g, ' ').replace(/[^a-z0-9]+/g, ' ').trim() + ' '
  x = x.replace(/ qc /g, ' ').replace(/ quebec /g, ' ')
  // saint/sainte/st/ste → un seul token 'st' (corrige les erreurs Saint↔Sainte fréquentes dans les données)
  return x.split(/\s+/).filter(t => t && !STOP.has(t) && !NOISE.has(t)).map(t => (t === 'ste' || t === 'sainte' || t === 'saint' || t === 'st') ? 'st' : t)
}
function lev (a, b) { if (a === b) return 0; const m = a.length, n = b.length; if (!m || !n) return m || n; let prev = Array.from({ length: n + 1 }, (_, i) => i); for (let i = 1; i <= m; i++) { const cur = [i]; for (let j = 1; j <= n; j++) cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)); prev = cur } return prev[n] }
// un token d'entrée "matche" un token candidat : exact, préfixe (≥4), ou faute de frappe (lev≤1 ; ≤2 si long)
function tokMatch (t, toks) { for (const c of toks) { if (c === t) return true; if (t.length >= 5 && c.length >= 5 && (c.startsWith(t) || t.startsWith(c))) return true; if (lev(t, c) <= 1 && Math.min(t.length, c.length) >= 4) return true } return false } // lev≤1 SEULEMENT (≤2 conflateait Sherrington↔Harrington) ; préfixe ≥5

// ── Alias campings / hameaux → municipalité. toks (normalisés, sans bruit) ⊆ tokens d'entrée = match. muni = nom résolu via la table. EXTENSIBLE. ──
const CAMPING_ALIAS = [
  { toks: ['lac', 'pins'], muni: 'Franklin' },            // Camping Lac des Pins (secteur St-Antoine-Abbé → code FRK = Franklin)
  { toks: ['dauphinais'], muni: 'Hemmingford' },          // Camping / Domaine Dauphinais
  { toks: ['sandysun'], muni: 'Franklin' },               // Camping SandySun
  { toks: ['sandy', 'sun'], muni: 'Franklin' },
  { toks: ['rockburn'], muni: 'Hinchinbrooke' },          // hameau
  { toks: ['herdman'], muni: 'Hinchinbrooke' },           // hameau
  { toks: ['acadie'], muni: 'Saint-Jean-sur-Richelieu' }, // L'Acadie (secteur)
  { toks: ['melocheville'], muni: 'Beauharnois' },        // secteur
]

let MUNIS = null; let _byKey = null; let _loadedAt = 0
async function load (force) {
  if (MUNIS && !force && (Date.now() - _loadedAt) < 6 * 3600 * 1000) return MUNIS
  const p = pool(); if (!p) throw new Error('mysql2 indisponible')
  const [rows] = await p.query('SELECT id, nom, code, administration, often_used FROM municipalite')
  MUNIS = rows.map(r => ({ id: r.id, nom: r.nom, code: r.code, mrc: r.administration, often: r.often_used ? 1 : 0, toks: tokens(r.nom) }))
  _byKey = {}; for (const m of MUNIS) { const k = m.toks.join(' '); if (!_byKey[k] || m.often > (_byKey[k].often || 0)) _byKey[k] = m }
  _loadedAt = Date.now()
  return MUNIS
}

// Coeur : matche une liste de tokens → meilleure municipalité {m, via, score, ambiguous, candidates} ou null.
function matchMunicipality (it) {
  if (!it.length) return null
  if (_byKey[it.join(' ')]) return { m: _byKey[it.join(' ')], via: 'exact', score: 1 }
  let best = null; const cands = []
  for (const m of MUNIS) {
    let inter = 0; for (const t of it) if (tokMatch(t, m.toks)) inter++
    if (!inter) continue
    const contain = inter / it.length; const coverage = inter / m.toks.length; const extra = m.toks.length - inter
    const score = contain * 2 + coverage; const c = { m, contain, coverage, extra, score }; cands.push(c)
    if (!best || score > best.score || (score === best.score && (m.often > best.m.often || (m.often === best.m.often && (extra < best.extra || (extra === best.extra && m.id < best.m.id)))))) best = c
  }
  if (!best || best.contain < 0.6) {
    for (const t of it) { if (t.length < 4) continue; const par = MUNIS.find(mm => mm.toks.length === 1 && mm.toks[0] === t); if (par) return { m: par, via: 'parent', score: 0.9 } }
    return null
  }
  const top = cands.filter(c => c.contain >= 0.999)
  const ambiguous = top.length > 1 && top.filter(c => c.m.often === best.m.often).length > 1
  return { m: best.m, via: best.contain >= 0.999 ? 'superset' : 'fuzzy', score: Math.round(best.score * 100) / 100, ambiguous, candidates: ambiguous ? top.map(c => c.m.nom).slice(0, 5) : undefined }
}

// Résout une ville texte libre → municipalité canonique (alias campings/hameaux d'abord, puis matching).
function resolveSync (city) {
  const raw = String(city || '').trim()
  const isCamping = /\b(camping|domaine|terrain|roulotte)\b/i.test(deburr(raw))
  const it = tokens(raw)
  if (!it.length) return { input: raw, matched: false, reason: 'empty', is_camping: isCamping }
  let aliasHit = null
  for (const a of CAMPING_ALIAS) { if (a.toks.every(t => it.includes(t) || it.some(x => tokMatch(t, [x]))) && (!aliasHit || a.toks.length > aliasHit.toks.length)) aliasHit = a }
  if (aliasHit) { const r = matchMunicipality(tokens(aliasHit.muni)); if (r) return { input: raw, matched: true, via: 'alias', is_camping: isCamping, canonical: r.m.nom, code: r.m.code, id: r.m.id, mrc: r.m.mrc, score: 1, alias_to: aliasHit.muni } }
  const r = matchMunicipality(it)
  if (r) return { input: raw, matched: true, via: r.via, is_camping: isCamping, canonical: r.m.nom, code: r.m.code, id: r.m.id, mrc: r.m.mrc, score: r.score, ambiguous: r.ambiguous, candidates: r.candidates }
  return { input: raw, matched: false, is_camping: isCamping, reason: 'aucun_match' }
}
async function resolve (city) { await load(); return resolveSync(city) }

// Rapport : distinct delivery.city de F → résolus → effondrement des groupes.
async function previewDelivery (limit) {
  await load()
  const p = pool(); if (!p) throw new Error('mysql2 indisponible')
  const [rows] = await p.query('SELECT city, COUNT(*) n FROM delivery WHERE city IS NOT NULL AND city <> "" GROUP BY city')
  const groups = {}; const unmatched = []; const campings = []; const ambiguous = []
  for (const r of rows) {
    const res = resolveSync(r.city)
    if (res.matched) { const k = res.code || res.canonical; (groups[k] = groups[k] || { canonical: res.canonical, code: res.code, mrc: res.mrc, variants: [], total: 0 }); groups[k].variants.push({ raw: r.city, n: r.n, via: res.via }); groups[k].total += r.n; if (res.ambiguous) ambiguous.push({ raw: r.city, candidates: res.candidates }) }
    else if (res.is_camping) campings.push({ raw: r.city, n: r.n })
    else unmatched.push({ raw: r.city, n: r.n, near: res.near })
  }
  const collapsed = Object.values(groups).filter(g => g.variants.length > 1).sort((a, b) => b.variants.length - a.variants.length)
  return {
    raw_distinct: rows.length,
    canonical_groups: Object.keys(groups).length,
    multi_variant_groups: collapsed.length,
    unmatched: unmatched.length, campings: campings.length, ambiguous: ambiguous.length,
    top_collapses: collapsed.slice(0, 12).map(g => ({ municipalite: g.canonical, code: g.code, variants: g.variants.length, rows: g.total, examples: g.variants.sort((a, b) => b.n - a.n).slice(0, 6).map(v => v.raw + ' (' + v.n + ')') })),
    unmatched_top: unmatched.sort((a, b) => b.n - a.n).slice(0, 15),
    campings_list: campings.sort((a, b) => b.n - a.n).slice(0, 15),
    ambiguous_top: ambiguous.slice(0, 10)
  }
}

async function handle (req, res, method, path, url) {
  const qs = (url && url.searchParams) || new URLSearchParams()
  try {
    if (path === '/municipality/resolve') return json(res, 200, await resolve(qs.get('city') || ''))
    if (path === '/municipality/preview-delivery') return json(res, 200, await previewDelivery())
    if (path === '/municipality/reload') { await load(true); return json(res, 200, { ok: true, count: MUNIS.length }) }
    return json(res, 404, { ok: false, error: 'route municipality inconnue' })
  } catch (e) { return json(res, 500, { ok: false, error: String((e && e.message) || e) }) }
}

module.exports = { handle, resolve, resolveSync, previewDelivery, load, tokens }
