import { makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } from '@whiskeysockets/baileys';
import http from 'http';
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import fetch from 'node-fetch';
import qrcode from 'qrcode-terminal';
import { config } from 'dotenv';
import { loadClients as loadClientsFromAPI, updateMentee, getCareerStrategy } from './lib/viaweb.js';
import { getDocMsg, downloadDocument, extractText } from './lib/documents.js';
import { saveCvFile, saveCvText, hasCv, generateAndSendReport } from './lib/report.js';
import { syncCreate, syncWrite, syncWriteCv, startSyncLoop } from './lib/sync.js';
import { handleGroupMsg } from './lib/groupHandler.js';
import { startJobScheduler, sendJobsToGroup } from './lib/groupJobs.js';
import { search, triage, toCodes, searchParamsFromStrategy } from './lib/jobSearch.js';
config({ path: new URL('.env', import.meta.url).pathname });

const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));  // ecobot/
const BOT_DIR    = process.cwd();                              // bots/351923199040/

const TRAINING = readFileSync(join(SCRIPT_DIR, 'training', 'jobsearch.txt'), 'utf8').trim();

// ── Authorized numbers ─────────────────────────────────────────────────────────

let NUMBERS = [];
try {
  NUMBERS = JSON.parse(readFileSync(join(SCRIPT_DIR, 'json', 'numbers.json'), 'utf8'));
} catch { console.error('[numbers] failed to load numbers.json'); }

function getUser(chatId) {
  const num = chatId.replace(/@.*/, '');
  return NUMBERS.find(n => n.number === num) || null;
}

function isAuthorized(chatId) {
  return getUser(chatId) !== null;
}

function normalizeUrl(u) { try { return decodeURIComponent(u); } catch { return u; } }

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

// ── Client profiles index ──────────────────────────────────────────────────────

let CLIENTS = [];

function findClient(query) {
  const q = query.toLowerCase();
  return CLIENTS.find(c => c.name?.toLowerCase().includes(q) || c.slug.includes(q));
}

function loadLocalCareerStrategy(slug) {
  const p = join(SCRIPT_DIR, 'mentees', slug, 'career-strategy.json');
  if (!existsSync(p)) return null;
  try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; }
}

// ── History ────────────────────────────────────────────────────────────────────

function histFile(id) {
  const dir = join(BOT_DIR, 'chats', id);
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  return join(dir, 'messages.json');
}

function loadHistory(id, limit = 20) {
  try { return JSON.parse(readFileSync(histFile(id), 'utf8')).slice(-limit); }
  catch { return []; }
}

function saveMsg(id, role, text) {
  const f = histFile(id);
  const msgs = existsSync(f) ? JSON.parse(readFileSync(f, 'utf8')) : [];
  msgs.push({ role, text, ts: Date.now() });
  writeFileSync(f, JSON.stringify(msgs, null, 2));
}

// ── AI ─────────────────────────────────────────────────────────────────────────
// Anthropic Messages API (replaces OpenAI Chat Completions).
// Same (system, messages, model) signature so existing callers keep working.
// Maps OpenAI model strings to Claude equivalents for back-compat.

const MODEL_MAP = {
  'gpt-4.1':      'claude-sonnet-4-5',
  'gpt-4.1-mini': 'claude-haiku-4-5',
  'gpt-4o':       'claude-sonnet-4-5',
  'gpt-4o-mini':  'claude-haiku-4-5',
};

async function ai(system, messages, model = 'gpt-4.1-mini') {
  const claudeModel = model.startsWith('claude-') ? model : (MODEL_MAP[model] || 'claude-haiku-4-5');
  // Anthropic requires user/assistant only — drop any inline system messages.
  const cleanMessages = (messages || [])
    .filter(m => m.role === 'user' || m.role === 'assistant')
    .map(m => ({ role: m.role, content: m.content }));
  const res = await fetch('https://api.anthropic.com/v1/messages', {
    method: 'POST',
    headers: {
      'x-api-key': process.env.ANTHROPIC_API_KEY,
      'anthropic-version': '2023-06-01',
      'content-type': 'application/json',
    },
    body: JSON.stringify({
      model: claudeModel,
      max_tokens: 3000,
      system: system || '',
      messages: cleanMessages,
    }),
    signal: AbortSignal.timeout(60000),
  });
  const data = await res.json();
  if (data.error || data.type === 'error') {
    console.error('[ai]', data.error?.message || JSON.stringify(data));
    return '';
  }
  // Concatenate any text blocks the model returned.
  const text = (data.content || [])
    .filter(b => b.type === 'text')
    .map(b => b.text)
    .join('')
    .trim();
  return text;
}

// ── Job Search (imported from lib/jobSearch.js) ───────────────────────────────
// search(), triage(), toCodes() are imported at the top of this file.
// Wrap them to inject the local `ai` function automatically.
const _search = (titles, location, maxAgeDays = 30, locationFallback = null) => search(titles, location, ai, maxAgeDays, locationFallback);
const _triage = (jobs, msg, location, extra = '', priorityCities = null) => triage(jobs, msg, location, ai, extra, priorityCities);

// ── Dispatch commands ──────────────────────────────────────────────────────────

