Bot discord Métiers pour Ashes of creation

const JOB_EMOJIS = {
  mining: "⛏️",
  woodcutting: "🪓",
  fishing: "🎣",
  herbalism: "🌿",
  hunting: "🏹",
  lumber: "🪵",
  masonry: "🧱",
  animal: "🏇",
  alchemy: "⚗️",
  metal : "⚒️",
  weaving: "🧶",
  taming: "🐔",
  farming: "🌾",
  cooking: "🍲",
  weapon: "🗡️",
  carpentry: "🪚",
  arcane: "🪄",
  armor: "🛡️",
  leather: "🐂",
  tailoring: "🪡",
  jewel: "💎",
  scribe: "📜",
};
const LEVEL_RANK = {
  apprentice: 1,
  journeyman: 2,
  expert: 3,
  master: 4,
};
function levelScore(val) {
  const k = (val || '').toLowerCase();
  for (const name in LEVEL_RANK) {
    if (k.includes(name)) return LEVEL_RANK[name];
  }
  return 0;
}
function emojiFor(jobName) {
  const key = jobName.toLowerCase();
  for (const k in JOB_EMOJIS) {
    if (key.includes(k)) return JOB_EMOJIS[k];
  }
  return "🔹";
}
import 'dotenv/config';
import { Client, GatewayIntentBits, EmbedBuilder } from 'discord.js';

const TOKEN = process.env.DISCORD_TOKEN;
const CSV_URL = process.env.SHEET_CSV_URL;
const PREFIX = process.env.PREFIX || '!';
const CACHE_SECONDS = Number(process.env.CACHE_SECONDS || 180);

if (!TOKEN || !CSV_URL) {
  console.error("❌ Il manque DISCORD_TOKEN ou SHEET_CSV_URL dans .env");
  process.exit(1);
}

// ---- Cache en mémoire ----
let cache = {
  at: 0,
  header: [],
  rows: [],
};

function nowMs() { return Date.now(); }

// CSV parser simple qui gère les guillemets
function parseCSV(text) {
  const rows = [];
  let row = [];
  let cur = '';
  let inQuotes = false;

  for (let i = 0; i < text.length; i++) {
    const c = text[i];
    const next = text[i + 1];

    if (c === '"' ) {
      if (inQuotes && next === '"') { // échappement ""
        cur += '"';
        i++;
      } else {
        inQuotes = !inQuotes;
      }
    } else if (c === ',' && !inQuotes) {
      row.push(cur);
      cur = '';
    } else if ((c === '\n' || c === '\r') && !inQuotes) {
      if (c === '\r' && next === '\n') i++;
      row.push(cur);
      cur = '';
      // ignore lignes vides
      if (row.some(cell => cell.trim() !== '')) rows.push(row);
      row = [];
    } else {
      cur += c;
    }
  }
  // dernier champ
  row.push(cur);
  if (row.some(cell => cell.trim() !== '')) rows.push(row);

  return rows;
}

async function fetchSheet(force = false) {
  const age = (nowMs() - cache.at) / 1000;
  if (!force && cache.at && age < CACHE_SECONDS) return cache;

  const res = await fetch(CSV_URL, { headers: { 'cache-control': 'no-cache' }});
  if (!res.ok) throw new Error(`HTTP ${res.status} en lisant le CSV`);

  const text = await res.text();
  const parsed = parseCSV(text);

  const header = (parsed[0] || []).map(h => (h || '').trim());
  const rows = parsed.slice(1).map(r => r.map(x => (x || '').trim()));

  cache = { at: nowMs(), header, rows };
  return cache;
}

function normalize(s) {
  return (s || '')
    .toLowerCase()
    .normalize('NFD').replace(/[\u0300-\u036f]/g, '') // retire accents
    .replace(/[^a-z0-9 ]/g, '') // retire ponctuation
    .trim();
}