async function dispatch(reply, session, sock, chatId, originalMsg) {
  // UPDATEDATA("slug", {...}) — update an existing mentee's career-strategy
  const mUpdate = reply.match(/UPDATEDATA\("([^"]+)",\s*(\{[\s\S]*?\})\)/i);
  if (mUpdate) {
    const rawSlug = mUpdate[1];
    let payload;
    try { payload = JSON.parse(mUpdate[2]); } catch { return 'Error: invalid data.'; }
    try {
      // Resolve the slug via findClient so partial names (e.g. "carla" → "carla-naagel_111")
      // always write directly to the real profile — never create a new folder.
      const client = findClient(rawSlug);
      const slug = client ? client.slug : rawSlug;
      if (client && slug !== rawSlug) {
        console.log(`[update] resolved "${rawSlug}" → "${slug}" (from client: ${client.name})`);
      }
      // Load existing data and merge — never overwrite the whole document
      const existing = loadLocalCareerStrategy(slug) || {};
      const merged = { ...existing, ...payload };
      // For exclusions, combine rather than replace
      if (existing.exclusions && payload.exclusions) {
        merged.exclusions = [...new Set([...existing.exclusions, ...payload.exclusions])];
      }
      await syncWrite(SCRIPT_DIR, slug, 'career-strategy', merged);
      CLIENTS = await loadClientsFromAPI();
    } catch (e) {
      console.error('[update]', e.message);
      return `Error updating profile: ${e.message}`;
    }
    return null;
  }

  // GENERATEREPORT() — trigger report generation for the current pending lead
  const mGenReport = /GENERATEREPORT\(\)/i.test(reply);
  if (mGenReport) {
    const lead = session.pendingLead;
    if (lead) {
      console.log(`[report] generating for ${lead.leadName} (${lead.slug})`);
      generateAndSendReport(sock, chatId, SCRIPT_DIR, lead).catch(e => console.error('[report]', e.message));
    } else {
      console.warn('[report] GENERATEREPORT emitted but no pendingLead in session');
    }
    return null; // let surrounding AI message through
  }

  // GENERATEREPORTFOR("name") — look up client by name and generate report (manual trigger)
  const mGenReportFor = reply.match(/GENERATEREPORTFOR\("([^"]+)"\)/i);
  if (mGenReportFor) {
    CLIENTS = await loadClientsFromAPI();
    const client = findClient(mGenReportFor[1]);
    if (!client) return `Client "${mGenReportFor[1]}" not found.`;
    const cs = loadLocalCareerStrategy(client.slug) || client.profile || {};
    const lead = {
      leadId:      cs.id || client.slug,
      leadName:    cs.leadName || client.name,
      slug:        client.slug,
      email:       cs.email || '',
      phone:       cs.phone || '',
      description: cs.shortDescription || '',
    };
    console.log(`[report] manual trigger for ${lead.leadName} (${lead.slug})`);
    generateAndSendReport(sock, chatId, SCRIPT_DIR, lead).catch(e => console.error('[report]', e.message));
    return null;
  }

  // LEADDATA({...}) — AI has collected all fields, create the mentee
  // NOTE: LEADDATA must be checked BEFORE SAVECV — the AI often emits both in the same
  // response, and SAVECV needs pendingLead (set here) to be populated first.
  const mLead = reply.match(/LEADDATA\((\{[\s\S]*?\})\)/i);
  if (mLead) {
    let payload;
    try { payload = JSON.parse(mLead[1]); } catch { return 'Error: invalid lead data.'; }
    if (!payload.leadName) return 'Error: leadName is required.';
    const id        = String(Math.floor(1000000000 + Math.random() * 9000000000));
    const slug      = toSlug(payload.leadName);
    const folderKey = `${slug}_${id}`;
    payload = { id, odooIdPending: true, ...payload, leadSlug: slug, createdAt: new Date().toISOString() };
    try {
      await syncCreate(SCRIPT_DIR, folderKey, payload);
      CLIENTS = await loadClientsFromAPI();
    } catch (e) {
      console.error('[lead]', e.message);
      return `Erro ao criar perfil: ${e.message}`;
    }
    // Save lead info in session so SAVECV knows which slug to use
    session.pendingLead = {
      leadId:      id,
      leadName:    payload.leadName,
      slug:        folderKey,
      email:       payload.email       || '',
      phone:       payload.phone       || '',
      description: payload.shortDescription || '',
    };

    // If the AI also emitted SAVECV in the same response, process it now.
    // If the AI forgot to emit SAVECV but a document is pending, auto-save as personal CV
    // and override the AI's reply so the user gets the correct next-step message.
    const mSaveCvInLead = reply.match(/SAVECV\("(personal|linkedin)"\)/i);
    if (session.pendingUpload) {
      const cvType = mSaveCvInLead
        ? (mSaveCvInLead[1].toLowerCase() === 'personal' ? 'cv-personal' : 'cv-linkedin')
        : 'cv-personal';
      const { buffer, ext, extracted } = session.pendingUpload;
      saveCvFile(SCRIPT_DIR, folderKey, cvType, ext, buffer);
      if (extracted) saveCvText(SCRIPT_DIR, folderKey, cvType, extracted);
      syncWriteCv(SCRIPT_DIR, folderKey, cvType, ext, buffer).catch(e => console.warn('[cv-sync]', e.message));
      session.pendingUpload = null;
      console.log(`[cv] saved ${cvType} for ${payload.leadName} (${folderKey}) alongside LEADDATA${mSaveCvInLead ? '' : ' (auto)'}`);
      if (cvType === 'cv-personal') {
        session.awaitingLinkedinCv = session.pendingLead;
        // If the AI forgot to emit SAVECV, its message won't mention the CV was saved
        // or ask about LinkedIn — override it with a corrected AI response.
        if (!mSaveCvInLead) {
          const hist = loadHistory(chatId, 10);
          const msgs = hist.slice(0, -1).map(m => ({ role: m.role, content: m.text }));
          msgs.push({ role: 'system', content: `[SYSTEM EVENT: Profile for ${payload.leadName} has been created and their personal CV has been saved. Now confirm this to the coach and ask if the client also has a LinkedIn CV PDF to send.]` });
          const corrected = await ai(TRAINING, msgs);
          return corrected;
        }
      } else {
        session.awaitingLinkedinCv = null;
        generateAndSendReport(sock, chatId, SCRIPT_DIR, session.pendingLead).catch(e => console.error('[report]', e.message));
      }
    }

    return null; // let the AI's surrounding message through
  }

  // SAVECV("personal"|"linkedin") — save pending document buffer as a CV file
  // Runs AFTER LEADDATA so pendingLead is always populated when both are in the same response.
  const mSaveCv = reply.match(/SAVECV\("(personal|linkedin)"\)/i);
  if (mSaveCv) {
    const cvType = mSaveCv[1].toLowerCase() === 'personal' ? 'cv-personal' : 'cv-linkedin';
    // Recovery: if pendingLead was lost (e.g. server restart), find the client from history
    if (!session.pendingLead && session.pendingUpload) {
      if (!CLIENTS.length) CLIENTS = await loadClientsFromAPI();
      const recentHistory = loadHistory(chatId, 10);
      const recentText = recentHistory.map(m => m.text).join(' ').toLowerCase();
      const matchedClient = CLIENTS.find(c => c.name && recentText.includes(c.name.toLowerCase()));
      if (matchedClient) {
        const cs = matchedClient.profile || loadLocalCareerStrategy(matchedClient.slug) || {};
        session.pendingLead = {
          leadId:      cs.id || matchedClient.slug,
          leadName:    cs.leadName || matchedClient.name,
          slug:        matchedClient.slug,
          email:       cs.email || '',
          phone:       cs.phone || '',
          description: cs.shortDescription || '',
        };
        console.log(`[cv] recovered pendingLead for ${session.pendingLead.leadName} from history`);
      }
    }
    const lead   = session.pendingLead;
    if (session.pendingUpload && lead) {
      const { buffer, ext, extracted } = session.pendingUpload;
      saveCvFile(SCRIPT_DIR, lead.slug, cvType, ext, buffer);
      if (extracted) saveCvText(SCRIPT_DIR, lead.slug, cvType, extracted);
      syncWriteCv(SCRIPT_DIR, lead.slug, cvType, ext, buffer).catch(e => console.warn('[cv-sync]', e.message));
      session.pendingUpload = null;
      console.log(`[cv] saved ${cvType} for ${lead.leadName} (${lead.slug})`);

      if (cvType === 'cv-personal') {
        session.awaitingLinkedinCv = lead;
      } else {
        session.awaitingLinkedinCv = null;
        generateAndSendReport(sock, chatId, SCRIPT_DIR, lead).catch(e => console.error('[report]', e.message));
      }
    } else {
      console.warn('[cv] SAVECV emitted but no pendingUpload or pendingLead');
    }
    return null; // let surrounding AI message through
  }

  // CLIENTSEARCH("name") — search jobs for a specific client using their profile
  const mClient = reply.match(/CLIENTSEARCH\("([^"]+)"\)/i);
  if (mClient) {
    CLIENTS = await loadClientsFromAPI(); // refresh from API
    const client = findClient(mClient[1]);
    if (!client) return `Client "${mClient[1]}" not found. Available: ${CLIENTS.map(c => c.name).join(', ')}.`;
    // API is the primary source of truth; local file is fallback if API is unavailable
    const profile = client.profile || loadLocalCareerStrategy(client.slug);
    const { roles, location, locationFallback, exclusions, priorityCities } = searchParamsFromStrategy(profile);
    if (!roles.length) return `No target roles defined in ${client.name}'s profile.`;
    await sock.sendMessage(chatId, { text: `🔍 ${client.name} → ${roles.slice(0,3).join(' | ')} · ${priorityCities?.join(', ') || location}` });
    const rawJobs = await _search(roles, location, 30, locationFallback);
    if (!rawJobs.length) return `No results found for ${client.name} (${roles.join(', ')}) in ${location}. Want to adjust criteria?`;
    // Pre-filter: hard reject jobs whose title contains an exclusion keyword (case-insensitive)
    const exclLower = exclusions.map(e => e.toLowerCase());
    const jobs = exclLower.length
      ? rawJobs.filter(j => !exclLower.some(kw => j.title.toLowerCase().includes(kw)))
      : rawJobs;
    if (!jobs.length) return `All results for ${client.name} were filtered by exclusion rules. The market may be thin right now — want me to search with broader criteria?`;
    session.lastJobs = jobs;
    session.lastClient = client;
    jobs.forEach(j => { if (j.url) session.shownUrls.add(normalizeUrl(j.url)); });
    const seniority = profile.seniority || null;
    const exclusionParts = [
      exclusions.length ? `\n- EXCLUDE roles matching any of these client exclusions: ${exclusions.join(', ')}. This is ABSOLUTE — never include a role whose title matches any exclusion, even if results are scarce.` : '',
      seniority ? `\n- SENIORITY: This client targets "${seniority}" level. EXCLUDE any job clearly below this level (e.g., Junior, Trainee, Intern, Analyst, Assistant) unless the target role itself is at that level.` : '',
    ].filter(Boolean).join('');
    return _triage(jobs, originalMsg, location, exclusionParts, priorityCities);
  }

  // MULTISEARCH(["r1","r2"],"location") or ...,"7d"/"24h")
  const mMulti = reply.match(/MULTISEARCH\((\[[\s\S]*?\]),"([^"]+)"(?:,"([^"]*)")?\)/i);
  if (mMulti) {
    let roles;
    try { roles = JSON.parse(mMulti[1]); } catch { roles = []; }
    roles = roles.filter(r => typeof r === 'string' && r.trim());
    if (!roles.length) return 'No roles specified.';
    const location = mMulti[2];
    const maxAgeDays = mMulti[3] === '24h' ? 1 : mMulti[3] === '7d' ? 7 : 30;
    await sock.sendMessage(chatId, { text: `🔍 ${roles.join(' | ')} · ${location}` });
    const jobs = await _search(roles, location, maxAgeDays);
    if (!jobs.length) return await ai(TRAINING, [{ role: 'user', content: originalMsg }, { role: 'system', content: `No jobs found for those roles in "${location}". Inform the user in their language and ask if they want to adjust criteria or try a different location.` }]);
    session.lastJobs = jobs;
    jobs.forEach(j => { if (j.url) session.shownUrls.add(normalizeUrl(j.url)); });
    return _triage(jobs, originalMsg, location);
  }

  // SEARCHJOBS("title","location") or ...,"7d"/"24h")
  const mSearch = reply.match(/SEARCHJOBS\("([^"]+)","([^"]+)"(?:,"([^"]*)")?\)/i);
  if (mSearch) {
    const role = mSearch[1], location = mSearch[2];
    const maxAgeDays = mSearch[3] === '24h' ? 1 : mSearch[3] === '7d' ? 7 : 30;
    await sock.sendMessage(chatId, { text: `🔍 ${role} · ${location}` });
    const jobs = await _search(role, location, maxAgeDays);
    if (!jobs.length) return await ai(TRAINING, [{ role: 'user', content: originalMsg }, { role: 'system', content: `No jobs found for "${role}" in "${location}". Inform the user in their language and ask if they want to adjust criteria or try a different location.` }]);
    session.lastJobs = jobs;
    jobs.forEach(j => { if (j.url) session.shownUrls.add(normalizeUrl(j.url)); });
    return _triage(jobs, originalMsg, location);
  }

  // SENDTOGROUP("name") — send last searched jobs to a client's WhatsApp group
  const mSend = reply.match(/SENDTOGROUP\("([^"]+)"\)/i);
  if (mSend) {
    const client = session.lastClient || findClient(mSend[1]);
    if (!client) return `No recent client search found. Run CLIENTSEARCH("name") first.`;
    const jobs = session.lastJobs || [];
    if (!jobs.length) return `No jobs in the current session to send. Run a search first.`;

    const groupPath = join(SCRIPT_DIR, 'mentees', client.slug, 'group.json');
    if (!existsSync(groupPath)) return `No group found for ${client.name}.`;
    let group;
    try { group = JSON.parse(readFileSync(groupPath, 'utf8')); } catch { return `Could not read group data for ${client.name}.`; }
    if (group.phase !== 'active') return `${client.name}'s group is not in active phase (current: ${group.phase || 'unknown'}).`;

    try {
      const count = await sendJobsToGroup(sock, SCRIPT_DIR, BOT_DIR, client.slug, group, jobs, ai);
      return `✅ Sent ${count} job(s) to ${client.name}'s group.`;
    } catch (e) {
      console.error('[sendtogroup]', e.message);
      return `⚠️ Failed to send to group: ${e.message}`;
    }
  }

  // CAREERSTRATEGY("name") — show a client's full career strategy
  const mCS = reply.match(/CAREERSTRATEGY\("([^"]+)"\)/i);
  if (mCS) {
    CLIENTS = await loadClientsFromAPI();
    const client = findClient(mCS[1]);
    if (!client) return `Client "${mCS[1]}" not found. Available: ${CLIENTS.map(c => c.name).join(', ')}.`;
    let cs;
    try { cs = await getCareerStrategy(client.slug); } catch { cs = client.profile; }
    return ai(
      `You are presenting a client's career strategy to their coach on WhatsApp. Format the data below as a clear, well-structured message using sections and bullet points. Be complete — include every non-empty field. Do not invent data.\n\nLANGUAGE RULE (NON-NEGOTIABLE): The coach's message is: "${originalMsg}". Detect the language of that message and reply ONLY in that language. This overrides the language of the data below. If the coach wrote in English → reply in English. If in Portuguese → reply in Portuguese. No exceptions.`,
      [{ role: 'user', content: JSON.stringify(cs, null, 2) }],
      'gpt-4.1-mini'
    );
  }

  return null;
}