function bestColumnMatch(header, query) {
  const q = normalize(query);
  // match exact d'abord
  let idx = header.findIndex(h => normalize(h) === q);
  if (idx !== -1) return idx;

  // match "contient"
  idx = header.findIndex(h => normalize(h).includes(q));
  if (idx !== -1) return idx;

  // match sur mots (ex: "metal working" vs "metal")
  const qWords = q.split(/\s+/).filter(Boolean);
  idx = header.findIndex(h => {
    const hh = normalize(h);
    return qWords.every(w => hh.includes(w));
  });
  return idx;
}

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

client.once('ready', () => {
  console.log(`✅ Connecté en tant que ${client.user.tag}`);
  console.log(`📄 CSV: ${CSV_URL}`);
});

function makeEmbed(title, description) {
  return new EmbedBuilder()
    .setTitle(title)
    .setDescription(description)
    .setColor(0x57F287); // vert sympa (tu peux changer)
}

client.on('messageCreate', async (msg) => {
  if (msg.author.bot) return;
  if (!msg.content.startsWith(PREFIX)) return;

  const content = msg.content.slice(PREFIX.length).trim();
  const [cmdRaw, ...rest] = content.split(/\s+/);
  const command = (cmdRaw || '').toLowerCase();

  // Helper: liste des métiers = colonnes sauf la première (NOM)
  function getJobs(header) {
    return header.slice(1).filter(Boolean);
  }

  // !qui <metier>
  // !liste <metier>  (alias)
  if (command === 'qui' || command === 'liste') {
    const query = rest.join(' ').trim();

    try {
      const sheet = await fetchSheet(false);
      const header = sheet.header;
      const rows = sheet.rows;

      // si "!liste" sans argument -> liste des métiers dispos
      if (command === 'liste' && !query) {
        const jobs = getJobs(header);
        const preview = jobs.slice(0, 25).join(', ');
        const extra = jobs.length > 25 ? ` … +${jobs.length - 25} autres` : '';
        const title = `${emojiFor(header[col])} ${header[col]} — ${matches.length} joueur(s)`;
		//const embed = makeEmbed(title, `${top}${extra}`);
		const embed = makeEmbed(
		`🧰 Métiers disponibles (${jobs.length})`,
		jobs.slice(0, 40).map(j => `${emojiFor(j)} ${j}`).join('\n') + (jobs.length > 40 ? `\n… +${jobs.length - 40} autres` : '')
);
await msg.channel.send({ embeds: [embed] });

        return;
      }

      if (!query) {
        await msg.channel.send(`Usage: \`${PREFIX}${command} <metier>\`  (ex: \`${PREFIX}${command} mining\`)`);
        return;
      }

      const col = bestColumnMatch(header, query);
      if (col === -1 || col === 0) {
        await msg.channel.send(`Je ne trouve pas le métier **${query}**. Essaie \`${PREFIX}liste\` pour voir les colonnes.`);
        return;
      }

      const nameCol = 0; // colonne A = NOM
      const matches = [];

      for (const r of rows) {
        const player = r[nameCol] || '';
        const value = (r[col] || '').trim();
        if (player && value) matches.push({ player, value });
      }

      if (!matches.length) {
        await msg.channel.send(`Personne trouvé pour **${header[col]}**.`);
        return;
      }

      const top = matches
        .sort((a, b) => a.player.localeCompare(b.player))
        .slice(0, 25)
        .map(x => `• ${emojiFor(header[col])} **${x.player}** — ${x.value}`)
        .join('\n');

      const extra = matches.length > 25 ? `\n… +${matches.length - 25} autres` : '';
      await msg.channel.send(`📜 **${header[col]}**:\n${top}${extra}`);
      return;

    } catch (err) {
      console.error(err);
      await msg.channel.send(`💥 Problème lecture sheet. Vérifie le lien CSV et réessaie.`);
      return;
    }
  }

  // !metiers <Nom>
  if (command === 'metiers') {
    const who = rest.join(' ').trim();
    if (!who) {
      await msg.channel.send(`Usage: \`${PREFIX}metiers <Nom>\` (ex: \`${PREFIX}metiers Skurcey\`)`);
      return;
    }

    try {
      const sheet = await fetchSheet(false);
      const header = sheet.header;
      const rows = sheet.rows;

      const nameCol = 0;
      const target = normalize(who);

      const row = rows.find(r => normalize(r[nameCol] || '') === target);

      if (!row) {
        const embed = makeEmbed(
		`🧑‍🏭 Métiers de ${row[nameCol]}`,
		`${shown}${extra}`
);
await msg.channel.send({ embeds: [embed] });

        return;
      }

      // Construire liste des métiers non vides
      const items = [];
      for (let i = 1; i < header.length; i++) {
        const job = header[i];
        const val = (row[i] || '').trim();
        if (job && val) items.push(`• ${emojiFor(job)} **${job}** — ${val}`);
      }

      if (!items.length) {
        await msg.channel.send(`**${who}** n’a aucun métier rempli (ou ligne vide).`);
        return;
      }

      const shown = items.slice(0, 40).join('\n');
      const extra = items.length > 40 ? `\n… +${items.length - 40} autres` : '';
      await msg.channel.send(`🧑‍🏭 **Métiers de ${row[nameCol]}**:\n${shown}${extra}`);
      return;

    } catch (err) {
      console.error(err);
      await msg.channel.send(`💥 Problème lecture sheet. Vérifie le lien CSV et réessaie.`);
      return;
    }
  }

  // !refresh
  if (command === 'refresh') {
    try {
      await fetchSheet(true);
      await msg.channel.send(`✅ Cache rafraîchi.`);
    } catch (err) {
      console.error(err);
      await msg.channel.send(`💥 Impossible de rafraîchir. Vérifie le lien CSV.`);
    }
  }
  // !top <metier> [n]
if (command === 'top') {
  const query = rest.join(' ').trim();
  if (!query) {
    await msg.channel.send(`Usage: \`${PREFIX}top <metier> [nombre]\``);
    return;
  }

  // séparer le métier et le nombre (ex: "mining 10")
  const parts = query.split(/\s+/);
  const maybeNum = Number(parts[parts.length - 1]);
  const limit = Number.isInteger(maybeNum) ? Math.min(Math.max(maybeNum, 1), 25) : 10;
  const jobQuery = Number.isInteger(maybeNum) ? parts.slice(0, -1).join(' ') : parts.join(' ');

  try {
    const sheet = await fetchSheet(false);
    const header = sheet.header;
    const rows = sheet.rows;

    const col = bestColumnMatch(header, jobQuery);
    if (col === -1 || col === 0) {
      await msg.channel.send(`Métier **${jobQuery}** introuvable. Essaie \`${PREFIX}liste\`.`);
      return;
    }

    const nameCol = 0;
    const ranked = [];

    for (const r of rows) {
      const player = r[nameCol] || '';
      const val = (r[col] || '').trim();
      const score = levelScore(val);
      if (player && score > 0) {
        ranked.push({ player, val, score });
      }
    }

    if (!ranked.length) {
      await msg.channel.send(`Aucun résultat pour **${header[col]}**.`);
      return;
    }

    ranked.sort((a, b) => b.score - a.score || a.player.localeCompare(b.player));

    const lines = ranked.slice(0, limit).map((x, i) =>
      `**${i + 1}.** ${emojiFor(header[col])} **${x.player}** — ${x.val}`
    ).join('\n');

    const embed = makeEmbed(
      `🏆 Top ${header[col]} (Top ${Math.min(limit, ranked.length)})`,
      lines
    );

    await msg.channel.send({ embeds: [embed] });

  } catch (err) {
    console.error(err);
    await msg.channel.send(`💥 Impossible de calculer le top.`);
  }
}

});

client.login(TOKEN);