// ── Sessions ───────────────────────────────────────────────────────────────────

const sessions = {};
function getSession(id) {
  if (!sessions[id]) sessions[id] = {
    lastJobs: [], shownUrls: new Set(), processing: false,
    pendingUpload:       null,  // { buffer, ext, filename, extracted } — held until AI emits SAVECV
    pendingLead:         null,  // { leadId, leadName, slug } — set by LEADDATA dispatch
    awaitingLinkedinCv:  null,  // lead object — set after SAVECV("personal"), cleared on doc/no
  };
  return sessions[id];
}

// ── Message handler ────────────────────────────────────────────────────────────

// ── Silver coach commands ─────────────────────────────────────────────────────
async function handleSilverCoachCommand(text) {
  const args = text.trim().split(/\s+/).slice(1); // drop "/silver"
  if (args.length === 0 || args[0] === 'help') {
    return [
      'Silver commands:',
      '  /silver <slug> [start=YYYY-MM-DD] [email=foo@bar.com]',
      '     → flip a mentee to Silver. start defaults to today; email is the Hotmart buyer email.',
      '  /silver list',
      '     → list all silver mentees with current day + completion %.',
      '  /silver status <slug>',
      '     → show progress detail for one mentee.',
    ].join('\n');
  }

  const fs = await import('fs');
  const path = await import('path');
  const menteesRoot = path.join(SCRIPT_DIR, 'mentees');

  if (args[0] === 'list') {
    if (!fs.existsSync(menteesRoot)) return '(no mentees)';
    const slugs = fs.readdirSync(menteesRoot);
    const lines = ['Silver mentees:'];
    for (const slug of slugs) {
      const sp = path.join(menteesRoot, slug, 'silver.json');
      if (!fs.existsSync(sp)) continue;
      const s = JSON.parse(fs.readFileSync(sp, 'utf8'));
      const total = 30;
      const done = Object.values(s.lessons || {}).filter(l => l.completedAt).length;
      lines.push(`  • ${slug} — ${s.leadName || ''} · day ${s.currentDay}/30 · ${done}/${total} watched · started ${s.startedAt?.slice(0,10)}`);
    }
    return lines.length === 1 ? '(no silver mentees yet)' : lines.join('\n');
  }

  if (args[0] === 'status') {
    const slug = args[1];
    if (!slug) return 'Usage: /silver status <slug>';
    const sp = path.join(menteesRoot, slug, 'silver.json');
    if (!fs.existsSync(sp)) return `No silver.json for ${slug}.`;
    const s = JSON.parse(fs.readFileSync(sp, 'utf8'));
    const done = Object.entries(s.lessons || {}).filter(([_,l]) => l.completedAt).map(([k]) => k);
    const tasks = Object.values(s.tasks || {});
    return [
      `${slug} — ${s.leadName || ''}`,
      `started: ${s.startedAt}`,
      `current day: ${s.currentDay}/30`,
      `completed lessons: ${done.length} (${done.join(', ') || 'none'})`,
      `awaiting yes for: ${s.awaitingYesFor || '—'}`,
      `pending task ask: ${s.pendingTaskAsk || '—'}`,
      `tasks logged: ${tasks.length}`,
      ...tasks.map(t => `  · ${t.taskId}: ${t.status}${t.proofUrls?.length ? ' [' + t.proofUrls.length + ' proof]' : ''}`),
      `escalations: ${(s.escalations||[]).length}`,
    ].join('\n');
  }

  // /silver <slug> [start=…] [email=…]
  const slug = args[0];
  const opts = {};
  for (const a of args.slice(1)) {
    const [k, v] = a.split('=');
    if (k && v) opts[k] = v;
  }
  const groupJsonPath = path.join(menteesRoot, slug, 'group.json');
  if (!fs.existsSync(groupJsonPath)) return `No mentee folder for ${slug}. Onboard them first via the existing flow.`;
  const group = JSON.parse(fs.readFileSync(groupJsonPath, 'utf8'));
  group.tier = 'silver';
  group.phase = 'silver_active';
  fs.writeFileSync(groupJsonPath, JSON.stringify(group, null, 2));

  const { startSilver } = await import('./lib/silverCourse.js');
  const state = await startSilver(SCRIPT_DIR, slug, {
    groupJid: group.groupJid,
    clientPhone: group.clientPhone,
    leadName: group.leadName,
    startDate: opts.start ? `${opts.start}T08:00:00+02:00` : undefined,
    email: opts.email || undefined,
  });
  return `✅ ${slug} flipped to Silver.\n  start: ${state.startedAt}\n  email: ${state.email || '(not set)'}\n  group: ${state.groupJid}`;
}

async function handleMsg(sock, msg) {
  const chatId = msg.key.remoteJid;

  // Group messages → group handler (onboarding, form, active phase)
  if (chatId.endsWith('@g.us')) {
    const botPhone = sock.user?.id?.replace(/:.*/, '').replace(/@.*/, '') || '';
    const botLid   = sock.user?.lid ? String(sock.user.lid).replace(/:.*/, '').replace(/@.*/, '') : '';
    await handleGroupMsg(sock, msg, SCRIPT_DIR, SCRIPT_DIR, NUMBERS, ai, botPhone, botLid);
    return;
  }

  if (!isAuthorized(chatId)) return;

  const session = getSession(chatId);
  if (session.processing) return;

  // ── Document received — extract text and treat as a regular message ───────────
  const docMsg = getDocMsg(msg);
  let text = msg.message?.conversation || msg.message?.extendedTextMessage?.text || '';

  // ── Coach slash-commands (Silver) ─────────────────────────────────────────
  // /silver <slug> [start=YYYY-MM-DD] [email=foo@bar]
  //   Flips an existing mentee to tier="silver" and starts the 30-day flow.
  // /silver list
  //   Show all silver mentees with progress.
  if (text && text.startsWith('/silver')) {
    try {
      const reply = await handleSilverCoachCommand(text);
      await sock.sendMessage(chatId, { text: reply });
    } catch (e) {
      await sock.sendMessage(chatId, { text: `silver command failed: ${e.message}` });
    }
    return;
  }

  if (docMsg) {
    try {
      const { buffer, ext } = await downloadDocument(docMsg);
      const extracted = await extractText(buffer, ext);
      const filename  = docMsg.fileName || `document.${ext}`;

      // Recovery: if awaitingLinkedinCv was lost (e.g. server restart), infer from history
      if (!session.awaitingLinkedinCv) {
        const recentHistory = loadHistory(chatId, 6);
        const lastBotMsg = [...recentHistory].reverse().find(m => m.role === 'assistant');
        if (lastBotMsg && /linkedin/i.test(lastBotMsg.text)) {
          if (!CLIENTS.length) CLIENTS = await loadClientsFromAPI();
          const recentText = recentHistory.map(m => m.text).join(' ').toLowerCase();
          const matchedClient = CLIENTS.find(c => c.name && recentText.includes(c.name.toLowerCase()));
          if (matchedClient) {
            const cs = matchedClient.profile || loadLocalCareerStrategy(matchedClient.slug) || {};
            session.awaitingLinkedinCv = {
              leadId:      cs.id || matchedClient.slug,
              leadName:    cs.leadName || matchedClient.name,
              slug:        matchedClient.slug,
              email:       cs.email || '',
              phone:       cs.phone || '',
              description: cs.shortDescription || '',
            };
            console.log(`[cv] recovered awaitingLinkedinCv for ${session.awaitingLinkedinCv.leadName} from history`);
          }
        }
      }

      // If we're waiting for a LinkedIn CV, auto-classify and save immediately
      if (session.awaitingLinkedinCv) {
        const lead = session.awaitingLinkedinCv;
        session.awaitingLinkedinCv = null;
        saveCvFile(SCRIPT_DIR, lead.slug, 'cv-linkedin', ext, buffer);
        if (extracted) saveCvText(SCRIPT_DIR, lead.slug, 'cv-linkedin', extracted);
        syncWriteCv(SCRIPT_DIR, lead.slug, 'cv-linkedin', ext, buffer).catch(e => console.warn('[cv-sync]', e.message));
        console.log(`[cv] saved cv-linkedin for ${lead.leadName} (${lead.slug})`);
        session.processing = true;
        saveMsg(chatId, 'user', `[Document: ${filename}]`);
        try {
          const history = loadHistory(chatId, 10);
          const messages = history.slice(0, -1).map(m => ({ role: m.role, content: m.text }));
          messages.push({ role: 'system', content: `[SYSTEM EVENT: ${lead.leadName}'s LinkedIn CV has been saved. Report generation is now starting with both CVs. Confirm briefly in the user's language.]` });
          await sock.sendPresenceUpdate('composing', chatId);
          const r = await ai(TRAINING, messages);
          saveMsg(chatId, 'assistant', r);
          await sock.sendMessage(chatId, { text: r });
          generateAndSendReport(sock, chatId, SCRIPT_DIR, lead).catch(e => console.error('[report]', e.message));
        } finally {
          session.processing = false;
        }
        return;
      }

      // Store buffer so the AI can emit SAVECV("personal"|"linkedin") later
      session.pendingUpload = { buffer, ext, filename, extracted };

      // Inject the document content as the incoming text — the AI reads it directly
      const content = extracted || '(could not extract text from this file)';
      text = `[Document: ${filename}]\n\n${content.slice(0, 4000)}`;
    } catch (e) {
      console.error('[doc]', e.message);
      return;
    }
  }

  if (!text.trim()) return;

  // ── Awaiting LinkedIn CV response (set after SAVECV("personal") is processed) ─
  if (session.awaitingLinkedinCv && !docMsg) {
    const isNo = /\b(no|não|nao|nope|n\/a|pas de|nein|não tem|nao tem|sem linkedin|without|none)\b/i.test(text);
    if (isNo) {
      const lead = session.awaitingLinkedinCv;
      session.awaitingLinkedinCv = null;
      session.processing = true;
      saveMsg(chatId, 'user', text);
      try {
        const history = loadHistory(chatId, 10);
        const messages = history.slice(0, -1).map(m => ({ role: m.role, content: m.text }));
        messages.push({ role: 'user', content: text });
        messages.push({ role: 'system', content: `[SYSTEM EVENT: Coach confirmed no LinkedIn CV for ${lead.leadName}. Report generation is now starting with the Personal CV only. Confirm briefly in the user's language.]` });
        await sock.sendPresenceUpdate('composing', chatId);
        const r = await ai(TRAINING, messages);
        saveMsg(chatId, 'assistant', r);
        await sock.sendMessage(chatId, { text: r });
        generateAndSendReport(sock, chatId, SCRIPT_DIR, lead).catch(e => console.error('[report]', e.message));
      } finally {
        session.processing = false;
      }
      return;
    }
    // Not a clear "no" — let normal AI flow handle (will re-ask naturally)
  }

  session.processing = true;
  saveMsg(chatId, 'user', text);

  const history = loadHistory(chatId, 50);
  const messages = history.slice(0, -1).map(m => ({ role: m.role, content: m.text }));

  // Inject last client context ONLY for refinement requests — not for new client lookups
  // This prevents cross-contamination where Jelena's roles bleed into Felipe's search
  if (session.lastClient) {
    const freshProfile = loadLocalCareerStrategy(session.lastClient.slug) || session.lastClient.profile;
    const { roles, location } = searchParamsFromStrategy(freshProfile);
    messages.push({
      role: 'system',
      content: `[Last client search context — USE ONLY when refining/adjusting this same client's search] Client: ${session.lastClient.name} | Roles: ${roles.join(', ')} | Location: ${location}. IMPORTANT: If the user names a DIFFERENT client, use CLIENTSEARCH("other name") and IGNORE this context entirely.`,
    });
  }

  messages.push({ role: 'user', content: text });

  // ── Lead confirmation nudge ───────────────────────────────────────────────
  // When the last AI message was a lead summary ("Confirm?") and the user just
  // confirmed, inject a forcing instruction so the AI always emits LEADDATA.
  {
    const lastAI = history.slice().reverse().find(m => m.role === 'assistant');
    const isLeadSummary = lastAI &&
      /confirm\?/i.test(lastAI.text) &&
      /\bName:/i.test(lastAI.text) &&
      /\bEmail:/i.test(lastAI.text) &&
      /\bPhone:/i.test(lastAI.text);
    const isAffirmation = /^\s*(yes|yeah|yep|confirm|ok|ok\.|sure|correct|go ahead|sim|oui|ja|si|yup|absolutely)\s*[.!]?\s*$/i.test(text);
    if (isLeadSummary && isAffirmation) {
      messages.push({
        role: 'system',
        content: `[SYSTEM] The user confirmed the profile creation. You MUST emit LEADDATA({...}) RIGHT NOW with the exact name/email/phone/shortDescription from your previous message. Place it on its own line. Do NOT ask for a CV before emitting LEADDATA.`,
      });
    }
  }

  await sock.sendPresenceUpdate('composing', chatId);

  let reply;
  try {
    reply = await ai(TRAINING, messages);
  } catch (e) {
    console.error('[ai error]', e.message);
    session.processing = false;
    await sock.sendMessage(chatId, { text: 'AI error. Please try again.' });
    return;
  }

  console.log(`[msg] ${text.slice(0, 60)}`);
  console.log(`[ai]  ${reply.slice(0, 80)}`);

  let finalReply = reply;
  for (let attempt = 0; attempt < 2; attempt++) {
    try {
      const result = await dispatch(attempt === 0 ? reply : await ai(TRAINING, messages), session, sock, chatId, text);
      if (result) { finalReply = result; break; }
      break;
    } catch (e) {
      console.error(`[dispatch] attempt ${attempt + 1}:`, e.message);
      if (attempt === 1) finalReply = '';
    }
  }

  // Detect which commands were in the original reply (before stripping)
  const hadLeadData   = /LEADDATA\s*\(/i.test(finalReply);
  const hadUpdateData = /UPDATEDATA\s*\(/i.test(finalReply);

  // Strip leftover command tokens
  // LEADDATA and UPDATEDATA use {JSON} bodies — anchor on { } to avoid stopping
  // early at ) characters that may appear inside string values (e.g. "Spanish (intermediate)")
  finalReply = finalReply
    .replace(/\bCREATELEAD\b/gi, '')
    .replace(/\bLEADDATA\s*\(\s*\{[\s\S]*?\}\s*\)\s*/gi, '')
    .replace(/\bUPDATEDATA\s*\(\s*"[^"]*"\s*,\s*\{[\s\S]*?\}\s*\)\s*/gi, '')
    .replace(/\bSAVECV\s*\("[^"]*"\)\s*/gi, '')
    .replace(/\bGENERATEREPORT\s*\(\s*\)\s*/gi, '')
    .replace(/\bGENERATEREPORTFOR\s*\("[^"]*"\)\s*/gi, '')
    .replace(/\b(SEARCHJOBS|MULTISEARCH|CLIENTSEARCH|CAREERSTRATEGY|SENDTOGROUP)\s*\([\s\S]*?\)/gi, '')
    .trim();

  // If AI only emitted a command with no surrounding text, ask it to generate the confirmation
  if (!finalReply && (hadLeadData || hadUpdateData)) {
    const action = hadLeadData
      ? `The profile for "${session.pendingLead?.leadName}" was successfully created.`
      : 'The profile update was successfully applied.';
    try {
      finalReply = await ai(TRAINING, [
        ...messages,
        { role: 'assistant', content: reply },
        { role: 'system',    content: `[System] ${action} Generate a brief, natural confirmation for the user. Use the same language as the conversation.` },
      ]);
      finalReply = finalReply
        .replace(/\bLEADDATA\s*\(\s*\{[\s\S]*?\}\s*\)\s*/gi, '')
        .replace(/\bUPDATEDATA\s*\(\s*"[^"]*"\s*,\s*\{[\s\S]*?\}\s*\)\s*/gi, '')
        .trim();
    } catch (e) {
      console.warn('[confirm-ai]', e.message);
    }
  }

  // URL guard — strip any job entry whose LinkedIn URL wasn't registered from search results
  const urlRe = /https?:\/\/(?:[\w-]+\.)?linkedin\.com\/jobs\/view\/[\w%-]+/gi;
  const urlsInReply = [...finalReply.matchAll(urlRe)].map(m => normalizeUrl(m[0]));
  if (urlsInReply.length) {
    const fake = new Set(urlsInReply.filter(u => !session.shownUrls.has(u)));
    if (fake.size) {
      console.warn(`[guard] Stripping ${fake.size} unregistered URL(s):`, [...fake]);
      // Remove the entire numbered entry (line starting with digit + dot through next blank line)
      finalReply = finalReply
        .split(/\n(?=\d+\.)/)
        .filter(block => {
          const urls = [...block.matchAll(urlRe)].map(m => normalizeUrl(m[0]));
          return !urls.some(u => fake.has(u));
        })
        .join('\n');
      // Re-number sequentially
      let n = 1;
      finalReply = finalReply.replace(/^\d+\./gm, () => `${n++}.`);
    }
  }

  if (finalReply) {
    saveMsg(chatId, 'assistant', finalReply);
    await sock.sendPresenceUpdate('paused', chatId);
    await sock.sendMessage(chatId, { text: finalReply });
  }
  session.processing = false;
}

// ── Watchdog — retry unanswered messages every 30s ────────────────────────────

let watchdogTimer = null;

function startWatchdog(sock) {
  if (watchdogTimer) clearInterval(watchdogTimer); // never accumulate intervals
  watchdogTimer = setInterval(async () => {
    const chatsDir = join(BOT_DIR, 'chats');
    if (!existsSync(chatsDir)) return;
    const now = Date.now();
    for (const chatId of readdirSync(chatsDir)) {
      const session = getSession(chatId);
      if (session.processing) continue;
      const history = loadHistory(chatId, 5);
      if (!history.length) continue;
      const last = history[history.length - 1];
      if (last.role !== 'user') continue;
      const age = now - last.ts;
      if (age < 5000 || age > 10 * 60 * 1000) continue;
      console.log(`[watchdog] unanswered msg in ${chatId}, retrying...`);
      const syntheticMsg = {
        key: { remoteJid: chatId, fromMe: false },
        message: { conversation: last.text },
      };
      const f = histFile(chatId);
      const msgs = JSON.parse(readFileSync(f, 'utf8'));
      msgs.pop();
      writeFileSync(f, JSON.stringify(msgs, null, 2));
      try { await handleMsg(sock, syntheticMsg); } catch (e) { console.error('[watchdog]', e.message); }
    }
  }, 30000);
}

// ── WhatsApp ───────────────────────────────────────────────────────────────────

let isConnecting = false;
let jobSchedulerStarted = false;
let activeSock = null;

async function connect() {
  if (isConnecting) return;
  isConnecting = true;

  const { state, saveCreds } = await useMultiFileAuthState(join(BOT_DIR, 'session'));
  const { version } = await fetchLatestBaileysVersion();

  const sock = makeWASocket({
    version,
    auth: state,
    printQRInTerminal: false,
    browser: ['EcoBe Job', 'Chrome', '1.0'],
    syncFullHistory: false,
    logger: (await import('pino')).default({ level: 'silent' }),
  });

  sock.ev.on('creds.update', saveCreds);

  sock.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => {
    if (qr) qrcode.generate(qr, { small: true });
    if (connection === 'open') {
      isConnecting = false;
      activeSock = sock;
      console.log('[ws] connected');
      startWatchdog(sock);
      // Wire the silver WhatsApp adapter to this live socket so silverCourse
      // can send messages without knowing about Baileys directly.
      try {
        const { configureProd } = await import('./lib/whatsappAdapter.js');
        configureProd(sock);
      } catch (e) { console.warn('[silver] adapter wire failed:', e.message); }
      if (!jobSchedulerStarted) {
        jobSchedulerStarted = true;
        startJobScheduler(sock, SCRIPT_DIR, BOT_DIR, ai);
      }
    }
    if (connection === 'close') {
      isConnecting = false;
      const code = lastDisconnect?.error?.output?.statusCode;
      const reason = lastDisconnect?.error?.message || 'unknown';
      if (code === DisconnectReason.loggedOut) {
        console.log('[ws] logged out — delete session/ and restart');
      } else {
        console.log(`[ws] disconnected (code=${code} reason=${reason}) — reconnecting in 5s`);
        setTimeout(connect, 5000);
      }
    }
  });

  sock.ev.on('messages.upsert', async ({ messages, type }) => {
    if (type !== 'notify') return;
    for (const msg of messages) {
      if (msg.key.fromMe) continue;
      try { await handleMsg(sock, msg); } catch (e) { console.error('[handler]', e.message); }
    }
  });

  // ECO-39: detect coach joining a Silver group → flip coachJoinedAt + send
  // "first lesson tomorrow" copy. Lookup against numbers.json (coach roster)
  // and the silver.json registry (group-jid → mentee).
  sock.ev.on('group-participants.update', async ({ id: groupJid, participants, action }) => {
    if (action !== 'add') return;
    try {
      const { findSilverByGroupJid } = await import('./lib/silverStore.js');
      const { applyCoachJoin } = await import('./lib/silverCourse.js');
      const state = findSilverByGroupJid(SCRIPT_DIR, groupJid);
      if (!state) return; // not a Silver group, ignore
      if (state.coachJoinedAt) return; // already joined; idempotent
      // Match each new participant against numbers.json (coach roster).
      for (const jid of participants) {
        const num = jid.replace(/:.*/, '').replace(/@.*/, '');
        const coach = NUMBERS.find(n => n.number === num);
        if (!coach) continue; // not a coach, just a guest — skip
        await applyCoachJoin(SCRIPT_DIR, state.slug, {
          coachJid: jid,
          coachName: coach.name || null,
          source: 'baileys-event',
        });
        break; // first coach to join wins
      }
    } catch (e) {
      console.error('[coach-join]', e.message);
    }
  });
}

// ── HTTP server: Hotmart webhook (public) + /inject (loopback test) ──────────
// PORT defaults to 7331. /webhooks/hotmart is bound publicly so Hotmart can
// POST to it (point a reverse proxy on vianexu.com → this port). /inject keeps
// its loopback-only behavior to avoid coach-impersonation from the network.
const PORT = Number(process.env.PORT || 7331);
const BIND = process.env.BIND_HOST || '0.0.0.0';

http.createServer(async (req, res) => {
  // Hotmart Postback webhook (ECO-5)
  if (req.method === 'POST' && req.url === '/webhooks/hotmart') {
    const chunks = [];
    req.on('data', d => chunks.push(d));
    req.on('end', async () => {
      try {
        const raw = Buffer.concat(chunks);
        const { handleHotmartWebhook } = await import('./lib/hotmartWebhook.js');
        const result = await handleHotmartWebhook(SCRIPT_DIR, { rawBody: raw, headers: req.headers });
        res.writeHead(result.status, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(result.body));
      } catch (e) {
        console.error('[hotmart]', e);
        res.writeHead(500); res.end(JSON.stringify({ error: e.message }));
      }
    });
    return;
  }

  // Coach admin endpoints (ECO-15 / ECO-38). Bearer-auth via
  // WHATSAPP_ORCHESTRATOR_TOKEN; called by the dashboard at vianexu.com/clients/ecobe/admin.
  if (req.url.startsWith('/admin/silver/')) {
    const chunks = [];
    req.on('data', d => chunks.push(d));
    req.on('end', async () => {
      try {
        const raw = Buffer.concat(chunks);
        const { handleCoachAdminRequest } = await import('./lib/coachAdmin.js');
        const result = await handleCoachAdminRequest(SCRIPT_DIR, req, raw);
        res.writeHead(result.status, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(result.body));
      } catch (e) {
        console.error('[coach-admin]', e);
        res.writeHead(500); res.end(JSON.stringify({ error: e.message }));
      }
    });
    return;
  }

  // /inject (loopback only — for local QA of Gold flow)
  if (req.method === 'POST' && req.url === '/inject') {
    const remote = req.socket.remoteAddress || '';
    if (!/^(127\.|::1|::ffff:127\.)/.test(remote)) {
      res.writeHead(403); res.end('inject is loopback-only'); return;
    }
    let body = '';
    req.on('data', d => body += d);
    req.on('end', async () => {
      try {
        const { chatId = '155336468082758@lid', text } = JSON.parse(body);
        if (!activeSock) { res.writeHead(503); res.end(JSON.stringify({ error: 'bot not connected yet' })); return; }
        const captured = [];
        const proxySock = new Proxy(activeSock, {
          get(target, prop) {
            if (prop === 'sendMessage') return async (jid, content) => { if (content?.text) captured.push(content.text); };
            if (prop === 'sendPresenceUpdate') return async () => {};
            return typeof target[prop] === 'function' ? target[prop].bind(target) : target[prop];
          },
        });
        const fakeMsg = {
          key: { remoteJid: chatId, fromMe: false, id: `inject-${Date.now()}` },
          message: { conversation: text },
          messageTimestamp: Math.floor(Date.now() / 1000),
          pushName: 'TestCoach',
        };
        saveMsg(chatId, 'user', text);
        await handleMsg(proxySock, fakeMsg);
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ replies: captured }));
      } catch (e) {
        console.error('[inject]', e.message);
        res.writeHead(500); res.end(JSON.stringify({ error: e.message }));
      }
    });
    return;
  }

  res.writeHead(404); res.end('not found');
}).listen(PORT, BIND, () => console.log(`[http] listening on ${BIND}:${PORT} (hotmart webhook + /inject loopback)`));

async function main() {
  startSyncLoop(SCRIPT_DIR);
  try {
    CLIENTS = await loadClientsFromAPI();
    console.log(`[viaweb] ${CLIENTS.length} mentee(s) loaded`);
  } catch (e) {
    console.error('[viaweb] failed to load clients:', e.message);
  }
  connect();
}

main();
