require("dotenv").config({ path: require("path").join(__dirname, ".env") });
    const express = require('express');
    const fs = require('fs');
    const path = require('path');
    const crypto = require('crypto');
    const nodemailer = require('nodemailer');
    const bcrypt = require('bcryptjs');
    const { runOutreachDiscovery } = require('./outreach');

    const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY || 'sk-ant-REPLACE_WITH_YOUR_REAL_KEY';

    const app = express();
    const PORT = 3000;

    // ====================== FIXED PATHS ======================
    const BASE_PATH   = '/admin';   // coach/admin routes
    const CLIENT_PATH = '/client';  // client/mentee routes
    const APP_URL     = process.env.APP_URL || 'https://ecobeadmin.com';
    const BASE_DIR = __dirname;  // Real filesystem path

    // ====================== EXPRESS SETUP ======================
    app.set('view engine', 'ejs');
    app.set('views', path.join(BASE_DIR, 'src', 'views'));

    // Static files
    app.use(BASE_PATH + '/css', express.static(path.join(BASE_DIR, 'src', 'views', 'css')));
    // NOTE: /db is intentionally NOT served as static — all DB files are private

    app.use(express.json({ limit: '20mb' }));
    app.use(express.urlencoded({ extended: true, limit: '20mb' }));

    // Cookie parser
    app.use((req, res, next) => {
        const cookies = {};
        if (req.headers.cookie) {
            req.headers.cookie.split(';').forEach(cookie => {
                const [name, value] = cookie.trim().split('=');
                if (name) cookies[name] = value;
            });
        }
        req.cookies = cookies;
        next();
    });

    // ====================== SESSIONS ======================
    const sessions = {};

    function generateSessionId() {
        return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
    }

    function isLoggedIn(req) {
        const sessionId = req.cookies?.sessionId;
        return Boolean(sessionId && sessions[sessionId]?.loggedIn && sessions[sessionId]?.role === 'coach');
    }

    function setCookie(res, name, value, maxAge) {
        const maxAgeStr = maxAge ? `; Max-Age=${maxAge}` : '';
        res.setHeader('Set-Cookie', `${name}=${value}; Path=/; HttpOnly; SameSite=Lax${maxAgeStr}`);
    }

    function requireAuth(req, res, next) {
        if (!isLoggedIn(req)) {
            return res.redirect(BASE_PATH + '/login');
        }
        next();
    }

    // ====================== COACHES DB ======================
    const COACHES_FILE = path.join(BASE_DIR, 'src', 'db', 'coaches.json');

    function readCoaches() {
        try {
            if (!fs.existsSync(COACHES_FILE)) return [];
            return JSON.parse(fs.readFileSync(COACHES_FILE, 'utf8'));
        } catch { return []; }
    }

    function writeCoaches(coaches) {
        fs.writeFileSync(COACHES_FILE, JSON.stringify(coaches, null, 2));
    }

    function findCoachByEmail(email) {
        return readCoaches().find(c => c.email.toLowerCase() === email.toLowerCase()) || null;
    }

    function findCoachByToken(token, field) {
        return readCoaches().find(c => c[field] === token) || null;
    }

    function updateCoach(email, updates) {
        const coaches = readCoaches();
        const idx = coaches.findIndex(c => c.email.toLowerCase() === email.toLowerCase());
        if (idx === -1) return false;
        coaches[idx] = { ...coaches[idx], ...updates };
        writeCoaches(coaches);
        return true;
    }

    // ====================== CLIENT AUTH ======================
    function isClientLoggedIn(req) {
        const sessionId = req.cookies?.clientSessionId;
        return Boolean(sessionId && sessions[sessionId]?.loggedIn && sessions[sessionId]?.role === 'client');
    }

    function requireClientAuth(req, res, next) {
        if (!isClientLoggedIn(req)) {
            return res.redirect(CLIENT_PATH + '/login');
        }
        next();
    }

    function getClientSession(req) {
        const sessionId = req.cookies?.clientSessionId;
        return sessionId ? sessions[sessionId] : null;
    }

    // Accepts both coach and client sessions
    function requireAnyAuth(req, res, next) {
        if (isLoggedIn(req) || isClientLoggedIn(req)) return next();
        return res.redirect(BASE_PATH + '/login');
    }

    // Returns true if the authenticated user may operate on this slug
    function canAccessSlug(req, slug) {
        if (isLoggedIn(req)) return true;
        if (isClientLoggedIn(req)) {
            const session = getClientSession(req);
            const base = session.clientSlug.split('_')[0];
            return session.clientSlug === slug || base === slug;
        }
        return false;
    }

    // Returns display first name for audit log
    function getActorName(req) {
        const sid = req.cookies?.sessionId;
        if (sid && sessions[sid]?.role === 'coach') {
            const part = (sessions[sid].email || '').split('@')[0];
            return part.charAt(0).toUpperCase() + part.slice(1);
        }
        const cid = req.cookies?.clientSessionId;
        if (cid && sessions[cid]?.role === 'client') {
            const raw = sessions[cid].clientName || sessions[cid].clientSlug || '';
            const first = raw.split(/[-\s]/)[0];
            return first.charAt(0).toUpperCase() + first.slice(1);
        }
        return 'Unknown';
    }

    // Builds a human-readable description of field changes for the changelog
    function describeUpdates(actorName, updates, oldData) {
        const labels = {
            exclusions: 'exclusions', languages: 'languages', workPermits: 'work permits',
            location: 'location', phone: 'phone', linkedin: 'LinkedIn',
            nationality: 'nationality', openToRemote: 'open to remote',
            openToRelocate: 'open to relocate', targetRoleFamilies: 'target roles',
            targetGeographies: 'target geographies', salaryTarget: 'salary target',
            phase: 'phase', seniority: 'seniority', shortDescription: 'description',
            leadName: 'name', email: 'email', note: 'note'
        };
        const parts = [];
        for (const [field, newVal] of Object.entries(updates)) {
            const label = labels[field] || field;
            const oldVal = oldData?.[field];
            if (Array.isArray(newVal)) {
                const oldArr = Array.isArray(oldVal) ? oldVal : [];
                const added = newVal.filter(v => !oldArr.includes(v));
                const removed = oldArr.filter(v => !newVal.includes(v));
                if (added.length) parts.push(`added '${added.join("', '")}' to ${label}`);
                if (removed.length) parts.push(`removed '${removed.join("', '")}' from ${label}`);
                if (!added.length && !removed.length) parts.push(`updated ${label}`);
            } else if (typeof newVal === 'boolean') {
                parts.push(`set ${label} to ${newVal ? 'yes' : 'no'}`);
            } else if (oldVal !== undefined && String(oldVal) !== String(newVal)) {
                parts.push(`changed ${label} to '${newVal}'`);
            } else if (newVal !== undefined) {
                parts.push(`set ${label} to '${newVal}'`);
            }
        }
        if (!parts.length) return `${actorName} made profile changes`;
        return `${actorName} ${parts.join('; ')}`;
    }

    // ====================== EMAIL ======================
    const mailer = nodemailer.createTransport({
        host: process.env.SMTP_HOST || 'vianexu.com',
        port: parseInt(process.env.SMTP_PORT || '465'),
        secure: true,
        auth: {
            user: process.env.SMTP_USER || 'info@vianexu.com',
            pass: process.env.SMTP_PASS || 'wxuixzhjqvdbwldz'
        }
    });

    async function sendSetupEmail(email, name, token) {
        const setupUrl = `${APP_URL}${CLIENT_PATH}/setup/${token}`;
        await mailer.sendMail({
            from: '"EcoBot" <info@vianexu.com>',
            to: email,
            subject: 'Welcome to EcoBot — Set your password',
            html: `
    <!DOCTYPE html>
    <html>
    <head><meta charset="UTF-8"></head>
    <body style="margin:0;padding:0;background:#f4f4f4;font-family:Ubuntu,Arial,sans-serif;">
    <div style="max-width:600px;margin:40px auto;padding:20px;">
        <div style="background:white;border-radius:10px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,0.1);">
        <div style="background:linear-gradient(135deg,#28a745,#20c997);padding:30px;text-align:center;">
            <h1 style="color:white;margin:0;font-size:28px;font-weight:700;">EcoBot</h1>
            <p style="color:rgba(255,255,255,0.85);margin:8px 0 0;font-size:14px;">Career Strategy Platform</p>
        </div>
        <div style="padding:35px 30px;">
            <h2 style="color:#333;margin:0 0 15px;font-size:22px;">Hello, ${name}!</h2>
            <p style="color:#666;line-height:1.7;margin:0 0 15px;">
            Your EcoBot account has been created. You can now access your personal career dossier.
            </p>
            <p style="color:#666;line-height:1.7;margin:0 0 25px;">
            Click the button below to set your password.
            This link is valid for <strong>48 hours</strong>.
            </p>
            <div style="text-align:center;margin:30px 0;">
            <a href="${setupUrl}"
                style="display:inline-block;background:#28a745;color:white;padding:14px 35px;
                        border-radius:6px;text-decoration:none;font-weight:600;font-size:16px;
                        letter-spacing:0.3px;">
                Set Password
            </a>
            </div>
            <hr style="border:none;border-top:1px solid #eee;margin:25px 0;">
            <p style="color:#999;font-size:12px;line-height:1.6;margin:0;">
            If you did not expect this email, you can safely ignore it.<br>
            Direct link: <a href="${setupUrl}" style="color:#28a745;">${setupUrl}</a>
            </p>
        </div>
        </div>
    </div>
    </body>
    </html>`
        });
    }

    async function sendCoachInviteEmail(email, token) {
        const setupUrl = `${APP_URL}${BASE_PATH}/coach/setup/${token}`;
        await mailer.sendMail({
            from: '"EcoBot" <info@vianexu.com>',
            to: email,
            subject: 'EcoBot — Crie a sua conta de coach',
            html: `
    <!DOCTYPE html>
    <html>
    <head><meta charset="UTF-8"></head>
    <body style="margin:0;padding:0;background:#f4f4f4;font-family:Ubuntu,Arial,sans-serif;">
    <div style="max-width:600px;margin:40px auto;padding:20px;">
        <div style="background:white;border-radius:10px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,0.1);">
        <div style="background:linear-gradient(135deg,#28a745,#20c997);padding:30px;text-align:center;">
            <h1 style="color:white;margin:0;font-size:28px;font-weight:700;">EcoBot</h1>
            <p style="color:rgba(255,255,255,0.85);margin:8px 0 0;font-size:14px;">Career Strategy Platform</p>
        </div>
        <div style="padding:35px 30px;">
            <h2 style="color:#333;margin:0 0 15px;font-size:22px;">Bem-vindo ao EcoBot!</h2>
            <p style="color:#666;line-height:1.7;margin:0 0 15px;">
            Foi criada uma conta de coach para o endereço <strong>${email}</strong>.
            </p>
            <p style="color:#666;line-height:1.7;margin:0 0 25px;">
            Clique no botão abaixo para definir a sua senha e aceder ao painel.
            Este link é válido por <strong>48 horas</strong>.
            </p>
            <div style="text-align:center;margin:30px 0;">
            <a href="${setupUrl}"
                style="display:inline-block;background:#28a745;color:white;padding:14px 35px;
                        border-radius:6px;text-decoration:none;font-weight:600;font-size:16px;
                        letter-spacing:0.3px;">
                Criar Conta
            </a>
            </div>
            <hr style="border:none;border-top:1px solid #eee;margin:25px 0;">
            <p style="color:#999;font-size:12px;line-height:1.6;margin:0;">
            Se não esperava este email, pode ignorá-lo com segurança.<br>
            Link direto: <a href="${setupUrl}" style="color:#28a745;">${setupUrl}</a>
            </p>
        </div>
        </div>
    </div>
    </body>
    </html>`
        });
    }

    async function sendCoachNewClientEmail(coachEmail, coachName, clientName, adminUrl) {
        await mailer.sendMail({
            from: '"EcoBot" <info@vianexu.com>',
            to: coachEmail,
            subject: `EcoBot — Novo cliente adicionado: ${clientName}`,
            html: `
    <!DOCTYPE html>
    <html>
    <head><meta charset="UTF-8"></head>
    <body style="margin:0;padding:0;background:#f4f4f4;font-family:Ubuntu,Arial,sans-serif;">
    <div style="max-width:600px;margin:40px auto;padding:20px;">
        <div style="background:white;border-radius:10px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,0.1);">
        <div style="background:linear-gradient(135deg,#28a745,#20c997);padding:30px;text-align:center;">
            <h1 style="color:white;margin:0;font-size:28px;font-weight:700;">EcoBot</h1>
            <p style="color:rgba(255,255,255,0.85);margin:8px 0 0;font-size:14px;">Career Strategy Platform</p>
        </div>
        <div style="padding:35px 30px;">
            <h2 style="color:#333;margin:0 0 15px;font-size:22px;">Novo cliente adicionado</h2>
            <p style="color:#666;line-height:1.7;margin:0 0 15px;">
            Olá, <strong>${coachName}</strong>! Um novo perfil de cliente foi criado na plataforma.
            </p>
            <p style="color:#666;line-height:1.7;margin:0 0 25px;">
            <strong>Cliente:</strong> ${clientName}
            </p>
            <div style="text-align:center;margin:30px 0;">
            <a href="${adminUrl}"
                style="display:inline-block;background:#28a745;color:white;padding:14px 35px;
                        border-radius:6px;text-decoration:none;font-weight:600;font-size:16px;
                        letter-spacing:0.3px;">
                Ver perfil no admin
            </a>
            </div>
            <hr style="border:none;border-top:1px solid #eee;margin:25px 0;">
            <p style="color:#999;font-size:12px;line-height:1.6;margin:0;">
            Link direto: <a href="${adminUrl}" style="color:#28a745;">${adminUrl}</a>
            </p>
        </div>
        </div>
    </div>
    </body>
    </html>`
        });
    }

    async function sendCoachPendingJobsEmail(coachEmail, coachName, clientName, jobs, profileUrl) {
        const jobRows = jobs.map(j =>
            `<tr>
                <td style="padding:8px 12px;border-bottom:1px solid #f0f0f0;font-size:13px;color:#222;">${j.title}</td>
                <td style="padding:8px 12px;border-bottom:1px solid #f0f0f0;font-size:13px;color:#555;">${j.company}</td>
                <td style="padding:8px 12px;border-bottom:1px solid #f0f0f0;font-size:13px;color:#888;">${j.location}</td>
            </tr>`
        ).join('');
        await mailer.sendMail({
            from: '"EcoBot" <info@vianexu.com>',
            to: coachEmail,
            subject: `EcoBot — ${jobs.length} vaga(s) aguardando aprovação: ${clientName}`,
            html: `<!DOCTYPE html><html><head><meta charset="UTF-8"></head>
<body style="margin:0;padding:0;background:#f4f4f4;font-family:Ubuntu,Arial,sans-serif;">
<div style="max-width:620px;margin:40px auto;padding:20px;">
  <div style="background:white;border-radius:10px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,0.1);">
    <div style="background:linear-gradient(135deg,#28a745,#20c997);padding:28px 30px;text-align:center;">
      <h1 style="color:white;margin:0;font-size:26px;font-weight:700;">EcoBot</h1>
      <p style="color:rgba(255,255,255,0.85);margin:6px 0 0;font-size:14px;">Vagas aguardando aprovação</p>
    </div>
    <div style="padding:30px;">
      <p style="color:#333;font-size:15px;margin:0 0 8px;">Olá, <strong>${coachName}</strong>!</p>
      <p style="color:#555;font-size:14px;line-height:1.7;margin:0 0 20px;">
        O sistema encontrou <strong>${jobs.length} vaga(s)</strong> para <strong>${clientName}</strong> (plano Partnership).
        Revise e aprove as vagas antes que apareçam no painel do cliente.
      </p>
      <table style="width:100%;border-collapse:collapse;margin-bottom:24px;">
        <thead>
          <tr style="background:#f9f9f9;">
            <th style="text-align:left;padding:8px 12px;font-size:11px;font-weight:700;color:#aaa;text-transform:uppercase;letter-spacing:0.5px;border-bottom:2px solid #eee;">Cargo</th>
            <th style="text-align:left;padding:8px 12px;font-size:11px;font-weight:700;color:#aaa;text-transform:uppercase;letter-spacing:0.5px;border-bottom:2px solid #eee;">Empresa</th>
            <th style="text-align:left;padding:8px 12px;font-size:11px;font-weight:700;color:#aaa;text-transform:uppercase;letter-spacing:0.5px;border-bottom:2px solid #eee;">Local</th>
          </tr>
        </thead>
        <tbody>${jobRows}</tbody>
      </table>
      <div style="text-align:center;margin:28px 0;">
        <a href="${profileUrl}" style="display:inline-block;background:#28a745;color:white;padding:13px 32px;border-radius:6px;text-decoration:none;font-weight:600;font-size:15px;">
          Revisar e Aprovar Vagas
        </a>
      </div>
      <hr style="border:none;border-top:1px solid #eee;margin:20px 0;">
      <p style="color:#bbb;font-size:12px;margin:0;">Link direto: <a href="${profileUrl}" style="color:#28a745;">${profileUrl}</a></p>
    </div>
  </div>
</div>
</body></html>`
        });
    }

    async function sendCourseCompleteEmail(email, firstName) {
        const loginUrl = `${APP_URL}${BASE_PATH}/client/login`;
        await mailer.sendMail({
            from: '"Ecobe Academy" <info@vianexu.com>',
            to: email,
            subject: 'Your Ecobe journey is ready — complete your profile now',
            html: `<!DOCTYPE html><html><head><meta charset="UTF-8"></head>
<body style="margin:0;padding:0;background:#f4f6f4;font-family:Ubuntu,Arial,sans-serif;">
<div style="max-width:600px;margin:40px auto;padding:20px;">
  <div style="background:white;border-radius:10px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,0.1);">
    <div style="background:linear-gradient(135deg,#28a745,#20c997);padding:32px 30px;text-align:center;">
      <h1 style="color:white;margin:0;font-size:28px;font-weight:700;">Ecobe Academy</h1>
      <p style="color:rgba(255,255,255,0.85);margin:8px 0 0;font-size:14px;">Career Strategy Platform</p>
    </div>
    <div style="padding:35px 30px;">
      <h2 style="color:#333;margin:0 0 12px;font-size:22px;">You're ready to go, ${firstName}!</h2>
      <p style="color:#555;line-height:1.7;margin:0 0 16px;font-size:15px;">
        Your coach has confirmed your onboarding is complete. Your <strong>Profile Form</strong> and
        <strong>Career Dossier</strong> are now unlocked in your portal — please fill them in so we
        can build your personalised career strategy.
      </p>
      <p style="color:#555;line-height:1.7;margin:0 0 28px;font-size:15px;">
        The more detail you provide, the better we can tailor your job search and coaching sessions.
        It only takes a few minutes!
      </p>
      <div style="text-align:center;margin:28px 0;">
        <a href="${loginUrl}" style="display:inline-block;background:#28a745;color:white;padding:14px 36px;border-radius:7px;text-decoration:none;font-weight:700;font-size:15px;letter-spacing:0.3px;">
          Open My Portal
        </a>
      </div>
      <hr style="border:none;border-top:1px solid #eee;margin:24px 0;">
      <p style="color:#bbb;font-size:12px;margin:0;text-align:center;">
        If the button doesn't work, copy this link: <a href="${loginUrl}" style="color:#28a745;">${loginUrl}</a>
      </p>
    </div>
  </div>
</div>
</body></html>`
        });
    }

    async function sendPasswordResetEmail(email, token) {
        const resetUrl = `${APP_URL}${BASE_PATH}/reset-password/${token}`;
        await mailer.sendMail({
            from: '"EcoBot" <info@vianexu.com>',
            to: email,
            subject: 'EcoBot — Redefinir senha',
            html: `
    <!DOCTYPE html>
    <html>
    <head><meta charset="UTF-8"></head>
    <body style="margin:0;padding:0;background:#f4f4f4;font-family:Ubuntu,Arial,sans-serif;">
    <div style="max-width:600px;margin:40px auto;padding:20px;">
        <div style="background:white;border-radius:10px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,0.1);">
        <div style="background:linear-gradient(135deg,#28a745,#20c997);padding:30px;text-align:center;">
            <h1 style="color:white;margin:0;font-size:28px;font-weight:700;">EcoBot</h1>
            <p style="color:rgba(255,255,255,0.85);margin:8px 0 0;font-size:14px;">Career Strategy Platform</p>
        </div>
        <div style="padding:35px 30px;">
            <h2 style="color:#333;margin:0 0 15px;font-size:22px;">Redefinir Senha</h2>
            <p style="color:#666;line-height:1.7;margin:0 0 15px;">
            Recebemos um pedido para redefinir a senha da conta <strong>${email}</strong>.
            </p>
            <p style="color:#666;line-height:1.7;margin:0 0 25px;">
            Clique no botão abaixo para criar uma nova senha.
            Este link é válido por <strong>2 horas</strong>.
            </p>
            <div style="text-align:center;margin:30px 0;">
            <a href="${resetUrl}"
                style="display:inline-block;background:#28a745;color:white;padding:14px 35px;
                        border-radius:6px;text-decoration:none;font-weight:600;font-size:16px;
                        letter-spacing:0.3px;">
                Redefinir Senha
            </a>
            </div>
            <hr style="border:none;border-top:1px solid #eee;margin:25px 0;">
            <p style="color:#999;font-size:12px;line-height:1.6;margin:0;">
            Se não pediu a redefinição de senha, pode ignorar este email.<br>
            Link direto: <a href="${resetUrl}" style="color:#28a745;">${resetUrl}</a>
            </p>
        </div>
        </div>
    </div>
    </body>
    </html>`
        });
    }

    // ====================== HELPERS ======================
    function appendChangelog(menteeDir, entry) {
        const logPath = path.join(menteeDir, 'changelog.json');
        let log = [];
        try { log = JSON.parse(fs.readFileSync(logPath, 'utf8')); } catch {}
        log.push({ ...entry, ts: new Date().toISOString() });
        try { fs.writeFileSync(logPath, JSON.stringify(log, null, 2)); } catch {}
    }

    function generateToken() {
        return crypto.randomBytes(32).toString('hex');
    }

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

    // Find the actual folder for a given slug (accepts "slug" or "slug_id")
    function findMenteeFolder(slug) {
        const menteesDir = path.join(BASE_DIR, 'src', 'db', 'mentees');
        try {
            const entries = fs.readdirSync(menteesDir);
            if (entries.includes(slug)) return path.join(menteesDir, slug);
            const match = entries.find(e => e === slug || e.startsWith(slug + '_'));
            return match ? path.join(menteesDir, match) : null;
        } catch { return null; }
    }

    // Find mentee folder by email (scans all mentees' auth.json)
    function findMenteeByEmail(email) {
        const menteesDir = path.join(BASE_DIR, 'src', 'db', 'mentees');
        try {
            const entries = fs.readdirSync(menteesDir);
            for (const entry of entries) {
                const authPath = path.join(menteesDir, entry, 'auth.json');
                if (!fs.existsSync(authPath)) continue;
                try {
                    const auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
                    if (auth.email && auth.email.toLowerCase() === email.toLowerCase()) {
                        return { slug: entry, auth, dir: path.join(menteesDir, entry) };
                    }
                } catch {}
            }
        } catch {}
        return null;
    }

    // Find mentee folder by setup token
    function findMenteeByToken(token) {
        const menteesDir = path.join(BASE_DIR, 'src', 'db', 'mentees');
        try {
            const entries = fs.readdirSync(menteesDir);
            for (const entry of entries) {
                const authPath = path.join(menteesDir, entry, 'auth.json');
                if (!fs.existsSync(authPath)) continue;
                try {
                    const auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
                    if (auth.setupToken === token) {
                        return { slug: entry, auth, dir: path.join(menteesDir, entry) };
                    }
                } catch {}
            }
        } catch {}
        return null;
    }

    // ====================== ROUTES ======================

    // Root redirects
    app.get(BASE_PATH, (req, res) => res.redirect(BASE_PATH + '/login'));
    app.get(BASE_PATH + '/', (req, res) => res.redirect(BASE_PATH + '/login'));

    // Login Page
    app.get(BASE_PATH + '/login', (req, res) => {
        if (isLoggedIn(req)) return res.redirect(BASE_PATH + '/dashboard');
        res.render('login', { error: null });
    });

    // Login API
    app.post(BASE_PATH + '/api/login', async (req, res) => {
        const { email, password } = req.body;
        if (!email || !password) {
            return res.status(400).json({ success: false, error: 'Email e senha são obrigatórios' });
        }
        const coach = findCoachByEmail(email);
        if (!coach || !coach.setupComplete || !coach.passwordHash) {
            return res.status(401).json({ success: false, error: 'Email ou senha inválidos' });
        }
        const valid = await bcrypt.compare(password, coach.passwordHash);
        if (!valid) {
            return res.status(401).json({ success: false, error: 'Email ou senha inválidos' });
        }
        const sessionId = generateSessionId();
        sessions[sessionId] = { loggedIn: true, email: coach.email, role: 'coach' };
        setCookie(res, 'sessionId', sessionId);
        res.json({ success: true, redirect: BASE_PATH + '/dashboard' });
    });

    // Forgot password page
    app.get(BASE_PATH + '/forgot-password', (req, res) => {
        res.render('forgot-password', { sent: false });
    });

    // Forgot password API
    app.post(BASE_PATH + '/api/forgot-password', async (req, res) => {
        const { email } = req.body;
        console.log('[FORGOT] email:', email);
        if (email) {
            const coach = findCoachByEmail(email);
            console.log('[FORGOT] coach found:', coach ? { email: coach.email, setupComplete: coach.setupComplete } : null);
            if (coach && coach.setupComplete) {
                const resetToken = generateToken();
                updateCoach(email, {
                    resetToken,
                    resetTokenExpiry: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString()
                });
                sendPasswordResetEmail(email, resetToken)
                    .then(() => console.log('[FORGOT] email sent to', email))
                    .catch(err => console.error('[FORGOT] email failed:', err.message, err.code));
            }
        }
        res.json({ success: true });
    });

    // Reset password — show form
    app.get(BASE_PATH + '/reset-password/:token', (req, res) => {
        const coach = findCoachByToken(req.params.token, 'resetToken');
        if (!coach || !coach.resetTokenExpiry || new Date(coach.resetTokenExpiry) < new Date()) {
            return res.render('reset-password', { error: 'Invalid or expired link.', token: null });
        }
        res.render('reset-password', { error: null, token: req.params.token });
    });

    // Reset password — save
    app.post(BASE_PATH + '/reset-password/:token', async (req, res) => {
        const { token } = req.params;
        const { password, passwordConfirm } = req.body || {};
        const coach = findCoachByToken(token, 'resetToken');
        if (!coach || !coach.resetTokenExpiry || new Date(coach.resetTokenExpiry) < new Date()) {
            return res.render('reset-password', { error: 'Invalid or expired link.', token: null });
        }
        if (!password || password.length < 6) {
            return res.render('reset-password', { error: 'Password must be at least 6 characters.', token });
        }
        if (password !== passwordConfirm) {
            return res.render('reset-password', { error: 'Passwords do not match.', token });
        }
        const passwordHash = await bcrypt.hash(password, 10);
        updateCoach(coach.email, { passwordHash, resetToken: null, resetTokenExpiry: null });
        res.redirect(BASE_PATH + '/login?reset=done');
    });

    // Coach setup — show form (from invite email)
    app.get(BASE_PATH + '/coach/setup/:token', (req, res) => {
        const coach = findCoachByToken(req.params.token, 'setupToken');
        if (!coach) {
            return res.render('coach-setup', { error: 'Invalid or expired link.', token: null, email: null });
        }
        if (new Date(coach.setupTokenExpiry) < new Date()) {
            return res.render('coach-setup', { error: 'This link has expired. Please request a new invite.', token: null, email: null });
        }
        if (coach.setupComplete) {
            return res.redirect(BASE_PATH + '/login');
        }
        res.render('coach-setup', { error: null, token: req.params.token, email: coach.email });
    });

    // Coach setup — save
    app.post(BASE_PATH + '/coach/setup/:token', async (req, res) => {
        const { token } = req.params;
        const { fullName, password, passwordConfirm } = req.body || {};
        const coach = findCoachByToken(token, 'setupToken');
        if (!coach || new Date(coach.setupTokenExpiry) < new Date()) {
            return res.render('coach-setup', { error: 'Invalid or expired link.', token: null, email: null });
        }
        if (!fullName || !fullName.trim()) {
            return res.render('coach-setup', { error: 'Full name is required.', token, email: coach.email });
        }
        if (!password || password.length < 6) {
            return res.render('coach-setup', { error: 'Password must be at least 6 characters.', token, email: coach.email });
        }
        if (password !== passwordConfirm) {
            return res.render('coach-setup', { error: 'Passwords do not match.', token, email: coach.email });
        }
        const passwordHash = await bcrypt.hash(password, 10);
        updateCoach(coach.email, { name: fullName.trim(), passwordHash, setupToken: null, setupTokenExpiry: null, setupComplete: true });
        res.redirect(BASE_PATH + '/login?setup=done');
    });

    // Sign up page — open to any coach
    app.get(BASE_PATH + '/first-run', (req, res) => res.redirect(BASE_PATH + '/signup'));
    app.get(BASE_PATH + '/signup', (req, res) => {
        if (isLoggedIn(req)) return res.redirect(BASE_PATH + '/dashboard');
        res.render('signup', { error: null });
    });

    app.post(BASE_PATH + '/signup', async (req, res) => {
        const { fullName, email, password, passwordConfirm } = req.body || {};
        if (!fullName || !fullName.trim()) {
            return res.render('signup', { error: 'Full name is required.' });
        }
        if (!email || !password || password.length < 6) {
            return res.render('signup', { error: 'Email and password (min 6 chars) are required.' });
        }
        if (password !== passwordConfirm) {
            return res.render('signup', { error: 'Passwords do not match.' });
        }
        if (findCoachByEmail(email)) {
            return res.render('signup', { error: 'An account with this email already exists.' });
        }
        const passwordHash = await bcrypt.hash(password, 10);
        const coaches = readCoaches();
        coaches.push({
            name: fullName.trim(),
            email,
            passwordHash,
            setupToken: null,
            setupTokenExpiry: null,
            setupComplete: true,
            resetToken: null,
            resetTokenExpiry: null,
            createdAt: new Date().toISOString()
        });
        writeCoaches(coaches);
        res.redirect(BASE_PATH + '/login?setup=done');
    });

    // Invite coach (bot/admin only)
    app.post(BASE_PATH + '/api/coach/invite', requireBotToken, async (req, res) => {
        const { email } = req.body;
        if (!email) return res.status(400).json({ error: 'email required' });
        if (findCoachByEmail(email)) return res.status(409).json({ error: 'Coach already exists' });

        const setupToken = generateToken();
        const coaches = readCoaches();
        coaches.push({
            email,
            passwordHash: null,
            setupToken,
            setupTokenExpiry: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(),
            setupComplete: false,
            resetToken: null,
            resetTokenExpiry: null,
            createdAt: new Date().toISOString()
        });
        writeCoaches(coaches);

        sendCoachInviteEmail(email, setupToken).catch(err =>
            console.error('[EMAIL] Coach invite failed:', err.message)
        );
        res.json({ success: true });
    });

    // ====================== DEBUG (remove after fixing) ======================
    app.get(BASE_PATH + '/debug/status', requireBotToken, (req, res) => {
        const coaches = readCoaches().map(c => ({
            email: c.email,
            setupComplete: c.setupComplete,
            hasPassword: !!c.passwordHash,
            hasResetToken: !!c.resetToken
        }));
        res.json({ coachesFile: COACHES_FILE, coaches });
    });

    app.post(BASE_PATH + '/debug/test-email', requireBotToken, async (req, res) => {
        const { to } = req.body;
        try {
            await mailer.sendMail({
                from: '"EcoBot" <info@vianexu.com>',
                to: to || 'asvm.pt@gmail.com',
                subject: 'EcoBot SMTP test',
                html: '<p>SMTP is working from cPanel.</p>'
            });
            res.json({ success: true });
        } catch (e) {
            res.json({ success: false, error: e.message, code: e.code });
        }
    });

    // Logout
    app.get(BASE_PATH + '/logout', (req, res) => {
        const sessionId = req.cookies?.sessionId;
        if (sessionId) delete sessions[sessionId];
        res.redirect(BASE_PATH + '/login');
    });

    // ====================== BOT AUTH ======================
    const BOT_TOKEN = '15579990079586356780';

    function requireBotToken(req, res, next) {
        const auth = req.headers['authorization'] || '';
        if (auth === `Bearer ${BOT_TOKEN}`) return next();
        res.status(401).json({ error: 'Unauthorized' });
    }

    // ====================== API ======================
    app.get(BASE_PATH + '/api/mentees', requireBotToken, (req, res) => {
        const menteesDir = path.join(BASE_DIR, 'src', 'db', 'mentees');

        fs.readdir(menteesDir, { withFileTypes: true }, (err, entries) => {
            if (err) return res.status(500).json({ error: 'Failed to read mentees directory' });

            const menteeSlugs = entries
                .filter(entry => entry.isDirectory())
                .map(entry => entry.name);

            res.json(menteeSlugs);
        });
    });

    app.get(BASE_PATH + '/api/mentee/:slug/:file', requireBotToken, (req, res) => {
        const { slug, file } = req.params;

        // Prevent path traversal: only allow plain alphanumeric names with hyphens/underscores
        if (!/^[a-zA-Z0-9_-]+$/.test(file)) {
            return res.status(400).json({ error: 'Invalid file name' });
        }

        const menteeDir = findMenteeFolder(slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });
        const filePath = path.join(menteeDir, `${file}.json`);

        fs.readFile(filePath, 'utf8', (err, data) => {
            if (err) return res.status(404).json({ error: 'File not found' });
            try {
                res.json(JSON.parse(data));
            } catch (e) {
                res.status(500).json({ error: 'Invalid JSON format' });
            }
        });
    });

    // Create mentee (bot)
    app.post(BASE_PATH + '/api/mentee/create', requireBotToken, async (req, res) => {
        const data = req.body;
        if (!data.leadName) return res.status(400).json({ error: 'leadName required' });

        const slug = data.leadSlug || slugify(data.leadName);
        const id   = data.id || String(Math.floor(1000000000 + Math.random() * 9000000000));
        const folderKey = `${slug}_${id}`;

        if (findMenteeFolder(slug)) {
            return res.status(409).json({ error: 'Mentee already exists', slug: folderKey });
        }

        const menteeDir = path.join(BASE_DIR, 'src', 'db', 'mentees', folderKey);
        fs.mkdirSync(path.join(menteeDir, 'documents'), { recursive: true });
        const payload = { ...data, id, leadSlug: slug, createdAt: data.createdAt || new Date().toISOString() };
        fs.writeFileSync(path.join(menteeDir, 'career-strategy.json'), JSON.stringify(payload, null, 2));

        // Create auth.json and send setup email
        if (data.email) {
            const setupToken = generateToken();
            const auth = {
                email: data.email,
                passwordHash: null,
                setupToken,
                setupTokenExpiry: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(),
                setupComplete: false
            };
            fs.writeFileSync(path.join(menteeDir, 'auth.json'), JSON.stringify(auth, null, 2));

            // Send email async (don't block response)
            sendSetupEmail(data.email, data.leadName, setupToken).catch(err => {
                console.error(`[EMAIL] Failed to send setup email to ${data.email}:`, err.message);
            });
        }

        res.json({ success: true, slug: folderKey });
    });

    // Update any mentee JSON file (bot) — generic PUT
    const BOT_WRITABLE = ['career-strategy', 'form', 'group', 'jobtracking', 'cvmaster', 'group-chat', 'journey'];

    app.put(BASE_PATH + '/api/mentee/:slug/:file', requireBotToken, async (req, res) => {
        const { slug, file } = req.params;
        if (!BOT_WRITABLE.includes(file)) return res.status(403).json({ error: 'File not writable' });

        const menteeDir = findMenteeFolder(slug) || path.join(BASE_DIR, 'src', 'db', 'mentees', slug);
        const isNew = !fs.existsSync(menteeDir);
        if (isNew) fs.mkdirSync(path.join(menteeDir, 'documents'), { recursive: true });

        const filePath = path.join(menteeDir, `${file}.json`);
        try {
            let payload = req.body;
            if (file === 'career-strategy' && fs.existsSync(filePath)) {
                const current = JSON.parse(fs.readFileSync(filePath, 'utf8'));
                payload = { ...current, ...req.body };
            }
            fs.writeFileSync(filePath, JSON.stringify(payload, null, 2));

            // New mentee → initialise empty jobtracking
            if (isNew && file === 'career-strategy') {
                const jtPath = path.join(menteeDir, 'jobtracking.json');
                if (!fs.existsSync(jtPath)) {
                    fs.writeFileSync(jtPath, JSON.stringify({ slug: `${slug}_${payload.id || ''}`, jobs: [], updatedAt: null }, null, 2));
                }
            }

            // New mentee with email → create auth.json and send setup email
            if (isNew && file === 'career-strategy' && payload.email) {
                const authPath = path.join(menteeDir, 'auth.json');
                if (!fs.existsSync(authPath)) {
                    const setupToken = generateToken();
                    fs.writeFileSync(authPath, JSON.stringify({
                        email: payload.email,
                        passwordHash: null,
                        setupToken,
                        setupTokenExpiry: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(),
                        setupComplete: false
                    }, null, 2));
                    sendSetupEmail(payload.email, payload.leadName || payload.email, setupToken)
                        .catch(err => console.error(`[EMAIL] setup email failed for ${payload.email}:`, err.message));
                }
            }

            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ error: e.message });
        }
    });

    // Upload report PDF (bot)
    app.post(BASE_PATH + '/api/mentee/:slug/report', requireBotToken, (req, res) => {
        const { slug } = req.params;
        const { pdf } = req.body;
        if (!pdf) return res.status(400).json({ error: 'pdf field required (base64)' });

        const menteeDir = findMenteeFolder(slug) || path.join(BASE_DIR, 'src', 'db', 'mentees', slug);
        if (!fs.existsSync(menteeDir)) fs.mkdirSync(menteeDir, { recursive: true });

        try {
            fs.writeFileSync(path.join(menteeDir, 'report.pdf'), Buffer.from(pdf, 'base64'));
            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ error: e.message });
        }
    });

    // Upload CV (bot) — saves to documents/ with metadata
    app.post(BASE_PATH + '/api/mentee/:slug/cv-upload', requireBotToken, (req, res) => {
        const { slug } = req.params;
        const { data, ext, type } = req.body;
        if (!data || !ext || !type) return res.status(400).json({ error: 'data, ext, type required' });

        const baseDir = findMenteeFolder(slug) || path.join(BASE_DIR, 'src', 'db', 'mentees', slug);
        const docsDir = path.join(baseDir, 'documents');
        if (!fs.existsSync(docsDir)) fs.mkdirSync(docsDir, { recursive: true });

        try {
            const buf = Buffer.from(data, 'base64');
            const docId = crypto.randomUUID();
            const filename = `${docId}.${ext}`;
            fs.writeFileSync(path.join(docsDir, filename), buf);
            let clientName = slug.split('_')[0].replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
            try { const cs = JSON.parse(fs.readFileSync(path.join(baseDir, 'career-strategy.json'), 'utf8')); clientName = cs.leadName || clientName; } catch {}
            const mimeType = ext === 'pdf' ? 'application/pdf' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
            const displayName = `${clientName} - ${type === 'linkedin' ? 'LinkedIn CV' : 'Personal CV'}`;
            const docs = readDocIndex(baseDir);
            docs.unshift({ id: docId, filename, displayName, originalName: `cv-${type}.${ext}`, mimeType, size: buf.length, uploadedBy: 'bot', uploadedAt: new Date().toISOString() });
            writeDocIndex(baseDir, docs);
            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ error: e.message });
        }
    });

    // Upload binary in chunks (avoids body size limits)
    // ====================== DOCUMENTS HELPERS ======================

    const docUploadBuffer = {}; // tempId → { buf, mimeType, originalName, menteeDir, clientName }

    function readDocIndex(menteeDir) {
        const p = path.join(menteeDir, 'documents', '_index.json');
        if (!fs.existsSync(p)) return [];
        try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return []; }
    }

    function writeDocIndex(menteeDir, docs) {
        const dir = path.join(menteeDir, 'documents');
        if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
        fs.writeFileSync(path.join(dir, '_index.json'), JSON.stringify(docs, null, 2));
    }

    async function aiNameDocument(clientName, buf, mimeType, originalName) {
        const b64 = buf.toString('base64');
        const prompt = `You are filing a document for "${clientName}" in a career coaching system. Identify what this document is and return ONLY a display name in this format: "${clientName} - [3-6 word description]". Examples: "${clientName} - Personal CV", "${clientName} - B2 German Certificate", "${clientName} - LinkedIn Profile PDF". Return ONLY the name, nothing else.`;
        let content;
        if (mimeType === 'application/pdf') {
            content = [
                { type: 'document', source: { type: 'base64', media_type: 'application/pdf', data: b64 } },
                { type: 'text', text: prompt }
            ];
        } else if (mimeType && mimeType.startsWith('image/')) {
            content = [
                { type: 'image', source: { type: 'base64', media_type: mimeType, data: b64 } },
                { type: 'text', text: prompt }
            ];
        } else {
            const base = (originalName || '').replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
            return `${clientName} - ${base}`;
        }
        const r = await fetch('https://api.anthropic.com/v1/messages', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'x-api-key': ANTHROPIC_KEY, 'anthropic-version': '2023-06-01' },
            body: JSON.stringify({ model: 'claude-haiku-4-5', max_tokens: 80, messages: [{ role: 'user', content }] })
        });
        const d = await r.json();
        if (!r.ok) throw new Error(d.error?.message || 'Claude error');
        return d.content[0].text.trim().replace(/^["']|["']$/g, '');
    }

    // POST /api/mentee/:slug/upload-chunk  { id, index, total, ext, type, data: base64chunk }
    // POST /api/mentee/:slug/upload-finalize { id, ext, type }
    const uploadChunks = {}; // in-memory assembly buffer (small files, short-lived)

    app.post(BASE_PATH + '/api/mentee/:slug/upload-chunk', requireBotToken, (req, res) => {
        const { slug } = req.params;
        const { id, index, total, ext, type, data } = req.body;
        if (!id || index == null || !total || !ext || !type || !data)
            return res.status(400).json({ error: 'id, index, total, ext, type, data required' });
        if (!uploadChunks[id]) uploadChunks[id] = { chunks: [], ext, type, slug };
        uploadChunks[id].chunks[index] = data;
        res.json({ success: true, received: index });
    });

    app.post(BASE_PATH + '/api/mentee/:slug/upload-finalize', requireBotToken, (req, res) => {
        const { slug } = req.params;
        const { id, ext, type } = req.body;
        if (!id || !ext || !type) return res.status(400).json({ error: 'id, ext, type required' });
        const session = uploadChunks[id];
        if (!session) return res.status(404).json({ error: 'Upload session not found' });
        try {
            const combined = Buffer.from(session.chunks.join(''), 'base64');
            const baseDir = findMenteeFolder(slug) || path.join(BASE_DIR, 'src', 'db', 'mentees', slug);
            const docsDir = path.join(baseDir, 'documents');
            if (!fs.existsSync(docsDir)) fs.mkdirSync(docsDir, { recursive: true });
            const docId = crypto.randomUUID();
            const filename = `${docId}.${ext}`;
            fs.writeFileSync(path.join(docsDir, filename), combined);
            let clientName = slug.split('_')[0].replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
            try { const cs = JSON.parse(fs.readFileSync(path.join(baseDir, 'career-strategy.json'), 'utf8')); clientName = cs.leadName || clientName; } catch {}
            const mimeType = ext === 'pdf' ? 'application/pdf' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
            const displayName = `${clientName} - ${type === 'linkedin' ? 'LinkedIn CV' : 'Personal CV'}`;
            const docs = readDocIndex(baseDir);
            docs.unshift({ id: docId, filename, displayName, originalName: `cv-${type}.${ext}`, mimeType, size: combined.length, uploadedBy: 'bot', uploadedAt: new Date().toISOString() });
            writeDocIndex(baseDir, docs);
            delete uploadChunks[id];
            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ error: e.message });
        }
    });

    // ====================== ADMIN PROTECTED ROUTES ======================

    // Dashboard
    app.get(BASE_PATH + '/dashboard', requireAuth, (req, res) => {
        const sessionId = req.cookies.sessionId;
        const username = sessions[sessionId]?.email || 'Coach';

        const coaches = readCoaches();
        const currentCoach = coaches.find(c => c.email.toLowerCase() === username.toLowerCase());
        const coachName = currentCoach?.name || username.split('@')[0];
        const coachList = coaches.filter(c => c.name).map(c => ({ name: c.name, email: c.email }));

        const menteesDir = path.join(BASE_DIR, 'src', 'db', 'mentees');

        fs.readdir(menteesDir, { withFileTypes: true }, (err, entries) => {
            const menteeSlugs = !err ? entries.filter(e => e.isDirectory()).map(e => e.name) : [];

            if (menteeSlugs.length === 0) {
                return res.render('dashboard', { username, coachName, coaches: coachList, mentees: [] });
            }

            const mentees = [];
            let loaded = 0;

            menteeSlugs.forEach(slug => {
                const filePath = path.join(menteesDir, slug, 'career-strategy.json');
                fs.readFile(filePath, 'utf8', (err, data) => {
                    loaded++;
                    if (!err) {
                        try {
                            const json = JSON.parse(data);
                            let entryDate = json.entryDate;
                            let interviewCount = 0, offerCount = 0;
                            try {
                                const jt = JSON.parse(fs.readFileSync(path.join(menteesDir, slug, 'jobtracking.json'), 'utf8'));
                                const jtJobs = jt.jobs || [];
                                if (!entryDate) {
                                    const dates = jtJobs.map(j => j.foundAt).filter(Boolean).sort();
                                    entryDate = dates[0] || json.createdAt || new Date().toISOString();
                                }
                                interviewCount = jtJobs.filter(j => j.status === 'interview').length;
                                offerCount     = jtJobs.filter(j => j.status === 'offer' || j.status === 'offer_received' || j.status === 'offer_accepted').length;
                            } catch { if (!entryDate) entryDate = json.createdAt || new Date().toISOString(); }
                            if (!json.entryDate && entryDate) {
                                try { json.entryDate = entryDate; fs.writeFileSync(filePath, JSON.stringify(json, null, 2)); } catch {}
                            }
                            mentees.push({
                                slug,
                                name: json.leadName || slug.replace(/-/g, ' '),
                                email: json.email || '',
                                plan: json.plan || 'silver',
                                status: json.status || 'active',
                                coach: json.coach || '',
                                entryDate,
                                interviewCount,
                                offerCount,
                            });
                        } catch (e) {
                            mentees.push({ slug, name: slug.replace(/-/g, ' '), email: '', plan: 'silver', status: 'active', coach: '' });
                        }
                    } else {
                        mentees.push({ slug, name: slug.replace(/-/g, ' '), email: '', plan: 'silver', status: 'active', coach: '' });
                    }

                    if (loaded === menteeSlugs.length) {
                        mentees.sort((a, b) => {
                            const da = a.entryDate ? new Date(a.entryDate).getTime() : 0;
                            const db = b.entryDate ? new Date(b.entryDate).getTime() : 0;
                            return db - da; // most recent first
                        });
                        res.render('dashboard', { username, coachName, coaches: coachList, mentees });
                    }
                });
            });
        });
    });

    // Daily Jobs
    app.get(BASE_PATH + '/dailyjobs', (req, res, next) => {
        const tok = req.query._tok || (req.headers['authorization'] || '').replace('Bearer ', '');
        if (tok && tok === BOT_TOKEN) return next();
        return requireAuth(req, res, next);
    }, (req, res) => {
        const menteesDir = path.join(BASE_DIR, 'src', 'db', 'mentees');
        fs.readdir(menteesDir, { withFileTypes: true }, (err, entries) => {
            const slugs = !err ? entries.filter(e => e.isDirectory()).map(e => e.name) : [];
            if (!slugs.length) return res.render('dailyjobs', { mentees: [] });
            const mentees = [];
            let loaded = 0;
            slugs.forEach(slug => {
                let cs = {}, jt = null;
                try { cs = JSON.parse(fs.readFileSync(path.join(menteesDir, slug, 'career-strategy.json'), 'utf8')); } catch {}
                try { jt = JSON.parse(fs.readFileSync(path.join(menteesDir, slug, 'jobtracking.json'), 'utf8')); } catch {}
                loaded++;
                if ((cs.status || 'active') === 'active') {
                    const seg = (cs.strategy && cs.strategy.segments && cs.strategy.segments[0]) || {};
                    const roles = seg.targetRoles || cs.primaryRoles || cs.targetRoleFamilies || [];
                    const geos  = seg.targetGeographies || cs.priorityCities || cs.targetGeographies || (cs.primaryCountry ? [cs.primaryCountry] : []);
                    mentees.push({ slug, name: cs.leadName || slug.replace(/-/g,' '), plan: cs.plan || 'silver', roles, geos });
                }
                if (loaded === slugs.length) res.render('dailyjobs', { mentees });
            });
        });
    });

    // Profile
    app.get(BASE_PATH + '/profile/:slug', requireAuth, (req, res) => {
        const sessionId = req.cookies.sessionId;
        const username = sessions[sessionId]?.email || 'Coach';
        const slug = req.params.slug;

        const menteesDir = findMenteeFolder(slug) || path.join(BASE_DIR, 'src', 'db', 'mentees', slug);
        const files = ['career-strategy', 'form'];
        const data = {};
        let loaded = 0;

        const finish = () => {
            // CV: prefer cvmaster.json (generated by bot), fall back to cv.json (legacy)
            const cvmasterPath = path.join(menteesDir, 'cvmaster.json');
            const cvPath       = path.join(menteesDir, 'cv.json');
            let cvRaw = null;
            try { cvRaw = fs.readFileSync(cvmasterPath, 'utf8'); } catch {}
            if (!cvRaw) { try { cvRaw = fs.readFileSync(cvPath, 'utf8'); } catch {} }
            try { data.cv = cvRaw ? JSON.parse(cvRaw) : {}; } catch { data.cv = {}; }

            let jobtracking = null;
            try { jobtracking = JSON.parse(fs.readFileSync(path.join(menteesDir, 'jobtracking.json'), 'utf8')); } catch {}

            const cs = data['career-strategy'] || {};
            if (!cs.entryDate) {
                const dates = ((jobtracking && jobtracking.jobs) || []).map(j => j.foundAt).filter(Boolean).sort();
                cs.entryDate = dates[0] || cs.createdAt || new Date().toISOString();
                try {
                    const csPath = path.join(menteesDir, 'career-strategy.json');
                    const existing = JSON.parse(fs.readFileSync(csPath, 'utf8'));
                    existing.entryDate = cs.entryDate;
                    fs.writeFileSync(csPath, JSON.stringify(existing, null, 2));
                } catch {}
            }

            const documents = readDocIndex(menteesDir);

            res.render('profile', {
                username,
                slug,
                careerStrategy: cs,
                cv: data.cv,
                form: data.form,
                jobtracking,
                documents,
                anthropicKey: ANTHROPIC_KEY
            });
        };

        files.forEach(file => {
            fs.readFile(path.join(menteesDir, `${file}.json`), 'utf8', (err, fileData) => {
                loaded++;
                try {
                    data[file] = err ? {} : JSON.parse(fileData);
                } catch (e) {
                    data[file] = {};
                }
                if (loaded === files.length) finish();
            });
        });
    });

    // Update Profile
    app.put(BASE_PATH + '/api/profile/:slug', requireAnyAuth, (req, res) => {
        const slug = req.params.slug;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ success: false, error: 'Access denied' });
        const updates = req.body;
        const menteeDir = findMenteeFolder(slug) || path.join(BASE_DIR, 'src', 'db', 'mentees', slug);
        const filePath = path.join(menteeDir, 'career-strategy.json');

        fs.readFile(filePath, 'utf8', (err, data) => {
            if (err) return res.status(500).json({ success: false, error: 'Failed to read profile' });

            try {
                const careerStrategy = JSON.parse(data);
                const oldData = { ...careerStrategy };

                // If updates looks like a full AI-replaced profile (has leadId or leadName as top-level keys), replace entirely
                if (updates.leadId !== undefined || updates.leadName !== undefined) {
                    const savedEntryDate = careerStrategy.entryDate;
                    Object.assign(careerStrategy, updates);
                    if (savedEntryDate) careerStrategy.entryDate = savedEntryDate;
                } else {
                    Object.keys(updates).filter(f => f !== 'entryDate').forEach(field => {
                        const value = updates[field];
                        const parts = field.split('.');
                        if (parts.length === 1) {
                            careerStrategy[field] = value;
                        } else {
                            // Handle arbitrary nesting depth (e.g. strategy.segments.0.description)
                            let obj = careerStrategy;
                            for (let i = 0; i < parts.length - 1; i++) {
                                const key = parts[i];
                                if (obj[key] === undefined || obj[key] === null || typeof obj[key] !== 'object') {
                                    obj[key] = isNaN(parts[i + 1]) ? {} : [];
                                }
                                obj = obj[key];
                            }
                            obj[parts[parts.length - 1]] = value;
                        }
                    });
                }

                fs.writeFile(filePath, JSON.stringify(careerStrategy, null, 2), 'utf8', (err) => {
                    if (err) return res.status(500).json({ success: false, error: 'Failed to save data' });
                    const actorName = getActorName(req);
                    const text = describeUpdates(actorName, updates, oldData);
                    appendChangelog(menteeDir, { who: isLoggedIn(req) ? 'coach' : 'client', text, action: 'profile_edit', fields: Object.keys(updates) });
                    res.json({ success: true, message: 'Profile updated successfully' });
                });
            } catch (e) {
                res.status(500).json({ success: false, error: 'Invalid data format' });
            }
        });
    });

    // Update plan and status
    app.patch(BASE_PATH + '/api/admin/profile/:slug/plan-status', requireAuth, (req, res) => {
        const slug = req.params.slug;
        const { plan, status, inactiveReason, courseDone } = req.body;
        const menteeDir = findMenteeFolder(slug) || path.join(BASE_DIR, 'src', 'db', 'mentees', slug);
        const filePath = path.join(menteeDir, 'career-strategy.json');
        fs.readFile(filePath, 'utf8', (err, data) => {
            if (err) return res.status(500).json({ success: false, error: 'Failed to read profile' });
            try {
                const cs = JSON.parse(data);
                const wasCourseDone = cs.courseDone === true;
                if (plan) cs.plan = plan;
                if (status) {
                    cs.status = status;
                    cs.inactiveReason = status === 'inactive' ? (inactiveReason || '') : '';
                }
                if (typeof courseDone === 'boolean') cs.courseDone = courseDone;
                fs.writeFile(filePath, JSON.stringify(cs, null, 2), 'utf8', err2 => {
                    if (err2) return res.status(500).json({ success: false, error: 'Failed to save' });
                    // Send email when course is marked complete for the first time
                    if (courseDone === true && !wasCourseDone && cs.email) {
                        const firstName = (cs.leadName || 'there').split(' ')[0];
                        sendCourseCompleteEmail(cs.email, firstName).catch(e => {
                            console.error('[course-complete] email error:', e.message);
                        });
                    }
                    res.json({ success: true });
                });
            } catch (e) {
                res.status(500).json({ success: false, error: 'Invalid data' });
            }
        });
    });

    // Save CV (cvmaster) from admin UI
    app.put(BASE_PATH + '/api/profile/:slug/cv', requireAnyAuth, (req, res) => {
        const slug = req.params.slug;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ success: false, error: 'Access denied' });
        const menteeDir = findMenteeFolder(slug) || path.join(BASE_DIR, 'src', 'db', 'mentees', slug);
        // Write to cvmaster.json (canonical); also write cv.json for legacy compat
        const updates = req.body;
        const cvmasterPath = path.join(menteeDir, 'cvmaster.json');
        let existing = {};
        try { existing = JSON.parse(fs.readFileSync(cvmasterPath, 'utf8')); } catch {
            // try legacy cv.json
            try { existing = JSON.parse(fs.readFileSync(path.join(menteeDir, 'cv.json'), 'utf8')); } catch {}
        }
        // Handle dot-notation paths (e.g. "experience.0.title", "experience.0.highlights")
        Object.keys(updates).forEach(field => {
            const value = updates[field];
            const parts = field.split('.');
            if (parts.length === 1) {
                existing[field] = value;
            } else {
                let obj = existing;
                for (let i = 0; i < parts.length - 1; i++) {
                    const key = parts[i];
                    if (obj[key] === undefined || obj[key] === null || typeof obj[key] !== 'object') {
                        obj[key] = isNaN(parts[i + 1]) ? {} : [];
                    }
                    obj = obj[key];
                }
                obj[parts[parts.length - 1]] = value;
            }
        });
        try {
            fs.writeFileSync(cvmasterPath, JSON.stringify(existing, null, 2));
            // Keep cv.json in sync for legacy compat
            const cvLegacyPath = path.join(menteeDir, 'cv.json');
            if (fs.existsSync(cvLegacyPath)) fs.writeFileSync(cvLegacyPath, JSON.stringify(existing, null, 2));
            const actorName = getActorName(req);
            appendChangelog(menteeDir, { who: isLoggedIn(req) ? 'coach' : 'client', text: `${actorName} updated CV (${Object.keys(updates).join(', ')})`, action: 'cv_edit', fields: Object.keys(updates) });
            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ success: false, error: 'Failed to save CV' });
        }
    });

    // Update a single job in jobtracking from admin UI
    app.patch(BASE_PATH + '/api/profile/:slug/jobtracking/:jobId', requireAnyAuth, (req, res) => {
        const { slug, jobId } = req.params;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ success: false, error: 'Access denied' });
        const updates = req.body;
        const menteeDir = findMenteeFolder(slug) || path.join(BASE_DIR, 'src', 'db', 'mentees', slug);
        const filePath = path.join(menteeDir, 'jobtracking.json');
        fs.readFile(filePath, 'utf8', (err, data) => {
            if (err) return res.status(404).json({ success: false, error: 'Jobtracking not found' });
            try {
                const jt = JSON.parse(data);
                const job = (jt.jobs || []).find(j => j.id === jobId);
                if (!job) return res.status(404).json({ success: false, error: 'Job not found' });
                const oldStatus = job.status;
                Object.assign(job, updates);
                jt.updatedAt = new Date().toISOString();
                fs.writeFile(filePath, JSON.stringify(jt, null, 2), 'utf8', err => {
                    if (err) return res.status(500).json({ success: false, error: 'Failed to save' });
                    const actorName = getActorName(req);
                    const statusPart = updates.status && updates.status !== oldStatus ? ` to '${updates.status}'` : '';
                    const text = `${actorName} updated job '${job.title} at ${job.company}'${statusPart}`;
                    appendChangelog(menteeDir, { who: isLoggedIn(req) ? 'coach' : 'client', text, action: 'job_edit', jobId, fields: Object.keys(updates) });
                    res.json({ success: true });
                });
            } catch (e) {
                res.status(500).json({ success: false, error: 'Invalid data' });
            }
        });
    });

    // Save Form answers from admin UI
    app.put(BASE_PATH + '/api/profile/:slug/form', requireAnyAuth, (req, res) => {
        const slug = req.params.slug;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ success: false, error: 'Access denied' });
        const menteeDir = findMenteeFolder(slug) || path.join(BASE_DIR, 'src', 'db', 'mentees', slug);
        const updates = req.body; // { answers: { key: value, ... } }
        const formPath = path.join(menteeDir, 'form.json');
        let existing = { answers: {} };
        try { existing = JSON.parse(fs.readFileSync(formPath, 'utf8')); } catch {}
        if (!existing.answers) existing.answers = {};
        if (updates.answers) Object.assign(existing.answers, updates.answers);
        try {
            fs.writeFileSync(formPath, JSON.stringify(existing, null, 2));
            const actorName = getActorName(req);
            const keys = updates.answers ? Object.keys(updates.answers) : [];
            appendChangelog(menteeDir, { who: isLoggedIn(req) ? 'coach' : 'client', text: `${actorName} updated form answers (${keys.join(', ')})`, action: 'form_edit', fields: keys });
            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ success: false, error: 'Failed to save form' });
        }
    });

    // AI-assisted profile update
    app.post(BASE_PATH + '/api/profile/:slug/ai-update', requireAnyAuth, async (req, res) => {
        const slug = req.params.slug;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ success: false, error: 'Access denied' });
        const { formText } = req.body;
        if (!formText || !formText.trim()) {
            return res.status(400).json({ success: false, error: 'No text provided' });
        }

        const menteeDir = findMenteeFolder(slug) || path.join(BASE_DIR, 'src', 'db', 'mentees', slug);
        const filePath = path.join(menteeDir, 'career-strategy.json');

        let existing;
        try {
            existing = JSON.parse(fs.readFileSync(filePath, 'utf8'));
        } catch {
            return res.status(500).json({ success: false, error: 'Failed to read profile' });
        }

        const prompt = `You are a career coaching assistant. You will receive an existing client profile in JSON format and new information pasted from a form or notes. Your task is to update the JSON by merging the new information: add missing fields, update outdated values, and preserve existing data that is not contradicted. Return ONLY valid JSON, no explanation.

    EXISTING PROFILE:
    ${JSON.stringify(existing, null, 2)}

    NEW INFORMATION:
    ${formText}

    Return the complete updated JSON object.`;

        try {
            const response = await fetch('https://api.anthropic.com/v1/messages', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'x-api-key': ANTHROPIC_KEY, 'anthropic-version': '2023-06-01'
                },
                body: JSON.stringify({ model: 'claude-haiku-4-5', max_tokens: 1024, messages: [{ role: 'user', content: prompt }] })
            });

            const aiData = await response.json();
            if (!response.ok) {
                return res.status(500).json({ success: false, error: aiData.error?.message || 'Claude error' });
            }

            let content = aiData.content[0].text.trim();
            // Strip markdown code block if present
            content = content.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');

            const updated = JSON.parse(content);
            fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), 'utf8');
            const actorName = getActorName(req);
            appendChangelog(menteeDir, { who: isLoggedIn(req) ? 'coach' : 'client', text: `${actorName} updated profile via AI assistant`, action: 'ai_update' });
            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ success: false, error: 'AI processing failed: ' + e.message });
        }
    });

    // Generate Career Plan (Job Hunting Strategy)
    app.post(BASE_PATH + '/api/profile/:slug/generate-career-plan', requireAnyAuth, async (req, res) => {
        const slug = req.params.slug;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ success: false, error: 'Access denied' });

        const {
            linkedinUrl,
            targetRoles = [],
            targetGeographies = [],
            targetSectors = [],
            targetCompanies = [],
            salaryRange = '',
            vision = '',
            languages = [],
            nonNegotiables = ''
        } = req.body;

        if (!linkedinUrl || !targetRoles.length) {
            return res.status(400).json({ success: false, error: 'LinkedIn URL and target roles required' });
        }

        const menteeDir = findMenteeFolder(slug) || path.join(BASE_DIR, 'src', 'db', 'mentees', slug);
        const filePath = path.join(menteeDir, 'career-strategy.json');

        const prompt = `You are a senior career strategist with 20+ years advising mid-to-senior professionals on career positioning. Build a Career Plan for the candidate below.

Output sections (in this order):
- Career objective (1-2 sentences)
- Target positioning statement (3 lines max, defensible in interviews)
- Target roles ranked by fit (max 3)
- Target sectors and geographies
- Target companies
- Transferable strengths (3-5 bullets, evidence-based)
- Gap analysis (2-3 honest gaps and how to close them)
- 30/60/90-day action plan (specific weekly actions)
- 3 main risks and how to mitigate them

Rules:
- No clichés (avoid "passionate", "dynamic", "results-oriented", "team player")
- No invented experience
- Every claim must be defensible in a real interview
- Direct, structured, analytical tone
- Maximum 600 words total

Candidate information:
LinkedIn/CV URL: ${linkedinUrl}
Target roles: ${targetRoles.join(', ')}
Target sectors: ${targetSectors.join(', ') || 'Not specified'}
Target geographies: ${targetGeographies.join(', ')}
Target companies: ${targetCompanies.join(', ') || 'Not specified'}
Salary range: ${salaryRange || 'Not specified'}
Languages: ${Array.isArray(languages) ? languages.map(l => typeof l === 'string' ? l : (l.language + ' (' + l.level + ')')).join(', ') : 'Not specified'}
Non-negotiables: ${nonNegotiables || 'Not specified'}
3-year vision: ${vision || 'Not specified'}

Generate the career plan now.`;

        try {
            const response = await fetch('https://api.anthropic.com/v1/messages', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'x-api-key': ANTHROPIC_KEY,
                    'anthropic-version': '2023-06-01'
                },
                body: JSON.stringify({
                    model: 'claude-sonnet-4-6',
                    max_tokens: 2048,
                    messages: [{ role: 'user', content: prompt }]
                })
            });

            const aiData = await response.json();
            if (!response.ok) {
                return res.status(500).json({ success: false, error: aiData.error?.message || 'Claude error' });
            }

            const careerPlan = aiData.content[0].text.trim();

            // Save to career-strategy.json
            let existing;
            try {
                existing = JSON.parse(fs.readFileSync(filePath, 'utf8'));
            } catch {
                return res.status(500).json({ success: false, error: 'Failed to read profile' });
            }

            existing.careerPlan = careerPlan;
            fs.writeFileSync(filePath, JSON.stringify(existing, null, 2), 'utf8');

            const actorName = getActorName(req);
            appendChangelog(menteeDir, {
                who: isLoggedIn(req) ? 'coach' : 'client',
                text: `${actorName} generated career plan`,
                action: 'career_plan_generated'
            });

            res.json({ success: true, careerPlan: careerPlan });
        } catch (e) {
            res.status(500).json({ success: false, error: 'Career plan generation failed: ' + e.message });
        }
    });

    // ====================== ADMIN PROFILE CREATION ======================

    // Quick extract: receive plain CV text (parsed client-side) and return basic fields to auto-fill the form
    app.post(BASE_PATH + '/api/admin/cv-extract', requireAuth, async (req, res) => {
        const { cvText } = req.body;
        if (!cvText || !cvText.trim()) return res.status(400).json({ error: 'cvText required' });

        const prompt = `Extract the following fields from this CV. Return ONLY a JSON object with exactly these keys:
{
  "leadName": "full name",
  "email": "email address or empty string",
  "phone": "phone number or empty string",
  "shortDescription": "one sentence summarising their professional profile (always write in English)"
}

CV TEXT:
${cvText.slice(0, 8000)}`;

        try {
            const r = await fetch('https://api.anthropic.com/v1/messages', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'x-api-key': ANTHROPIC_KEY, 'anthropic-version': '2023-06-01' },
                body: JSON.stringify({ model: 'claude-haiku-4-5', max_tokens: 1024, messages: [{ role: 'user', content: prompt }] })
            });
            const d = await r.json();
            if (!r.ok) return res.status(500).json({ error: d.error?.message || 'Claude error' });
            let content = d.content[0].text.trim().replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
            res.json({ success: true, fields: JSON.parse(content) });
        } catch (e) {
            res.status(500).json({ error: 'AI extraction failed: ' + e.message });
        }
    });

    // Generate full profile: receives text already extracted client-side
    app.post(BASE_PATH + '/api/admin/profile-preview', requireAuth, async (req, res) => {
        const { name, email, phone, description, cvText = '', linkedinText = '' } = req.body;

        const hasDocuments = cvText.trim() || linkedinText.trim();

        // No CV uploaded: return basic profile (manual mode)
        if (!hasDocuments) {
            if (!name || !name.trim()) return res.status(400).json({ error: 'name required' });
            return res.json({
                success: true, aiGenerated: false,
                profile: { leadName: name.trim(), email: email || '', phone: phone || '', shortDescription: description || '' }
            });
        }

        const docsSection = [
            cvText ? `=== CV ===\n${cvText.slice(0, 8000)}` : '',
            linkedinText ? `=== LINKEDIN CV ===\n${linkedinText.slice(0, 4000)}` : ''
        ].filter(Boolean).join('\n\n');

        const hints = [
            name ? `Name hint: ${name}` : '',
            email ? `Email hint: ${email}` : '',
            phone ? `Phone hint: ${phone}` : '',
            description ? `Description hint: ${description}` : ''
        ].filter(Boolean).join('\n');

        const prompt = `You are a career coaching assistant. Extract information from the CV below and generate a comprehensive career strategy profile as a JSON object. ${hints ? `Use these hints from the coach if CV is ambiguous:\n${hints}\n` : ''}

${docsSection}

Return ONLY a valid JSON object with these fields (extract from CV content, use English field names):
{
  "leadName": "full name from CV",
  "email": "email address from CV",
  "phone": "phone number from CV",
  "shortDescription": "1-2 sentence career positioning summary",
  "location": "city, country",
  "primaryCountry": "country",
  "nationality": "nationality if inferable",
  "linkedin": "LinkedIn URL if found",
  "seniority": "e.g. Director / Head, Senior Manager, VP, C-Level, Mid-Level",
  "salaryTarget": "salary range if mentioned, otherwise empty string",
  "primaryRoles": ["array of primary target job titles"],
  "secondaryRoles": ["other relevant roles"],
  "targetSectors": ["industries or sectors"],
  "priorityCities": ["preferred cities for work"],
  "languages": [{"language": "English", "level": "Native"}],
  "gapAnalysis": { "strengths": ["3-5 key professional strengths extracted from CV"] },
  "strategy": {
    "segments": [{ "id": "s1", "label": "Primary Track", "targetRoles": [], "targetSectors": [], "targetGeographies": [] }]
  }
}`;

        try {
            const response = await fetch('https://api.anthropic.com/v1/messages', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'x-api-key': ANTHROPIC_KEY, 'anthropic-version': '2023-06-01' },
                body: JSON.stringify({ model: 'claude-haiku-4-5', max_tokens: 2000, messages: [{ role: 'user', content: prompt }] })
            });
            const aiData = await response.json();
            if (!response.ok) return res.status(500).json({ error: aiData.error?.message || 'Claude error' });
            let content = aiData.content[0].text.trim().replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
            res.json({ success: true, aiGenerated: true, profile: JSON.parse(content) });
        } catch (e) {
            res.status(500).json({ error: 'AI processing failed: ' + e.message });
        }
    });

    // ── Form pre-fill helper ────────────────────────────────────────────
    async function generateFormJson(menteeDir, profile, cvText) {
        const sectors = Array.isArray(profile.targetSectors)
            ? profile.targetSectors
            : [...(profile.targetSectors?.tier1 || []), ...(profile.targetSectors?.tier2 || [])];
        const roles = (profile.primaryRoles || profile.targetRoleFamilies || []);

        // Map profile data to QUESTIONS keys
        const form = {
            completedAt: null,
            formValidated: false,
            answers: {
                current_position:         null,
                current_area:             null,
                annual_bonus:             null,
                desired_area:             roles.join(' / ') || null,
                preferred_sectors:        sectors.join(', ') || null,
                preferred_companies:      null,
                open_roles:               roles.join(', ') || null,
                desired_salary:           profile.salaryTarget || null,
                date_of_birth:            null,
                address:                  profile.location || null,
                phone:                    profile.phone || null,
                linkedin_url:             profile.linkedin || null,
                nationality:              null,
                relocation_remote:        profile.openToRelocate != null ? String(profile.openToRelocate) : null,
                professional_goal_6_12m:  profile.shortDescription || null,
                biggest_challenge:        null,
                updated_cv:               null,
                linkedin_updated:         null,
                professional_perception:  null,
                used_ai_tools:            null,
                interested_in_ai:         null,
                interview_confidence:     null,
                salary_negotiation:       null,
                weekly_hours:             null,
                additional_notes:         null,
                target_applications_week: null,
                preferred_company_type:   null,
                job_search_start_date:    null
            }
        };

        // If CV text available, use AI to enrich fields extractable from the CV
        if (cvText && cvText.trim()) {
            try {
                const prompt = `Extract the following fields from this CV text and return ONLY a JSON object. Use null for fields you cannot determine.

{
  "current_position": "most recent job title and company (e.g. 'VP Finance at Acme Corp, 2022–present')",
  "current_area": "professional area/function (e.g. 'Finance & Accounting', 'Human Resources', 'Technology')",
  "nationality": "nationality if explicitly mentioned in the CV, else null",
  "date_of_birth": "date of birth if mentioned (YYYY-MM-DD format), else null"
}

CV TEXT:
${cvText.slice(0, 6000)}

Reply ONLY with the JSON object. No markdown. No explanation.`;

                const raw = await callClaude([{ role: 'user', content: prompt }], 300);
                const cleaned = raw.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
                const enriched = JSON.parse(cleaned);
                Object.entries(enriched).forEach(([k, v]) => {
                    if (v && form.answers[k] === null) form.answers[k] = v;
                });
            } catch (e) {
                console.error('[FORM] AI enrichment failed:', e.message);
            }
        }

        fs.writeFileSync(path.join(menteeDir, 'form.json'), JSON.stringify(form, null, 2));
    }

    // ── Career Dossier (cvmaster) auto-generation from CV ───────────────
    async function generateDossier(menteeDir, cvText) {
        if (!cvText || !cvText.trim()) return;
        try {
            const prompt = `You are a professional CV analyst. Extract structured data from the CV text below and return a JSON object in exactly this format. Be thorough and accurate.

{
  "headline": "one-line professional headline (e.g. 'Senior Finance Executive | CFO | M&A Specialist')",
  "narrative": "3-5 sentence professional summary in third person",
  "experience": [
    {
      "title": "Job title",
      "company": "Company name",
      "location": "City, Country",
      "from": "Month Year or Year",
      "to": "Month Year or Year or Present",
      "highlights": ["Achievement or responsibility 1", "Achievement or responsibility 2"]
    }
  ],
  "education": [
    {
      "degree": "Degree name",
      "institution": "Institution name",
      "year": "Year or year range"
    }
  ],
  "skills": ["Skill 1", "Skill 2"],
  "languages": [
    { "language": "Language name", "level": "Native / C2 / B2 / etc." }
  ],
  "certifications": ["Certification 1"],
  "achievements": ["Key achievement 1", "Key achievement 2"]
}

CV TEXT:
${cvText.slice(0, 8000)}

Reply ONLY with the JSON object. No markdown. No explanation.`;

            const raw = await callClaude([{ role: 'user', content: prompt }], 2500);
            const cleaned = raw.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
            const dossier = JSON.parse(cleaned);
            dossier.generatedAt = new Date().toISOString();
            dossier.source = 'cv-extract';
            fs.writeFileSync(path.join(menteeDir, 'cvmaster.json'), JSON.stringify(dossier, null, 2));
            console.log('[DOSSIER] Generated cvmaster.json for', menteeDir);
        } catch (e) {
            console.error('[DOSSIER] generation failed:', e.message);
        }
    }

    // Create mentee from admin dashboard (coach-initiated)
    app.post(BASE_PATH + '/api/admin/mentee/create', requireAuth, async (req, res) => {
        const { profile, cvFiles, cvText } = req.body; // cvFiles: [{ data: base64, ext: 'pdf', type: 'cv' }, ...]
        if (!profile || !profile.leadName) return res.status(400).json({ error: 'profile.leadName required' });

        const slug = slugify(profile.leadName);
        if (!slug) return res.status(400).json({ error: 'Invalid name (could not generate slug)' });

        if (findMenteeFolder(slug)) return res.status(409).json({ error: 'A mentee with this name already exists' });

        // Use provided hotmartId or fall back to random
        let id;
        if (profile.hotmartId && String(profile.hotmartId).trim()) {
            id = String(profile.hotmartId).trim();
            // Check no existing folder already uses this Hotmart ID
            const menteesDir = path.join(BASE_DIR, 'src', 'db', 'mentees');
            try {
                const existing = fs.readdirSync(menteesDir).find(f => f.endsWith('_' + id));
                if (existing) return res.status(409).json({ error: 'A mentee with this Hotmart ID already exists (' + existing + ')' });
            } catch {}
        } else {
            id = String(Math.floor(1000000000 + Math.random() * 9000000000));
        }
        const folderKey = `${slug}_${id}`;
        const menteeDir = path.join(BASE_DIR, 'src', 'db', 'mentees', folderKey);
        const docsDir = path.join(menteeDir, 'documents');
        fs.mkdirSync(docsDir, { recursive: true });

        const payload = {
            ...profile, id, leadSlug: slug,
            createdAt: new Date().toISOString(),
            entryDate: new Date().toISOString(),
            formValidated: false,
            dossierValidated: false
        };
        fs.writeFileSync(path.join(menteeDir, 'career-strategy.json'), JSON.stringify(payload, null, 2));

        // Save uploaded CV files to documents/ with AI naming (async, don't block response)
        if (Array.isArray(cvFiles) && cvFiles.length) {
            const clientName = profile.leadName || slug;
            const docIndex = [];
            for (const { data, ext, type } of cvFiles) {
                if (!data || !ext || !type) continue;
                try {
                    const buf = Buffer.from(data, 'base64');
                    const mimeType = ext === 'pdf' ? 'application/pdf' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
                    let displayName = `${clientName} - ${type === 'linkedin' ? 'LinkedIn CV' : 'Personal CV'}`;
                    try { displayName = await aiNameDocument(clientName, buf, mimeType, `cv-${type}.${ext}`); } catch {}
                    const docId = crypto.randomUUID();
                    const filename = `${docId}.${ext}`;
                    fs.writeFileSync(path.join(docsDir, filename), buf);
                    docIndex.unshift({ id: docId, filename, displayName, originalName: `cv-${type}.${ext}`, mimeType, size: buf.length, uploadedBy: 'coach', uploadedAt: new Date().toISOString() });
                } catch {}
            }
            if (docIndex.length) writeDocIndex(menteeDir, docIndex);
        }

        if (profile.email) {
            const setupToken = generateToken();
            const auth = {
                email: profile.email,
                passwordHash: null,
                setupToken,
                setupTokenExpiry: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(),
                setupComplete: false
            };
            fs.writeFileSync(path.join(menteeDir, 'auth.json'), JSON.stringify(auth, null, 2));
            sendSetupEmail(profile.email, profile.leadName, setupToken).catch(err => {
                console.error(`[EMAIL] Failed to send setup email to ${profile.email}:`, err.message);
            });
        }

        appendChangelog(menteeDir, { who: 'coach', text: `Profile created via admin dashboard`, action: 'profile_created' });
        res.json({ success: true, slug: folderKey });

        // Notify all coaches about the new client (non-blocking)
        const adminUrl = `${APP_URL}${BASE_PATH}/profile/${folderKey}`;
        const allCoaches = readCoaches().filter(c => c.setupComplete);
        for (const coach of allCoaches) {
            sendCoachNewClientEmail(coach.email, coach.name, profile.leadName, adminUrl).catch(err => {
                console.error(`[EMAIL] Failed to send new client notification to ${coach.email}:`, err.message);
            });
        }

        // Generate form.json and cvmaster.json async (non-blocking)
        generateFormJson(menteeDir, payload, cvText || '').catch(e =>
            console.error('[FORM] generation failed:', e.message)
        );
        if (cvText) {
            generateDossier(menteeDir, cvText).catch(e =>
                console.error('[DOSSIER] generation failed:', e.message)
            );
        }
    });

    // ====================== CLIENT PORTAL ROUTES ======================

    // Client login page
    app.get(CLIENT_PATH + '/login', (req, res) => {
        if (isClientLoggedIn(req)) return res.redirect(CLIENT_PATH + '/portal');
        res.render('client-login', { error: null });
    });

    // Client login API
    app.post(BASE_PATH + '/api/client/login', async (req, res) => {
        const { email, password } = req.body;
        if (!email || !password) {
            return res.status(400).json({ success: false, error: 'Email and password are required' });
        }

        const mentee = findMenteeByEmail(email);
        if (!mentee) {
            return res.status(401).json({ success: false, error: 'Invalid email or password' });
        }

        if (!mentee.auth.setupComplete || !mentee.auth.passwordHash) {
            return res.status(401).json({ success: false, error: 'Account not set up yet. Please check your email.' });
        }

        const valid = await bcrypt.compare(password, mentee.auth.passwordHash);
        if (!valid) {
            return res.status(401).json({ success: false, error: 'Invalid email or password' });
        }

        const sessionId = generateSessionId();
        let clientName = mentee.slug;
        try {
            const cs = JSON.parse(fs.readFileSync(path.join(mentee.dir, 'career-strategy.json'), 'utf8'));
            clientName = cs.leadName || clientName;
        } catch {}
        sessions[sessionId] = { loggedIn: true, role: 'client', clientSlug: mentee.slug, clientName };
        setCookie(res, 'clientSessionId', sessionId, 7 * 24 * 60 * 60); // 7 days
        res.json({ success: true, redirect: CLIENT_PATH + '/portal' });
    });

    // Client logout
    app.get(CLIENT_PATH + '/logout', (req, res) => {
        const sessionId = req.cookies?.clientSessionId;
        if (sessionId) delete sessions[sessionId];
        res.redirect(CLIENT_PATH + '/login');
    });

    // Setup password — show form
    app.get(CLIENT_PATH + '/setup/:token', (req, res) => {
        const { token } = req.params;
        const mentee = findMenteeByToken(token);

        if (!mentee) {
            return res.render('client-setup', { error: 'Link inválido ou expirado.', token: null, name: null });
        }

        if (new Date(mentee.auth.setupTokenExpiry) < new Date()) {
            return res.render('client-setup', { error: 'Este link expirou. Contacte o seu coach.', token: null, name: null });
        }

        if (mentee.auth.setupComplete) {
            return res.redirect(CLIENT_PATH + '/login');
        }

        // Get the mentee name from career-strategy
        let name = mentee.auth.email;
        try {
            const cs = JSON.parse(fs.readFileSync(path.join(mentee.dir, 'career-strategy.json'), 'utf8'));
            name = cs.leadName || name;
        } catch {}

        res.render('client-setup', { error: null, token, name });
    });

    // Setup password — save
    app.post(CLIENT_PATH + '/setup/:token', async (req, res) => {
        const { token } = req.params;
        const { password, passwordConfirm } = req.body || {};

        const mentee = findMenteeByToken(token);
        if (!mentee) {
            return res.render('client-setup', { error: 'Link inválido ou expirado.', token: null, name: null });
        }

        if (new Date(mentee.auth.setupTokenExpiry) < new Date()) {
            return res.render('client-setup', { error: 'Este link expirou. Contacte o seu coach.', token: null, name: null });
        }

        if (!password || password.length < 6) {
            return res.render('client-setup', { error: 'A password deve ter pelo menos 6 caracteres.', token, name: mentee.auth.email });
        }

        if (password !== passwordConfirm) {
            return res.render('client-setup', { error: 'As passwords não coincidem.', token, name: mentee.auth.email });
        }

        const passwordHash = await bcrypt.hash(password, 10);
        const updatedAuth = {
            ...mentee.auth,
            passwordHash,
            setupToken: null,
            setupTokenExpiry: null,
            setupComplete: true
        };
        fs.writeFileSync(path.join(mentee.dir, 'auth.json'), JSON.stringify(updatedAuth, null, 2));

        res.redirect(CLIENT_PATH + '/login?setup=done');
    });

    // Client portal — own dossier
    app.get(CLIENT_PATH + '/portal', requireClientAuth, (req, res) => {
        res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
        const session = getClientSession(req);
        const menteeDir = findMenteeFolder(session.clientSlug.split('_')[0]) ||
                        path.join(BASE_DIR, 'src', 'db', 'mentees', session.clientSlug);

        const files = ['career-strategy', 'jobtracking', 'cvmaster', 'form'];
        const data = {};
        let loaded = 0;

        files.forEach(file => {
            fs.readFile(path.join(menteeDir, `${file}.json`), 'utf8', (err, fileData) => {
                loaded++;
                try { data[file] = err ? null : JSON.parse(fileData); }
                catch { data[file] = null; }

                if (loaded === files.length) {
                    const cs = data['career-strategy'] || {};
                    const documents = readDocIndex(menteeDir);
                    // Filter out jobs pending coach approval — clients must not see them yet
                    const rawJt = data['jobtracking'] || null;
                    const clientJt = rawJt ? { ...rawJt, jobs: (rawJt.jobs || []).filter(j => j.status !== 'pending_coach_approval') } : null;
                    res.render('client-portal', {
                        careerStrategy:   cs,
                        jobtracking:      clientJt,
                        cvmaster:         data['cvmaster']    || null,
                        form:             data['form']        || null,
                        formValidated:    cs.formValidated    === true,
                        dossierValidated: cs.dossierValidated === true,
                        documents,
                        slug: session.clientSlug,
                        BASE_PATH
                    });
                }
            });
        });
    });

    // Client: upload CV on first access (when coach created profile without CV)
    app.post(BASE_PATH + '/api/client/cv-setup', requireClientAuth, async (req, res) => {
        const session = getClientSession(req);
        const menteeDir = findMenteeFolder(session.clientSlug.split('_')[0]) ||
                          path.join(BASE_DIR, 'src', 'db', 'mentees', session.clientSlug);
        if (!menteeDir || !fs.existsSync(menteeDir)) return res.status(404).json({ error: 'Profile not found' });

        const { cvText, cvFile, confirmedFields } = req.body;
        if (!cvText || !cvText.trim()) return res.status(400).json({ error: 'CV text is required' });

        // Save the CV file to documents/
        if (cvFile && cvFile.data && cvFile.ext) {
            try {
                const docsDir = path.join(menteeDir, 'documents');
                fs.mkdirSync(docsDir, { recursive: true });
                const buf = Buffer.from(cvFile.data, 'base64');
                const mimeType = cvFile.ext === 'pdf' ? 'application/pdf' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
                const docId = crypto.randomUUID();
                const filename = `${docId}.${cvFile.ext}`;
                const csRaw = fs.readFileSync(path.join(menteeDir, 'career-strategy.json'), 'utf8');
                const csData = JSON.parse(csRaw);
                const clientName = csData.leadName || session.clientSlug;
                let displayName = `${clientName} - Personal CV`;
                try { displayName = await aiNameDocument(clientName, buf, mimeType, `cv.${cvFile.ext}`); } catch {}
                fs.writeFileSync(path.join(docsDir, filename), buf);
                const existing = readDocIndex(menteeDir);
                writeDocIndex(menteeDir, [{ id: docId, filename, displayName, originalName: `cv.${cvFile.ext}`, mimeType, size: buf.length, uploadedBy: 'client', uploadedAt: new Date().toISOString() }, ...existing]);
            } catch (e) {
                console.error('[CV-SETUP] file save error:', e.message);
            }
        }

        // Update career-strategy with confirmed fields
        if (confirmedFields && Object.keys(confirmedFields).length) {
            try {
                const csPath = path.join(menteeDir, 'career-strategy.json');
                const cs = JSON.parse(fs.readFileSync(csPath, 'utf8'));
                const allowed = ['leadName', 'email', 'phone', 'location', 'linkedin', 'nationality', 'languages', 'shortDescription'];
                for (const k of allowed) {
                    if (confirmedFields[k] !== undefined && confirmedFields[k] !== '') cs[k] = confirmedFields[k];
                }
                cs.cvSetupComplete = true;
                cs.updatedAt = new Date().toISOString();
                fs.writeFileSync(csPath, JSON.stringify(cs, null, 2));
            } catch (e) {
                console.error('[CV-SETUP] career-strategy update error:', e.message);
            }
        }

        // Generate cvmaster and form async (non-blocking)
        generateDossier(menteeDir, cvText).catch(e => console.error('[CV-SETUP] dossier error:', e.message));
        try {
            const cs = JSON.parse(fs.readFileSync(path.join(menteeDir, 'career-strategy.json'), 'utf8'));
            generateFormJson(menteeDir, cs, cvText).catch(e => console.error('[CV-SETUP] form error:', e.message));
        } catch {}

        appendChangelog(menteeDir, { who: 'client', text: 'CV uploaded and profile completed', action: 'cv_setup' });
        res.json({ success: true });
    });

    // Client: validate form or dossier
    app.post(BASE_PATH + '/api/client/validate', requireClientAuth, (req, res) => {
        const { type } = req.body; // 'form' | 'dossier'
        if (!['form', 'dossier'].includes(type)) return res.status(400).json({ error: 'type must be form or dossier' });
        const session = getClientSession(req);
        const menteeDir = findMenteeFolder(session.clientSlug.split('_')[0]) ||
                          path.join(BASE_DIR, 'src', 'db', 'mentees', session.clientSlug);
        const csPath = path.join(menteeDir, 'career-strategy.json');
        try {
            const cs = JSON.parse(fs.readFileSync(csPath, 'utf8'));
            if (type === 'form') cs.formValidated = true;
            else cs.dossierValidated = true;
            fs.writeFileSync(csPath, JSON.stringify(cs, null, 2));
            const actorName = getActorName(req);
            const label = type === 'form' ? 'profile form' : 'career dossier';
            appendChangelog(menteeDir, { who: 'client', text: `${actorName} validated their ${label}`, action: 'validate', validateType: type });
            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ error: e.message });
        }
    });

    // ====================== ADMIN: resend setup email ======================
    app.post(BASE_PATH + '/api/mentee/:slug/resend-setup', requireAuth, async (req, res) => {
        const menteeDir = findMenteeFolder(req.params.slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });

        const authPath = path.join(menteeDir, 'auth.json');

        try {
            const cs = JSON.parse(fs.readFileSync(path.join(menteeDir, 'career-strategy.json'), 'utf8'));

            // Create auth.json if it doesn't exist yet (legacy mentees)
            if (!fs.existsSync(authPath)) {
                if (!cs.email) return res.status(400).json({ error: 'Mentee has no email address' });
                fs.writeFileSync(authPath, JSON.stringify({
                    email: cs.email,
                    passwordHash: null,
                    setupToken: null,
                    setupTokenExpiry: null,
                    setupComplete: false
                }, null, 2));
            }

            const auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));

            const setupToken = generateToken();
            const updatedAuth = {
                ...auth,
                setupToken,
                setupTokenExpiry: new Date(Date.now() + 48 * 60 * 60 * 1000).toISOString(),
                setupComplete: false,
                passwordHash: null
            };
            fs.writeFileSync(authPath, JSON.stringify(updatedAuth, null, 2));

            await sendSetupEmail(auth.email, cs.leadName || auth.email, setupToken);
            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ error: e.message });
        }
    }); 

    // ====================== CHANGELOG ======================
    app.get(BASE_PATH + '/api/mentee/:slug/changelog', requireAnyAuth, (req, res) => {
        const slug = req.params.slug;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ error: 'Access denied' });
        const menteeDir = findMenteeFolder(slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });
        try {
            const log = JSON.parse(fs.readFileSync(path.join(menteeDir, 'changelog.json'), 'utf8'));
            res.json(log.slice().reverse());
        } catch { res.json([]); }
    });

    // ====================== CLIENT EDIT ROUTES ======================

    // Client updates own profile fields
    app.put(BASE_PATH + '/api/client/profile', requireClientAuth, (req, res) => {
        const session = getClientSession(req);
        const menteeDir = findMenteeFolder(session.clientSlug.split('_')[0]) ||
                        path.join(BASE_DIR, 'src', 'db', 'mentees', session.clientSlug);
        const filePath = path.join(menteeDir, 'career-strategy.json');
        const updates = req.body;
        if (!Object.keys(updates).length) return res.status(400).json({ error: 'Nothing to update' });
        try {
            const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
            const oldData = { ...data };
            Object.assign(data, updates);
            fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
            const actorName = getActorName(req);
            const text = describeUpdates(actorName, updates, oldData);
            appendChangelog(menteeDir, { who: 'client', text, action: 'profile_edit', fields: Object.keys(updates) });
            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ error: e.message });
        }
    });

    // Client: change email (verifies current password)
    app.post(BASE_PATH + '/api/client/settings/email', requireClientAuth, async (req, res) => {
        const session = getClientSession(req);
        const menteeDir = findMenteeFolder(session.clientSlug.split('_')[0]) ||
                        path.join(BASE_DIR, 'src', 'db', 'mentees', session.clientSlug);
        const authPath = path.join(menteeDir, 'auth.json');
        const csPath   = path.join(menteeDir, 'career-strategy.json');
        const { email, password } = req.body;
        if (!email || !password) return res.status(400).json({ error: 'Email and password are required' });
        try {
            const auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
            const valid = await bcrypt.compare(password, auth.passwordHash);
            if (!valid) return res.status(401).json({ error: 'Incorrect password' });
            auth.email = email.trim().toLowerCase();
            fs.writeFileSync(authPath, JSON.stringify(auth, null, 2));
            const cs = JSON.parse(fs.readFileSync(csPath, 'utf8'));
            cs.email = auth.email;
            fs.writeFileSync(csPath, JSON.stringify(cs, null, 2));
            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ error: e.message });
        }
    });

    // Client: change password (verifies current password)
    app.post(BASE_PATH + '/api/client/settings/password', requireClientAuth, async (req, res) => {
        const session = getClientSession(req);
        const menteeDir = findMenteeFolder(session.clientSlug.split('_')[0]) ||
                        path.join(BASE_DIR, 'src', 'db', 'mentees', session.clientSlug);
        const authPath = path.join(menteeDir, 'auth.json');
        const { currentPassword, newPassword } = req.body;
        if (!currentPassword || !newPassword) return res.status(400).json({ error: 'Both passwords are required' });
        if (newPassword.length < 8) return res.status(400).json({ error: 'New password must be at least 8 characters' });
        try {
            const auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
            const valid = await bcrypt.compare(currentPassword, auth.passwordHash);
            if (!valid) return res.status(401).json({ error: 'Incorrect current password' });
            auth.passwordHash = await bcrypt.hash(newPassword, 12);
            fs.writeFileSync(authPath, JSON.stringify(auth, null, 2));
            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ error: e.message });
        }
    });

    // Client AI update (server-side — key not exposed to browser)
    app.post(BASE_PATH + '/api/client/ai-update', requireClientAuth, async (req, res) => {
        const session = getClientSession(req);
        const { text } = req.body;
        if (!text || !text.trim()) return res.status(400).json({ error: 'No text provided' });
        const menteeDir = findMenteeFolder(session.clientSlug.split('_')[0]) ||
                        path.join(BASE_DIR, 'src', 'db', 'mentees', session.clientSlug);
        const filePath = path.join(menteeDir, 'career-strategy.json');
        let existing;
        try { existing = JSON.parse(fs.readFileSync(filePath, 'utf8')); }
        catch { return res.status(500).json({ error: 'Failed to read profile' }); }

        const prompt = `You are a career coaching assistant. Merge the new information into the existing client profile JSON. Update any relevant fields based on the new information. Preserve all fields not mentioned or contradicted. Return ONLY valid JSON.\n\nEXISTING:\n${JSON.stringify(existing, null, 2)}\n\nNEW INFO:\n${text}`;
        try {
            const r = await fetch('https://api.anthropic.com/v1/messages', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', 'x-api-key': ANTHROPIC_KEY, 'anthropic-version': '2023-06-01' },
                body: JSON.stringify({ model: 'claude-haiku-4-5', max_tokens: 1024, messages: [{ role: 'user', content: prompt }] })
            });
            const aiData = await r.json();
            if (!r.ok) return res.status(500).json({ error: aiData.error?.message || 'Claude error' });
            let content = aiData.content[0].text.trim().replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
            const updated = JSON.parse(content);
            fs.writeFileSync(filePath, JSON.stringify(updated, null, 2));
            const actorName = getActorName(req);
            appendChangelog(menteeDir, { who: 'client', text: `${actorName} updated profile via AI assistant`, action: 'ai_update' });
            res.json({ success: true });
        } catch (e) {
            res.status(500).json({ error: 'AI failed: ' + e.message });
        }
    });

    // ====================== MOBILE API ======================
    const MOBILE_TOKEN   = process.env.MOBILE_TOKEN  || 'ecobe-mobile-2026-xk9';
    const DEEPSEEK_KEY   = process.env.DEEPSEEK_KEY  || 'sk-33643be5a8654f2ca30b429286e43cc9';
    const DEEPSEEK_URL   = 'https://api.deepseek.com/chat/completions';
    const JOBSEARCH_BASE   = 'https://ecobeadmin.com/jobsearch/adminjobsearch.php';
    const JOBSEARCH_TOKEN  = process.env.JOBSEARCH_TOKEN || 'ecobe-search-k7x2p9';
    const THEIRSTACK_API   = 'https://api.theirstack.com/v1/jobs/search';
    const THEIRSTACK_KEY   = process.env.THEIRSTACK_KEY  || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXIiOjEsImp0aSI6IjE1MTNiN2E4LTNmZGItNGRkMy1iMDdjLTQ3N2NjZDVhMGYwZiIsImNyZWF0ZWRfYnkiOjE0NzQ5MSwicGVybWlzc2lvbnMiOltdLCJhdWQiOiJhcGkiLCJpYXQiOjE3NzI5MDE3NzMsInN1YiI6IjE0Njg4NyIsIm5hbWUiOiJlY29iZSIsImVtYWlsIjoiYXN2bWJ1c2luZXNzQGdtYWlsLmNvbSJ9.0ixXLmCcHbMGHAB_nmN9M8ar6UPXpDCvmt-RW6nNQ34';
    const THEIRSTACK_KEY2  = process.env.THEIRSTACK_KEY2 || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXIiOjEsImp0aSI6ImYyOWJjMGM1LTgyNzctNDVjNi1iOWUwLWJkM2E0NTIwOGMyMyIsImNyZWF0ZWRfYnkiOjE1MDUzMywicGVybWlzc2lvbnMiOltdLCJhdWQiOiJhcGkiLCJpYXQiOjE3NzQxMzM0MjYsInN1YiI6IjE0OTkwMyIsIm5hbWUiOiJrZXlzMiIsImVtYWlsIjoiYXN2bTIyMDFAZ21haWwuY29tIn0.LFpdvH_9enrTKZEeGFXULK4yYAF694Acuju2TMXeQJ0';

    const MOBILE_SYSTEM = `You are Ecobot, the AI assistant of Ecobe Academy — friendly, direct, and helpful.
    Respond in the user's language (PT, EN, FR or other). Default: English.
    Write plain text only — no markdown bold (**text**). Use line breaks for clarity.
    Maximum 2-3 short sentences per reply. Answer first, then ask ONE question.

    === COACH COMMANDS (emit these tokens to trigger backend actions) ===
    When the coach asks you to look up or work with a specific client/mentee, emit exactly:
    CLIENTSEARCH:<name>
    When the coach asks to search jobs for the current client, emit:
    SEARCHJOBS:<role>|<location>
    When the coach asks for the client's career strategy / profile summary, emit:
    CAREERSTRATEGY
    When the coach asks to update a data field, emit:
    UPDATEDATA:<field>:<value>

    === RULES ===
    - If no client is loaded yet and the coach mentions a name, emit CLIENTSEARCH:<name>.
    - After a client is loaded, their career strategy data is appended to this system prompt.
    - Keep replies short. After commands are executed, the result is injected into the conversation.
    - Always confirm actions before updating data.

    === ABOUT ECOBE ACADEMY ===
    Name: Ecobe Academy | Website: https://ecobeacademy.com/ | Email: ecobe@ecobeacademy.com
    Founder: Giuliano Hippertt — 20+ years international HR.
    Programs: Silver (4x CHF 95), Gold (4x CHF 322), Platinum (4x CHF 644).
    Team: Giuliano Hippertt, Anna Dutra, Fabricio Ataide, Daniel Pereira.`;

    const mobileSessions = new Map(); // sessionId -> { clientSlug, clientData }

    function loadAllMentees() {
        const menteesDir = path.join(BASE_DIR, 'src', 'db', 'mentees');
        const results = [];
        try {
            const entries = fs.readdirSync(menteesDir);
            for (const slug of entries) {
                const csPath = path.join(menteesDir, slug, 'career-strategy.json');
                if (!fs.existsSync(csPath)) continue;
                try {
                    const cs = JSON.parse(fs.readFileSync(csPath, 'utf8'));
                    results.push({ slug, cs });
                } catch {}
            }
        } catch {}
        return results;
    }

    function findMenteeByName(query) {
        const q = query.toLowerCase().trim();
        const mentees = loadAllMentees();
        // exact slug match
        let found = mentees.find(m => m.slug.toLowerCase() === q);
        if (found) return found;
        // leadName match
        found = mentees.find(m => (m.cs.leadName || '').toLowerCase().includes(q));
        if (found) return found;
        // clientName match
        found = mentees.find(m => (m.cs.clientName || '').toLowerCase().includes(q));
        if (found) return found;
        // slug starts with
        found = mentees.find(m => m.slug.toLowerCase().startsWith(q));
        if (found) return found;
        // partial slug
        found = mentees.find(m => m.slug.toLowerCase().includes(q));
        return found || null;
    }

    async function deepseekMobile(messages) {
        const r = await fetch(DEEPSEEK_URL, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DEEPSEEK_KEY}` },
            body: JSON.stringify({ model: 'deepseek-chat', messages, max_tokens: 600, temperature: 0.7 })
        });
        const data = await r.json();
        if (!r.ok) throw new Error(data.error?.message || 'DeepSeek error');
        return data.choices[0].message.content.trim();
    }

    async function mobileJobSearch(role, location) {
        try {
            const url = `${JOBSEARCH_BASE}?action=search&q=${encodeURIComponent(role)}&location=${encodeURIComponent(location)}`;
            const r = await fetch(url, { signal: AbortSignal.timeout(15000), headers: { 'X-Search-Token': JOBSEARCH_TOKEN } });
            const data = await r.json();
            const jobs = Array.isArray(data) ? data : (data.jobs || data.results || []);
            if (!jobs.length) return 'No jobs found for that search.';
            const lines = jobs.slice(0, 8).map((j, i) =>
                `${i+1}. ${j.title || j.job_title || 'Role'} @ ${j.company || j.company_name || '?'} — ${j.location || location}`
            );
            return `Found ${jobs.length} results (showing top ${lines.length}):\n${lines.join('\n')}`;
        } catch (e) {
            return `Job search failed: ${e.message}`;
        }
    }

    async function mobileDispatch(reply, sess) {
        let injected = reply;

        // CLIENTSEARCH
        const csMatch = reply.match(/CLIENTSEARCH:([^\n]+)/i);
        if (csMatch) {
            const name = csMatch[1].trim();
            const found = findMenteeByName(name);
            if (found) {
                sess.clientSlug = found.slug;
                sess.clientData = found.cs;
                const cs = found.cs;
                injected = reply.replace(/CLIENTSEARCH:[^\n]+/i,
                    `[Client loaded: ${cs.leadName || cs.clientName || found.slug}]\n` +
                    `Phase: ${cs.phase || '?'} | Location: ${cs.location || '?'} | Target: ${(cs.targetRoleFamilies||[]).join(', ') || '?'} | Geo: ${(cs.targetGeographies||[]).join(', ') || '?'}`
                );
            } else {
                injected = reply.replace(/CLIENTSEARCH:[^\n]+/i, `[No client found matching "${name}"]`);
            }
        }

        // CAREERSTRATEGY
        if (/CAREERSTRATEGY/i.test(reply) && sess.clientData) {
            const cs = sess.clientData;
            const summary = [
                `Name: ${cs.leadName || cs.clientName || sess.clientSlug}`,
                `Phase: ${cs.phase || '?'}`,
                `Location: ${cs.location || '?'}`,
                `Target roles: ${(cs.targetRoleFamilies||[]).join(', ') || '?'}`,
                `Target geos: ${(cs.targetGeographies||[]).join(', ') || '?'}`,
                `Salary: ${cs.salaryTarget || '?'}`,
                `Permits: ${(cs.workPermits||[]).join(', ') || '?'}`,
                `Languages: ${(cs.languages||[]).join(', ') || '?'}`,
                cs.note ? `Note: ${cs.note}` : ''
            ].filter(Boolean).join('\n');
            injected = reply.replace(/CAREERSTRATEGY/i, `[Career Strategy]\n${summary}`);
        }

        // SEARCHJOBS
        const sjMatch = reply.match(/SEARCHJOBS:([^|\n]+)\|([^\n]+)/i);
        if (sjMatch) {
            const role = sjMatch[1].trim();
            const location = sjMatch[2].trim();
            const jobResult = await mobileJobSearch(role, location);
            injected = reply.replace(/SEARCHJOBS:[^\n]+/i, `[Job Search: ${role} in ${location}]\n${jobResult}`);
        }

        // UPDATEDATA
        const udMatch = reply.match(/UPDATEDATA:([^:]+):([^\n]+)/i);
        if (udMatch && sess.clientSlug) {
            const field = udMatch[1].trim();
            const value = udMatch[2].trim();
            const menteeDir = findMenteeFolder(sess.clientSlug);
            if (menteeDir) {
                try {
                    const csPath = path.join(menteeDir, 'career-strategy.json');
                    const cs = JSON.parse(fs.readFileSync(csPath, 'utf8'));
                    cs[field] = value;
                    fs.writeFileSync(csPath, JSON.stringify(cs, null, 2));
                    if (sess.clientData) sess.clientData[field] = value;
                    injected = reply.replace(/UPDATEDATA:[^\n]+/i, `[Updated ${field} = ${value}]`);
                } catch (e) {
                    injected = reply.replace(/UPDATEDATA:[^\n]+/i, `[Update failed: ${e.message}]`);
                }
            }
        }

        return injected;
    }

    app.post(BASE_PATH + '/api/mobile', async (req, res) => {
        if (req.headers.authorization !== `Bearer ${MOBILE_TOKEN}`)
            return res.status(401).json({ error: 'Unauthorized' });

        const { messages, sessionId } = req.body;
        if (!Array.isArray(messages) || !messages.length)
            return res.status(400).json({ error: 'messages required' });
        if (!sessionId)
            return res.status(400).json({ error: 'sessionId required' });

        // Get or create session
        if (!mobileSessions.has(sessionId)) mobileSessions.set(sessionId, {});
        const sess = mobileSessions.get(sessionId);

        // Build system prompt with client context if loaded
        let systemContent = MOBILE_SYSTEM;
        if (sess.clientData) {
            const cs = sess.clientData;
            systemContent += `\n\n=== CURRENT CLIENT ===\n` +
                `Name: ${cs.leadName || cs.clientName || sess.clientSlug}\n` +
                `Phase: ${cs.phase || '?'}\nLocation: ${cs.location || '?'}\n` +
                `Target roles: ${(cs.targetRoleFamilies||[]).join(', ') || '?'}\n` +
                `Target geos: ${(cs.targetGeographies||[]).join(', ') || '?'}\n` +
                `Salary: ${cs.salaryTarget || '?'}\n` +
                `Permits: ${(cs.workPermits||[]).join(', ') || '?'}\n` +
                `Languages: ${(cs.languages||[]).join(', ') || '?'}\n` +
                (cs.note ? `Note: ${cs.note}\n` : '');
        }

        const aiMessages = [
            { role: 'system', content: systemContent },
            ...messages.slice(-20)
        ];

        try {
            let reply = await deepseekMobile(aiMessages);
            reply = await mobileDispatch(reply, sess);
            res.json({ reply });
        } catch (e) {
            console.error('Mobile API error:', e.message);
            res.status(500).json({ error: e.message });
        }
    });

    // ====================== JOB SEARCH ======================

    async function callClaude(messages, maxTokens) {
        const r = await fetch('https://api.anthropic.com/v1/messages', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'x-api-key': ANTHROPIC_KEY, 'anthropic-version': '2023-06-01' },
            body: JSON.stringify({ model: 'claude-haiku-4-5', max_tokens: maxTokens || 300, messages })
        });
        const data = await r.json();
        if (!r.ok) throw new Error(data.error?.message || 'Claude error');
        return data.content[0].text.trim();
    }

    async function fetchJobDetail(id, source) {
        try {
            const url = `${JOBSEARCH_BASE}?action=detail&id=${encodeURIComponent(id)}&source=${encodeURIComponent(source || 'linkedin')}`;
            const r = await fetch(url, { signal: AbortSignal.timeout(12000), headers: { 'X-Search-Token': JOBSEARCH_TOKEN } });
            if (!r.ok) return null;
            const data = await r.json();
            return data.description || null;
        } catch { return null; }
    }

    function searchParamsFromStrategy(cs) {
        const seg = cs.strategy?.segments?.[0] || {};
        const roles = seg.targetRoles || cs.primaryRoles || cs.targetRoleFamilies || [];
        const priorityCities = cs.priorityCities?.length ? cs.priorityCities : null;
        const location = seg.targetGeographies?.[0]
            || priorityCities?.[0]
            || cs.primaryCountry
            || cs.location
            || 'Europe';
        const exclusions = cs.exclusions || [];
        const locationFallback = cs.primaryCountry && cs.primaryCountry !== location ? cs.primaryCountry : null;
        return { roles, location, locationFallback, exclusions, priorityCities };
    }

    function isSimilarJob(a, b) {
        const clean = s => (s || '').toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim();
        if (a.url && b.url && a.url === b.url) return true;
        const tA = clean(a.title), tB = clean(b.title);
        const cA = clean(a.company), cB = clean(b.company);
        return tA && tB && tA === tB && cA && cB && cA === cB;
    }

    async function toCodes(loc) {
        const prompt = `Return the ISO 3166-1 alpha-2 country codes for this location as a JSON array: "${loc}"\nRules:\n- Single country or city → its country code, e.g. ["PT"]\n- "worldwide","global","remote" → []\n- If unsure → []\nReply ONLY with the JSON array, no explanation.`;
        try {
            const raw = await callClaude([{ role: 'user', content: prompt }], 80);
            const parsed = JSON.parse(raw.trim());
            return Array.isArray(parsed) && parsed.length ? parsed : null;
        } catch { return null; }
    }

    async function tsSearch(roles, location) {
        const codes = await toCodes(location);
        const keys = [THEIRSTACK_KEY, THEIRSTACK_KEY2].filter(Boolean);
        for (const key of keys) {
            try {
                const res = await fetch(THEIRSTACK_API, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
                    body: JSON.stringify({
                        job_title_or: roles,
                        ...(codes ? { job_country_code_or: codes } : {}),
                        posted_at_max_age_days: 30,
                        limit: 10
                    }),
                    signal: AbortSignal.timeout(15000)
                });
                if (res.status === 402 || res.status === 403) continue;
                if (!res.ok) return [];
                const data = await res.json();
                return (data.data || []).map(j => ({
                    title:       j.job_title || '',
                    company:     (typeof j.company === 'string' ? j.company : j.company?.name) || '',
                    location:    j.short_location || j.location || '',
                    url:         j.url || j.final_url || '',
                    description: j.long_description || j.description || null,
                    source:      'theirstack'
                }));
            } catch { continue; }
        }
        return [];
    }

    async function phpSearchRole(role, location) {
        try {
            const res = await fetch(
                `${JOBSEARCH_BASE}?action=search&q=${encodeURIComponent(role)}&location=${encodeURIComponent(location)}`,
                { signal: AbortSignal.timeout(15000), headers: { 'X-Search-Token': JOBSEARCH_TOKEN } }
            );
            if (!res.ok) return [];
            const data = await res.json();
            return (Array.isArray(data) ? data : data.jobs || []).map(j => ({
                title:       j.title    || '',
                company:     j.company  || '',
                location:    j.location || '',
                url:         j.url      || '',
                description: j.description || null,
                source:      j.source   || 'web',
                id:          j.id       || null
            }));
        } catch { return []; }
    }

    async function searchAllSources(roles, location, locationFallback) {
        const [tsResults, ...phpResults] = await Promise.all([
            tsSearch(roles, location),
            ...roles.map(role => phpSearchRole(role, location))
        ]);

        const jobs = [];
        for (const j of [...tsResults, ...phpResults.flat()]) {
            if (j.title && !jobs.some(x => isSimilarJob(x, j))) jobs.push(j);
        }

        if (!jobs.length && locationFallback && locationFallback !== location) {
            const fallback = await Promise.all(roles.map(role => phpSearchRole(role, locationFallback)));
            for (const j of fallback.flat()) {
                if (j.title && !jobs.some(x => isSimilarJob(x, j))) jobs.push(j);
            }
        }

        return jobs;
    }

    async function screenJobWithAI(job, cs) {
        const csCompact = {
            targetRoles: cs.search?.targetRoles || cs.primaryRoles || cs.targetRoleFamilies || [],
            targetSectors: cs.search?.targetSectors || [
                ...(cs.targetSectors?.tier1 || []),
                ...(cs.targetSectors?.tier2 || [])
            ],
            targetGeographies: cs.search?.targetGeographies || cs.targetGeographies || [],
            exclusions: cs.exclusions || [],
            languages: cs.languages || [],
            seniority: cs.seniority || ''
        };

        const prompt = `You are a job-fit screener. Given a career strategy and a job posting, rate how well this job matches.

    CAREER STRATEGY:
    ${JSON.stringify(csCompact)}

    JOB:
    Title: ${job.title}
    Company: ${job.company}
    Location: ${job.location}
    ${job.description ? 'Description (excerpt): ' + job.description.slice(0, 600) : ''}

    Rate the match level:
    - High: strong fit, most key requirements align
    - Medium: partial fit, worth considering despite gaps
    - Low: significant mismatch, exclusion hit, or wrong seniority/sector

    Reply with ONLY valid JSON (no markdown): {"level":"High","reason":"one-line reason"} or {"level":"Medium","reason":"..."} or {"level":"Low","reason":"..."}`;

        try {
            let content = await callClaude([{ role: 'user', content: prompt }], 120);
            content = content.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
            const result = JSON.parse(content);
            const level = ['High', 'Medium', 'Low'].includes(result.level) ? result.level : 'Medium';
            return { level, reason: result.reason || '' };
        } catch {
            return { level: 'Medium', reason: '' };
        }
    }

    // POST /api/profile/:slug/job-search
    app.post(BASE_PATH + '/api/profile/:slug/job-search', (req, res, next) => {
        const tok = req.query._tok || (req.headers['authorization'] || '').replace('Bearer ', '');
        if (tok && tok === BOT_TOKEN) return next();
        return requireAnyAuth(req, res, next);
    }, async (req, res) => {
        const slug = req.params.slug;
        const tok = req.query._tok || (req.headers['authorization'] || '').replace('Bearer ', '');
        if (tok !== BOT_TOKEN && !canAccessSlug(req, slug)) return res.status(403).json({ error: 'Access denied' });

        const menteeDir = findMenteeFolder(slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });

        let cs, jt;
        try { cs = JSON.parse(fs.readFileSync(path.join(menteeDir, 'career-strategy.json'), 'utf8')); }
        catch { return res.status(500).json({ error: 'Could not read career strategy' }); }
        try { jt = JSON.parse(fs.readFileSync(path.join(menteeDir, 'jobtracking.json'), 'utf8')); }
        catch { jt = { jobs: [] }; }

        const { roles, location, locationFallback, exclusions } = searchParamsFromStrategy(cs);
        if (!roles.length) return res.status(400).json({ error: 'No target roles found in career strategy' });

        const existingKeys = new Set(
            (jt.jobs || []).map(j => `${(j.title || '').toLowerCase().trim()}|${(j.company || '').toLowerCase().trim()}`)
        );

        let allJobs = [];
        try {
            allJobs = await searchAllSources(roles, location, locationFallback);
        } catch (e) {
            return res.status(500).json({ error: 'Job search failed: ' + e.message });
        }

        // Apply exclusion filter from career strategy
        const exclLow = exclusions.map(e => e.toLowerCase());
        const filtered = exclLow.length
            ? allJobs.filter(j => !exclLow.some(kw => j.title.toLowerCase().includes(kw)))
            : allJobs;

        // Deduplicate against already-tracked jobs
        const newJobs = filtered.filter(j => {
            const key = `${(j.title || '').toLowerCase().trim()}|${(j.company || '').toLowerCase().trim()}`;
            return j.title && !existingKeys.has(key);
        });

        const toScreen = newJobs.slice(0, 15);

        // Fetch descriptions for PHP-sourced jobs that have an id but no description
        await Promise.all(toScreen.map(async job => {
            if (job.id && !job.description) {
                job.description = await fetchJobDetail(job.id, (job.source || 'linkedin').toLowerCase());
            }
        }));

        // AI screen each job individually
        const screened = [];
        for (const job of toScreen) {
            const result = await screenJobWithAI(job, cs);
            screened.push({ ...job, aiLevel: result.level, aiReason: result.reason });
        }

        const passed = screened.filter(j => j.aiLevel !== 'Low');
        const failed = screened.filter(j => j.aiLevel === 'Low');

        res.json({
            passed,
            failed,
            stats: {
                total: allJobs.length,
                afterDedup: newJobs.length,
                screened: toScreen.length,
                passed: passed.length,
                failed: failed.length,
                skipped: allJobs.length - newJobs.length
            },
            searchRoles: roles,
            searchLocation: location
        });
    });

    // POST /api/profile/:slug/job-search/expand
    app.post(BASE_PATH + '/api/profile/:slug/job-search/expand', requireAnyAuth, async (req, res) => {
        const slug = req.params.slug;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ error: 'Access denied' });
        const { instruction, currentRoles, currentLocation, seenKeys } = req.body;
        if (!instruction) return res.status(400).json({ error: 'Instruction required' });

        const menteeDir = findMenteeFolder(slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });

        let cs, jt;
        try { cs = JSON.parse(fs.readFileSync(path.join(menteeDir, 'career-strategy.json'), 'utf8')); }
        catch { return res.status(500).json({ error: 'Could not read career strategy' }); }
        try { jt = JSON.parse(fs.readFileSync(path.join(menteeDir, 'jobtracking.json'), 'utf8')); }
        catch { jt = { jobs: [] }; }

        const { roles: strategyRoles, location: strategyLocation, exclusions } = searchParamsFromStrategy(cs);
        const baseRoles = (currentRoles && currentRoles.length) ? currentRoles : strategyRoles;
        const baseLoc   = currentLocation || strategyLocation;

        // AI interprets instruction → updated search params
        const interpretPrompt = `A career coach is refining a job search. Interpret the instruction and return updated search parameters.

    CURRENT SEARCH:
    Roles: ${JSON.stringify(baseRoles)}
    Location: "${baseLoc}"

    INSTRUCTION: "${instruction}"

    Rules:
    - Return roles as specific job titles (not vague keywords)
    - If instruction adds a location ("as well", "also", "and"), include both old and new
    - If instruction replaces location, use only new one
    - If instruction broadens roles, add variants while keeping existing ones
    - If instruction narrows, filter accordingly
    - Location must be a single string (city, country, or region)

    Reply ONLY with valid JSON, no markdown: {"roles":["..."],"location":"..."}`;

        let newRoles, newLocation;
        try {
            const raw = await callClaude([{ role: 'user', content: interpretPrompt }], 200);
            const cleaned = raw.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
            const parsed = JSON.parse(cleaned);
            newRoles   = Array.isArray(parsed.roles) && parsed.roles.length ? parsed.roles : baseRoles;
            newLocation = parsed.location || baseLoc;
        } catch {
            newRoles   = baseRoles;
            newLocation = baseLoc;
        }

        const trackedKeys = new Set(
            (jt.jobs || []).map(j => `${(j.title || '').toLowerCase().trim()}|${(j.company || '').toLowerCase().trim()}`)
        );
        const shownKeys = new Set((seenKeys || []).map(k => k.toLowerCase()));

        let allJobs = [];
        try {
            allJobs = await searchAllSources(newRoles, newLocation, null);
        } catch (e) {
            return res.status(500).json({ error: 'Search failed: ' + e.message });
        }

        const exclLow = exclusions.map(e => e.toLowerCase());
        const candidates = allJobs.filter(j => {
            if (!j.title) return false;
            if (exclLow.some(kw => j.title.toLowerCase().includes(kw))) return false;
            const key = `${(j.title || '').toLowerCase().trim()}|${(j.company || '').toLowerCase().trim()}`;
            return !trackedKeys.has(key) && !shownKeys.has(key);
        });

        const toScreen = candidates.slice(0, 15);

        await Promise.all(toScreen.map(async job => {
            if (job.id && !job.description) {
                job.description = await fetchJobDetail(job.id, (job.source || 'linkedin').toLowerCase());
            }
        }));

        const screened = [];
        for (const job of toScreen) {
            const result = await screenJobWithAI(job, cs);
            screened.push({ ...job, aiLevel: result.level, aiReason: result.reason });
        }

        const passed = screened.filter(j => j.aiLevel !== 'Low');
        const failed = screened.filter(j => j.aiLevel === 'Low');

        res.json({
            passed,
            newRoles,
            newLocation,
            stats: {
                total: allJobs.length,
                screened: toScreen.length,
                passed: passed.length,
                failed: failed.length
            }
        });
    });

    // POST /api/profile/:slug/jobtracking/:jobId/generate
    app.post(BASE_PATH + '/api/profile/:slug/jobtracking/:jobId/generate', requireAnyAuth, async (req, res) => {
        const { slug, jobId } = req.params;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ error: 'Access denied' });
        const { type } = req.body;
        if (!['cv', 'cover_letter'].includes(type)) return res.status(400).json({ error: 'type must be cv or cover_letter' });

        const menteeDir = findMenteeFolder(slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });

        const jtPath = path.join(menteeDir, 'jobtracking.json');
        let jt;
        try { jt = JSON.parse(fs.readFileSync(jtPath, 'utf8')); }
        catch { return res.status(500).json({ error: 'Could not read jobtracking' }); }

        const job = (jt.jobs || []).find(j => j.id === jobId);
        if (!job) return res.status(404).json({ error: 'Job not found' });

        let cs = {}, cv = {};
        try { cs = JSON.parse(fs.readFileSync(path.join(menteeDir, 'career-strategy.json'), 'utf8')); } catch {}
        try {
            const cvmasterPath = path.join(menteeDir, 'cvmaster.json');
            const cvLegacyPath = path.join(menteeDir, 'cv.json');
            if (fs.existsSync(cvmasterPath)) cv = JSON.parse(fs.readFileSync(cvmasterPath, 'utf8'));
            else if (fs.existsSync(cvLegacyPath)) cv = JSON.parse(fs.readFileSync(cvLegacyPath, 'utf8'));
        } catch {}

        const name = cs.leadName || '';
        const expStr = (cv.experience || []).slice(0, 5).map(e =>
            `${e.title || ''} @ ${e.company || ''} (${e.period || ''}):\n${(e.highlights || []).slice(0, 4).join('\n')}`
        ).join('\n\n');
        const eduStr = (cv.education || []).map(e =>
            `${e.degree || e.title || ''} — ${e.institution || e.school || ''} (${e.period || e.year || ''})`
        ).join('\n');

        let prompt, maxTok;
        if (type === 'cv') {
            maxTok = 5000;
            prompt = `You are an expert CV writer. Generate a tailored, ATS-optimised CV as an HTML fragment, strictly following the playbook rules below.

═══ FORMAT ═══
• ATS-First Reverse-Chronological (single column, left-aligned, generous white space)
• Approximately 2 pages (700 words) for mid-to-senior profiles; 1 page for early career
• Return ONLY an HTML fragment — no DOCTYPE, html, head, body, or style tags
• Use ONLY inline styles — no CSS classes, no <style> blocks

═══ REQUIRED SECTION ORDER ═══
1. Header — Name · Phone · Email · LinkedIn · City/Country (avoid full address) · Nationality if relevant
2. Target Title — e.g. "Business Development Manager" (optional, small caps)
3. Professional Summary — 3–5 lines: role identity + scope + signature strengths + keywords. NO first-person ("I").
4. Core Strengths / Key Skills — 8–14 items: competencies, tools, domains (ATS-friendly pill tags)
5. Professional Experience — reverse chronological; ALL positions must appear.
   • Each role: Job Title | Company | Location | Dates
   • 3–6 bullets per role; lead every bullet with a metric or outcome
   • Present tense for current role, past tense for all previous roles
6. Education — degree, institution, year (include correct dates)
7. Certifications / Training (if any)
8. Languages (especially relevant for Europe)
9. Additional — projects, publications, volunteering, board memberships (if any)

═══ WRITING RULES — DO ═══
• Full semantic alignment with the Job Description: use JD keywords and terminology naturally
• Every bullet starts with an outcome, metric, or concrete business result — NOT a generic duty
• Professional Summary uses unique, non-repetitive wording; executive but natural language
• Use varied sentence structures; avoid patterns that sound AI-generated
• Short, direct sentences; real examples; minimal subjectivity
• Faithful to what the candidate can clearly defend in an interview

═══ WRITING RULES — DO NOT ═══
• Do NOT use first-person writing ("I am", "I led", "I managed")
• Do NOT invent, distort, or exaggerate any experience
• Do NOT use divider lines (no <hr> tags anywhere in the CV)
• Do NOT use tables, text boxes, icons, or graphics
• Do NOT use keyword stuffing or force JD terms artificially
• Do NOT use vague executive jargon or empty self-praise
• Do NOT use repetitive bullet patterns (e.g. "Responsible for X…" repeated)
• Do NOT use shortcut descriptions that dilute seniority

═══ BANNED WORDS — NEVER USE ═══
meticulous, orchestrated, pioneered, championed, realm, helm, helmed, showcasing,
comprehensive, demonstrating, boost, measurable, proven ability, proven record,
approximately, "the intersection of", passionate, dynamic, excited,
"Managed comprehensive integration strategies"

═══ HTML INLINE STYLE GUIDE ═══
• Outer wrapper: <div style="font-family:'Ubuntu',Arial,sans-serif;font-size:10.5pt;color:#333;line-height:1.55;">
• Name: <h1 style="font-size:22pt;font-weight:700;color:#1a1a1a;margin:0 0 4px;letter-spacing:-0.5px;">
• Contact line (one line, items separated by · ): <p style="font-size:9.5pt;color:#777;margin:0 0 14px;letter-spacing:0.2px;">
• Target title tag: <p style="font-size:9pt;color:#28a745;font-weight:600;text-transform:uppercase;letter-spacing:1.5px;margin:0 0 16px;">Applying for: ${job.title} at ${job.company}</p>
• Section header (NO hr, border-bottom only): <h2 style="font-size:10pt;font-weight:700;color:#28a745;text-transform:uppercase;letter-spacing:1.8px;border-bottom:1.5px solid #e8f5e9;padding-bottom:3px;margin:20px 0 9px;">
• Job entry wrapper (break-inside:avoid for print): <div style="break-inside:avoid;page-break-inside:avoid;margin-bottom:10px;">
• Job title: <div style="font-weight:700;color:#1a1a1a;font-size:11pt;margin-bottom:1px;">
• Employer + dates: <div style="font-size:9.5pt;color:#666;font-style:italic;margin-bottom:5px;">
• Bullet list: <ul style="margin:4px 0 4px 16px;padding:0;"> with <li style="margin-bottom:3px;color:#444;">
• Skill / language pills: <div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:4px;"> with <span style="background:#e8f5e9;color:#2e7d32;padding:3px 9px;border-radius:12px;font-size:9pt;">

═══ CANDIDATE PROFILE ═══
Name: ${name}
Email: ${cs.email || ''}
Phone: ${cs.phone || cv.contact?.phone || ''}
Location: ${cs.location || ''}
LinkedIn: ${cs.linkedin || cv.contact?.linkedin || ''}
Seniority: ${cs.seniority || ''}
Professional Summary: ${cv.keyNarrative || cv.narrative || cv.summary || cs.shortDescription || ''}
Skills: ${(cv.skills || []).join(', ')}
Languages: ${(cs.languages || (cv.languages || []).map(l => typeof l === 'string' ? l : l.language)).join(', ')}

EXPERIENCE:
${expStr || '(not provided)'}

EDUCATION:
${eduStr || '(not provided)'}

CERTIFICATIONS: ${(cv.certifications || []).map(c => typeof c === 'string' ? c : c.name).join(', ') || 'None'}

═══ TARGET JOB ═══
Title: ${job.title}
Company: ${job.company}
Location: ${job.location}
${job.description ? 'Job Description:\n' + job.description.slice(0, 1000) : ''}

Return ONLY the HTML fragment. No explanation. No markdown. No code fences.`;
        } else {
            maxTok = 2000;
            prompt = `You are an expert cover letter writer. Generate a compelling, personalised cover letter as an HTML fragment, strictly following the playbook rules below.

═══ COVER LETTER RULES — DO ═══
• Include a formal greeting and a formal closing (e.g. "Yours sincerely," or "Best regards,")
• Always mention the exact position title and company name in the body of the letter
• Personalise the content: reference something specific about the company, its business, market context, or a known project
• Open with a strong, direct hook — name a specific achievement or result that makes the fit obvious immediately
• 3–4 focused paragraphs: (1) strong opening with concrete hook, (2) experience/fit with real example, (3) motivation for this specific company, (4) confident closing with call to action
• Use clear, direct, executive language; varied sentence structures; natural keyword integration
• Keep language faithful to what the candidate can defend in an interview

═══ COVER LETTER RULES — DO NOT ═══
• Do NOT start with "I am writing to express my interest in…" or any equivalent cliché opener
• Do NOT use: "I am confident in my ability to contribute…", "Passionate", "Dynamic", "Excited"
• Do NOT use: meticulous, orchestrated, pioneered, championed, realm, helm, helmed, showcasing, comprehensive, demonstrating, boost, measurable, proven ability, proven record, approximately, "the intersection of"
• Do NOT invent information the candidate cannot defend in an interview
• Do NOT use overly polished or artificially AI-sounding language
• Do NOT use keyword stuffing or repeat the same phrases

═══ HTML INLINE STYLE GUIDE ═══
• Outer wrapper: <div style="font-family:'Ubuntu',Arial,sans-serif;font-size:11pt;color:#333;line-height:1.8;">
• Candidate name: <h1 style="font-size:18pt;font-weight:700;color:#1a1a1a;margin:0 0 4px;letter-spacing:-0.3px;">
• Candidate contacts (one line, separated by · ): <p style="font-size:9.5pt;color:#777;margin:0 0 20px;letter-spacing:0.2px;">
• Job reference line: <p style="font-size:10pt;color:#333;font-weight:600;margin:0 0 4px;">Re: Application for ${job.title} at ${job.company}</p>
• Date: <p style="font-size:9.5pt;color:#999;margin:0 0 22px;">[current date]</p>
• Paragraphs: <p style="margin:0 0 16px;">
• Closing block: <p style="margin:28px 0 4px;">Yours sincerely,</p><p style="font-weight:700;font-size:13pt;color:#1a1a1a;margin:0;">${name}</p>

═══ CANDIDATE ═══
Name: ${name}
Email: ${cs.email || ''}
Phone: ${cs.phone || ''}
Location: ${cs.location || ''}
LinkedIn: ${cs.linkedin || cv.contact?.linkedin || ''}
Seniority: ${cs.seniority || ''}
Summary: ${cv.keyNarrative || cv.narrative || cv.summary || cs.shortDescription || ''}
Key strengths: ${(cs.gapAnalysis?.strengths || []).slice(0, 5).join(', ')}
Languages: ${(cs.languages || []).join(', ')}

═══ TARGET JOB ═══
Title: ${job.title}
Company: ${job.company}
Location: ${job.location}
${job.description ? 'Job Description:\n' + job.description.slice(0, 900) : ''}

Return ONLY the HTML fragment. No explanation. No markdown. No code fences.`;
        }

        try {
            const content = await callClaude([{ role: 'user', content: prompt }], maxTok);
            const field = type === 'cv' ? 'generatedCv' : 'generatedCoverLetter';
            job[field] = content;
            jt.updatedAt = new Date().toISOString();
            fs.writeFileSync(jtPath, JSON.stringify(jt, null, 2));
            const actorName = getActorName(req);
            const typeLabel = type === 'cv' ? 'CV' : 'cover letter';
            appendChangelog(menteeDir, { who: isLoggedIn(req) ? 'coach' : 'client', text: `${actorName} generated ${typeLabel} for '${job.title} at ${job.company}'`, action: 'generate', jobId, docType: type });
            res.json({ success: true, content });
        } catch (e) {
            res.status(500).json({ error: 'Generation failed: ' + e.message });
        }
    });

    // POST /api/profile/:slug/jobtracking/:jobId/refine
    app.post(BASE_PATH + '/api/profile/:slug/jobtracking/:jobId/refine', requireAnyAuth, async (req, res) => {
        const { slug, jobId } = req.params;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ error: 'Access denied' });
        const { type, instruction, currentContent } = req.body;
        if (!['cv', 'cover_letter'].includes(type)) return res.status(400).json({ error: 'type required' });
        if (!instruction) return res.status(400).json({ error: 'instruction required' });
        if (!currentContent) return res.status(400).json({ error: 'currentContent required' });

        const menteeDir = findMenteeFolder(slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });

        const jtPath = path.join(menteeDir, 'jobtracking.json');
        let jt;
        try { jt = JSON.parse(fs.readFileSync(jtPath, 'utf8')); }
        catch { return res.status(500).json({ error: 'Could not read jobtracking' }); }

        const job = (jt.jobs || []).find(j => j.id === jobId);
        if (!job) return res.status(404).json({ error: 'Job not found' });

        let cs = {}, cv = {};
        try { cs = JSON.parse(fs.readFileSync(path.join(menteeDir, 'career-strategy.json'), 'utf8')); } catch {}
        try {
            const cvmasterPath = path.join(menteeDir, 'cvmaster.json');
            const cvLegacyPath = path.join(menteeDir, 'cv.json');
            if (fs.existsSync(cvmasterPath)) cv = JSON.parse(fs.readFileSync(cvmasterPath, 'utf8'));
            else if (fs.existsSync(cvLegacyPath)) cv = JSON.parse(fs.readFileSync(cvLegacyPath, 'utf8'));
        } catch {}

        const profileContext = JSON.stringify({
            name: cs.leadName, location: cs.location, email: cs.email,
            summary: cv.keyNarrative || cv.summary || cs.shortDescription,
            experience: (cv.experience || []).slice(0, 6),
            education: cv.education || [],
            skills: cv.skills || [],
            languages: cs.languages || [],
            strengths: cs.gapAnalysis?.strengths || [],
            seniority: cs.seniority
        });

        const prompt = `You are editing a professional ${type === 'cv' ? 'CV' : 'cover letter'}. Apply the instruction precisely to the existing HTML document.

CURRENT DOCUMENT (HTML fragment):
${currentContent.slice(0, 10000)}

FULL CANDIDATE PROFILE (for reference when adding or changing content):
${profileContext}

TARGET JOB: ${job.title} at ${job.company} (${job.location})

INSTRUCTION: "${instruction}"

EDITING RULES:
- Apply ONLY what the instruction requests — do not restructure or rewrite sections not mentioned
- Preserve all existing inline styles unless the instruction explicitly changes formatting
- Do NOT invent information not present in the candidate profile
- Do NOT use first-person writing ("I am", "I led") in the CV; cover letter may use first person naturally
- Do NOT use divider lines (<hr>) in CVs
- NEVER use these banned words: meticulous, orchestrated, pioneered, championed, realm, helm, helmed, showcasing, comprehensive, demonstrating, boost, measurable, proven ability, proven record, approximately, passionate, dynamic, excited
- Every CV bullet must lead with a metric, outcome, or concrete business result
- Return ONLY the complete updated HTML fragment — no explanation, no markdown, no code fences`;

        try {
            const content = await callClaude([{ role: 'user', content: prompt }], 3500);
            const field = type === 'cv' ? 'generatedCv' : 'generatedCoverLetter';
            job[field] = content;
            jt.updatedAt = new Date().toISOString();
            fs.writeFileSync(jtPath, JSON.stringify(jt, null, 2));
            const actorName = getActorName(req);
            const typeLabel = type === 'cv' ? 'CV' : 'cover letter';
            appendChangelog(menteeDir, { who: isLoggedIn(req) ? 'coach' : 'client', text: `${actorName} refined ${typeLabel} for '${job.title} at ${job.company}': "${instruction}"`, action: 'refine', jobId, docType: type });
            res.json({ success: true, content });
        } catch (e) {
            res.status(500).json({ error: 'Refinement failed: ' + e.message });
        }
    });

    // POST /api/profile/:slug/jobtracking/:jobId/outreach
    app.post(BASE_PATH + '/api/profile/:slug/jobtracking/:jobId/outreach', requireAnyAuth, async (req, res) => {
        const { slug, jobId } = req.params;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ error: 'Access denied' });

        const menteeDir = findMenteeFolder(slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });

        let cs = {}, jt;
        try { cs = JSON.parse(fs.readFileSync(path.join(menteeDir, 'career-strategy.json'), 'utf8')); } catch {}
        try { jt = JSON.parse(fs.readFileSync(path.join(menteeDir, 'jobtracking.json'), 'utf8')); }
        catch { return res.status(500).json({ error: 'Could not read jobtracking' }); }

        const job = (jt.jobs || []).find(j => j.id === jobId);
        if (!job) return res.status(404).json({ error: 'Job not found' });
        if (!job.company) return res.status(400).json({ error: 'Job has no company name' });

        const linkedinUrl = cs.linkedin || cs.contact?.linkedin || null;
        const companyLinkedin = req.body.companyLinkedin || job.companyLinkedin || null;

        try {
            const rawMatches = await runOutreachDiscovery({
                target_company:          job.company,
                target_role:             job.title || undefined,
                linkedin_url:            linkedinUrl || undefined,
                target_company_linkedin: companyLinkedin || undefined,
            });

            const matches = rawMatches.slice(0, 3).map(m => ({
                name:            m.name,
                title:           m.title,
                linkedin_url:    m.linkedin_url,
                location:        m.location || null,
                score:           m.score,
                connection_type: m.connection_type,
                approach_angle:  m.approach_angle,
                draft_message:   m.draft_message,
            }));

            job.outreachContacts = matches;
            job.outreachRunAt = new Date().toISOString();
            jt.updatedAt = new Date().toISOString();
            fs.writeFileSync(path.join(menteeDir, 'jobtracking.json'), JSON.stringify(jt, null, 2));
            const actorName = getActorName(req);
            appendChangelog(menteeDir, { who: isLoggedIn(req) ? 'coach' : 'client', text: `${actorName} ran outreach for '${job.title} at ${job.company}' — ${matches.length} contacts found`, action: 'outreach', jobId });

            res.json({ success: true, contacts: matches });
        } catch (e) {
            res.status(500).json({ error: 'Outreach failed: ' + e.message });
        }
    });

    // POST /api/profile/:slug/job-search/apply
    app.post(BASE_PATH + '/api/profile/:slug/job-search/apply', requireAnyAuth, async (req, res) => {
        const slug = req.params.slug;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ error: 'Access denied' });
        const { jobs: decisions } = req.body;

        if (!Array.isArray(decisions) || !decisions.length) {
            return res.status(400).json({ error: 'jobs array required' });
        }

        const menteeDir = findMenteeFolder(slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });

        const jtPath = path.join(menteeDir, 'jobtracking.json');
        let jt;
        try { jt = JSON.parse(fs.readFileSync(jtPath, 'utf8')); } catch { jt = { jobs: [] }; }
        if (!jt.jobs) jt.jobs = [];

        const now = new Date().toISOString();
        const prefix = slug.split('_')[0].split('-').map(p => p[0]).join('').toLowerCase();
        let maxId = jt.jobs.reduce((max, j) => {
            const m = String(j.id || '').match(/(\d+)/);
            return m ? Math.max(max, parseInt(m[1])) : max;
        }, 0);

        const savedJobs = [];
        const declineReasons = [];

        for (const item of decisions) {
            maxId++;
            const jobId = `${prefix}${String(maxId).padStart(3, '0')}_js`;

            const newJob = {
                id: jobId,
                title: item.title || '',
                company: item.company || '',
                location: item.location || '',
                url: item.url || '',
                source: (item.source || 'web').toLowerCase(),
                foundAt: now,
                aiReason: item.aiReason || null
            };

            if (item.decision === 'accept') {
                newJob.status = 'under_analysis';
                newJob.description = item.description || null;
                newJob.notes = item.aiReason ? `AI: ${item.aiReason}` : null;
            } else {
                newJob.status = 'declined_not_interested';
                newJob.clientDecision = 'rejected';
                newJob.clientRejectionReason = item.declineReason || 'Declined during job search';
                if (item.declineReason) {
                    declineReasons.push({ job: `${item.title} at ${item.company}`, reason: item.declineReason });
                }
            }

            jt.jobs.push(newJob);
            savedJobs.push(newJob);
        }

        jt.updatedAt = now;
        fs.writeFileSync(jtPath, JSON.stringify(jt, null, 2));

        // AI: propose career strategy updates based on decline reasons (client must confirm before saving)
        let strategyProposal = null;
        if (declineReasons.length > 0) {
            const csPath = path.join(menteeDir, 'career-strategy.json');
            let cs;
            try { cs = JSON.parse(fs.readFileSync(csPath, 'utf8')); } catch { cs = {}; }

            const prompt = `A job seeker declined these job postings during their search:
${declineReasons.map(d => `- "${d.job}": "${d.reason}"`).join('\n')}

Current career strategy snapshot:
- Exclusions: ${JSON.stringify(cs.exclusions || [])}
- Target geographies: ${JSON.stringify(cs.targetGeographies || [])}
- Primary roles: ${JSON.stringify(cs.primaryRoles || [])}
- Secondary roles: ${JSON.stringify(cs.secondaryRoles || [])}
- Target companies: ${JSON.stringify(cs.targetCompanies || [])}
- Salary range: ${cs.salaryRange || 'not set'}
- Mobility: ${cs.mobility || 'not set'}

Analyse the decline reasons and decide if the job hunting strategy should be updated to avoid similar positions in the future.

If a clear update is warranted, return:
1. A natural, conversational question (in English) asking the candidate if they want to update the strategy based on what you understood. Be specific — reference what you understood from their reasons. Example: "It sounds like you want to avoid roles in the banking sector. Would you like me to update your job hunting strategy to filter out positions like this going forward?"
2. The specific field changes (use ONLY: exclusions, targetGeographies, primaryRoles, secondaryRoles, targetCompanies, salaryRange, mobility)

Return ONLY valid JSON, no markdown:
{
  "question": "It sounds like... Would you like me to update your job hunting strategy to avoid these positions going forward?",
  "changes": [
    { "field": "exclusions", "action": "add", "values": ["Banking"], "reason": "Candidate explicitly declined a banking role" }
  ]
}

Actions: "add" (append unique values to array), "remove" (filter values from array), "set" (replace scalar field with values[0]).
If no changes are clearly warranted, return { "question": "", "changes": [] }.`;

            try {
                let content = await callClaude([{ role: 'user', content: prompt }], 500);
                content = content.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
                const result = JSON.parse(content);
                if (result.changes?.length) {
                    strategyProposal = { question: result.question || '', changes: result.changes };
                }
            } catch {}
        }

        const actorName = getActorName(req);
        const accepted = savedJobs.filter(j => j.status === 'under_analysis');
        const declined = savedJobs.filter(j => j.status === 'declined_not_interested');
        const parts = [];
        if (accepted.length) parts.push(`accepted ${accepted.length} job${accepted.length > 1 ? 's' : ''} (${accepted.map(j => `${j.title} at ${j.company}`).join(', ')})`);
        if (declined.length) parts.push(`declined ${declined.length} job${declined.length > 1 ? 's' : ''}`);
        if (parts.length) appendChangelog(menteeDir, { who: isLoggedIn(req) ? 'coach' : 'client', text: `${actorName} ${parts.join(' and ')}`, action: 'job_search_apply', accepted: accepted.length, declined: declined.length });

        res.json({
            success: true,
            saved: savedJobs.length,
            accepted: accepted.length,
            declined: declined.length,
            strategyProposal
        });
    });

    // POST /api/profile/:slug/career-strategy/propose
    // Generates a strategy proposal from a single job decline without saving anything to jobtracking
    app.post(BASE_PATH + '/api/profile/:slug/career-strategy/propose', requireAnyAuth, async (req, res) => {
        const slug = req.params.slug;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ error: 'Access denied' });
        const { job, reason } = req.body;
        if (!reason || !reason.trim()) return res.status(400).json({ error: 'reason required' });

        const menteeDir = findMenteeFolder(slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });
        const csPath = path.join(menteeDir, 'career-strategy.json');
        let cs;
        try { cs = JSON.parse(fs.readFileSync(csPath, 'utf8')); } catch { cs = {}; }

        const prompt = `A job seeker declined a job posting during their search:
- "${job || 'a job'}": "${reason.trim()}"

Current career strategy snapshot:
- Exclusions: ${JSON.stringify(cs.exclusions || [])}
- Target geographies: ${JSON.stringify(cs.targetGeographies || [])}
- Primary roles: ${JSON.stringify(cs.primaryRoles || [])}
- Secondary roles: ${JSON.stringify(cs.secondaryRoles || [])}
- Target companies: ${JSON.stringify(cs.targetCompanies || [])}
- Salary range: ${cs.salaryRange || 'not set'}
- Mobility: ${cs.mobility || 'not set'}

Analyse the decline reason and decide if the job hunting strategy should be updated to avoid similar positions in the future.

If a clear update is warranted, return:
1. A natural, conversational question (in English) asking the candidate if they want to update the strategy based on what you understood. Be specific — reference what you understood from their reason. Example: "It sounds like you want to avoid roles that require managing large teams. Would you like me to update your job hunting strategy to filter out positions like this going forward?"
2. The specific field changes (use ONLY: exclusions, targetGeographies, primaryRoles, secondaryRoles, targetCompanies, salaryRange, mobility)

Return ONLY valid JSON, no markdown:
{
  "question": "It sounds like... Would you like me to update your job hunting strategy to avoid these positions going forward?",
  "changes": [
    { "field": "exclusions", "action": "add", "values": ["Banking"], "reason": "Candidate explicitly declined a banking role" }
  ]
}

Actions: "add" (append unique values to array), "remove" (filter values from array), "set" (replace scalar field with values[0]).
If no changes are clearly warranted, return { "question": "", "changes": [] }.`;

        try {
            let content = await callClaude([{ role: 'user', content: prompt }], 500);
            content = content.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
            const result = JSON.parse(content);
            if (result.changes && result.changes.length) {
                return res.json({ strategyProposal: { question: result.question || '', changes: result.changes } });
            }
            return res.json({ strategyProposal: null });
        } catch (e) {
            return res.status(500).json({ error: 'AI proposal failed' });
        }
    });

    // POST /api/profile/:slug/career-strategy/confirm-proposal
    app.post(BASE_PATH + '/api/profile/:slug/career-strategy/confirm-proposal', requireAnyAuth, async (req, res) => {
        const slug = req.params.slug;
        if (!canAccessSlug(req, slug)) return res.status(403).json({ error: 'Access denied' });
        const { changes } = req.body;
        if (!Array.isArray(changes) || !changes.length) return res.status(400).json({ error: 'changes array required' });

        const menteeDir = findMenteeFolder(slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });
        const csPath = path.join(menteeDir, 'career-strategy.json');
        let cs;
        try { cs = JSON.parse(fs.readFileSync(csPath, 'utf8')); } catch { return res.status(500).json({ error: 'Could not read career strategy' }); }

        const applied = [];
        for (const change of changes) {
            const { field, action, values } = change;
            if (!field || !action || !Array.isArray(values)) continue;
            if (action === 'add') {
                if (!Array.isArray(cs[field])) cs[field] = [];
                for (const v of values) {
                    if (!cs[field].some(x => x.toLowerCase() === v.toLowerCase())) {
                        cs[field].push(v);
                        applied.push({ field, action, value: v });
                    }
                }
            } else if (action === 'remove') {
                if (Array.isArray(cs[field])) {
                    cs[field] = cs[field].filter(x => !values.some(v => v.toLowerCase() === x.toLowerCase()));
                    applied.push({ field, action, values });
                }
            } else if (action === 'set' && values[0] !== undefined) {
                cs[field] = values[0];
                applied.push({ field, action, value: values[0] });
            }
        }

        fs.writeFileSync(csPath, JSON.stringify(cs, null, 2));
        const actorName = getActorName(req);
        appendChangelog(menteeDir, {
            who: isLoggedIn(req) ? 'coach' : 'client',
            text: `${actorName} applied AI-suggested career strategy updates from job decline feedback`,
            action: 'strategy_proposal_confirmed',
            applied
        });
        res.json({ success: true, applied });
    });

    // Daily auto-run — searches & saves jobs for all active mentees
    async function runDailyJobs() {
        const menteesDir = path.join(BASE_DIR, 'src', 'db', 'mentees');
        let slugs = [];
        try { slugs = fs.readdirSync(menteesDir).filter(e => fs.statSync(path.join(menteesDir, e)).isDirectory()); } catch {}
        const results = [];
        for (const slug of slugs) {
            let cs, jt;
            try { cs = JSON.parse(fs.readFileSync(path.join(menteesDir, slug, 'career-strategy.json'), 'utf8')); } catch { continue; }
            if ((cs.status || 'active') !== 'active') continue;
            try { jt = JSON.parse(fs.readFileSync(path.join(menteesDir, slug, 'jobtracking.json'), 'utf8')); } catch { jt = { jobs: [] }; }
            try {
                const { roles, location, locationFallback, exclusions } = searchParamsFromStrategy(cs);
                if (!roles.length) { results.push({ slug, skipped: 'no roles' }); continue; }
                const existingKeys = new Set((jt.jobs||[]).map(j => `${(j.title||'').toLowerCase().trim()}|${(j.company||'').toLowerCase().trim()}`));
                let allJobs = await searchAllSources(roles, location, locationFallback);
                const exclLow = exclusions.map(e => e.toLowerCase());
                const newJobs = allJobs.filter(j => {
                    if (exclLow.some(kw => (j.title||'').toLowerCase().includes(kw))) return false;
                    return j.title && !existingKeys.has(`${(j.title||'').toLowerCase().trim()}|${(j.company||'').toLowerCase().trim()}`);
                }).slice(0, 15);
                await Promise.all(newJobs.map(async j => { if (j.id && !j.description) j.description = await fetchJobDetail(j.id, (j.source||'linkedin').toLowerCase()); }));
                const screened = [];
                for (const job of newJobs) { const r = await screenJobWithAI(job, cs); screened.push({ ...job, aiLevel: r.level, aiReason: r.reason }); }
                const allPassed = screened.filter(j => j.aiLevel !== 'Low');
                const plan = (cs.plan || 'silver').toLowerCase();
                const isPartnership = plan === 'golden' || plan === 'platinum';
                // Self-driven: cap at 2 per day; partnership: no cap (coach reviews all)
                const passed = isPartnership ? allPassed : allPassed.slice(0, 2);
                const jobStatus = isPartnership ? 'pending_coach_approval' : 'under_analysis';
                if (passed.length) {
                    const jtPath = path.join(menteesDir, slug, 'jobtracking.json');
                    const prefix = slug.split('_')[0].split('-').map(p=>p[0]).join('').toLowerCase();
                    let maxId = (jt.jobs||[]).reduce((m,j)=>{ const n=String(j.id||'').match(/(\d+)/); return n?Math.max(m,parseInt(n[1])):m; },0);
                    for (const j of passed) {
                        maxId++;
                        jt.jobs.push({ id:`${prefix}${String(maxId).padStart(3,'0')}_js`, title:j.title, company:j.company, location:j.location, url:j.url||'', source:(j.source||'web').toLowerCase(), status:jobStatus, foundAt:new Date().toISOString(), notifiedAt:null, aiLevel:j.aiLevel||null, aiReason:j.aiReason||null, notes:j.aiReason?`AI: ${j.aiReason}`:null });
                    }
                    fs.writeFileSync(jtPath, JSON.stringify(jt, null, 2));
                    // Partnership: notify coach by email
                    if (isPartnership) {
                        const coachName = cs.coach || '';
                        const coachRecord = readCoaches().find(c => c.name === coachName);
                        if (coachRecord && coachRecord.email) {
                            const profileUrl = `${APP_URL}${BASE_PATH}/profile/${slug}`;
                            sendCoachPendingJobsEmail(coachRecord.email, coachName, cs.leadName || slug, passed, profileUrl).catch(() => {});
                        }
                    }
                }
                results.push({ slug, name: cs.leadName || slug, found: allJobs.length, passed: passed.length, plan: plan, jobs: passed.map(j => `${j.title} @ ${j.company} (${j.location})`) });
            } catch(e) { results.push({ slug, name: cs?.leadName || slug, error: e.message }); }
        }
        const ts = new Date().toISOString();
        const logLines = [`\n=== Daily Run ${ts} ===`];
        for (const r of results) {
            if (r.error)   logLines.push(`  [ERROR] ${r.name}: ${r.error}`);
            else if (r.skipped) logLines.push(`  [SKIP]  ${r.name}: ${r.skipped}`);
            else {
                logLines.push(`  [OK]    ${r.name}: ${r.passed} new job(s) added (${r.found} found total)`);
                for (const j of (r.jobs || [])) logLines.push(`            • ${j}`);
            }
        }
        logLines.push('');
        fs.appendFile(path.join(BASE_DIR, 'dailyjobs.log'), logLines.join('\n'), () => {}); 
        return results;
    }

    // Approve a pending job (coach only) — moves to under_analysis so client can see it
    app.post(BASE_PATH + '/api/profile/:slug/jobtracking/:jobId/approve', requireAuth, (req, res) => {
        const { slug, jobId } = req.params;
        const menteeDir = findMenteeFolder(slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });
        const jtPath = path.join(menteeDir, 'jobtracking.json');
        let jt;
        try { jt = JSON.parse(fs.readFileSync(jtPath, 'utf8')); } catch { jt = { jobs: [] }; }
        const job = (jt.jobs || []).find(j => j.id === jobId);
        if (!job) return res.status(404).json({ error: 'Job not found' });
        job.status = 'under_analysis';
        job.approvedAt = new Date().toISOString();
        try { fs.writeFileSync(jtPath, JSON.stringify(jt, null, 2)); } catch (e) { return res.status(500).json({ error: e.message }); }
        appendChangelog(menteeDir, { who: 'coach', text: `Coach approved job: ${job.title} @ ${job.company}`, action: 'job_approved' });
        res.json({ success: true });
    });

    // Reject a pending job (coach only) — removes from list entirely
    app.post(BASE_PATH + '/api/profile/:slug/jobtracking/:jobId/reject', requireAuth, (req, res) => {
        const { slug, jobId } = req.params;
        const menteeDir = findMenteeFolder(slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });
        const jtPath = path.join(menteeDir, 'jobtracking.json');
        let jt;
        try { jt = JSON.parse(fs.readFileSync(jtPath, 'utf8')); } catch { jt = { jobs: [] }; }
        const job = (jt.jobs || []).find(j => j.id === jobId);
        if (!job) return res.status(404).json({ error: 'Job not found' });
        const title = job.title, company = job.company;
        jt.jobs = jt.jobs.filter(j => j.id !== jobId);
        try { fs.writeFileSync(jtPath, JSON.stringify(jt, null, 2)); } catch (e) { return res.status(500).json({ error: e.message }); }
        appendChangelog(menteeDir, { who: 'coach', text: `Coach rejected job (removed): ${title} @ ${company}`, action: 'job_rejected' });
        res.json({ success: true });
    });

    // Manual trigger (cron or external) — GET avoids LiteSpeed WAF blocking POST
    async function handleDailyRun(req, res) {
        const results = await runDailyJobs();
        res.json({ success: true, results });
    }
    app.get(BASE_PATH + '/api/daily-run',  requireBotToken, handleDailyRun);
    app.post(BASE_PATH + '/api/daily-run', requireBotToken, handleDailyRun);

    // ====================== DOCUMENTS API ======================

    // GET list (coach)
    app.get(BASE_PATH + '/api/profile/:slug/documents', requireAuth, (req, res) => {
        const menteeDir = findMenteeFolder(req.params.slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });
        res.json({ success: true, documents: readDocIndex(menteeDir) });
    });

    // POST upload → AI name (coach) — stores in buffer, returns suggestedName + tempId
    app.post(BASE_PATH + '/api/profile/:slug/documents/upload', requireAuth, async (req, res) => {
        const { data, mimeType, originalName } = req.body;
        if (!data || !mimeType || !originalName) return res.status(400).json({ error: 'data, mimeType, originalName required' });
        const buf = Buffer.from(data, 'base64');
        if (buf.length > 1024 * 1024) return res.status(413).json({ error: 'File too large (max 1 MB)' });
        const menteeDir = findMenteeFolder(req.params.slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });
        let clientName = 'Client';
        try { const cs = JSON.parse(fs.readFileSync(path.join(menteeDir, 'career-strategy.json'), 'utf8')); clientName = cs.leadName || cs.clientName || clientName; } catch {}
        try {
            const suggestedName = await aiNameDocument(clientName, buf, mimeType, originalName);
            const tempId = crypto.randomUUID();
            docUploadBuffer[tempId] = { buf, mimeType, originalName, menteeDir, clientName };
            setTimeout(() => { delete docUploadBuffer[tempId]; }, 10 * 60 * 1000);
            res.json({ success: true, suggestedName, tempId });
        } catch (e) {
            res.status(500).json({ error: 'AI naming failed: ' + e.message });
        }
    });

    // POST confirm → save to disk + index (coach)
    app.post(BASE_PATH + '/api/profile/:slug/documents/confirm', requireAuth, (req, res) => {
        const { tempId, displayName } = req.body;
        if (!tempId || !displayName) return res.status(400).json({ error: 'tempId and displayName required' });
        const session = docUploadBuffer[tempId];
        if (!session) return res.status(404).json({ error: 'Upload session expired, please re-upload' });
        const { buf, mimeType, originalName, menteeDir } = session;
        const ext = (originalName.split('.').pop() || 'bin').toLowerCase();
        const docId = crypto.randomUUID();
        const filename = `${docId}.${ext}`;
        const docsDir = path.join(menteeDir, 'documents');
        if (!fs.existsSync(docsDir)) fs.mkdirSync(docsDir, { recursive: true });
        try {
            fs.writeFileSync(path.join(docsDir, filename), buf);
            const docs = readDocIndex(menteeDir);
            const entry = { id: docId, filename, displayName, originalName, mimeType, size: buf.length, uploadedBy: 'coach', uploadedAt: new Date().toISOString() };
            docs.unshift(entry);
            writeDocIndex(menteeDir, docs);
            delete docUploadBuffer[tempId];
            res.json({ success: true, document: entry });
        } catch (e) { res.status(500).json({ error: e.message }); }
    });

    // DELETE doc (coach)
    app.delete(BASE_PATH + '/api/profile/:slug/documents/:id', requireAuth, (req, res) => {
        const menteeDir = findMenteeFolder(req.params.slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });
        const docs = readDocIndex(menteeDir);
        const idx = docs.findIndex(d => d.id === req.params.id);
        if (idx === -1) return res.status(404).json({ error: 'Document not found' });
        const doc = docs[idx];
        try {
            const fp = path.join(menteeDir, 'documents', doc.filename);
            if (fs.existsSync(fp)) fs.unlinkSync(fp);
            docs.splice(idx, 1);
            writeDocIndex(menteeDir, docs);
            res.json({ success: true });
        } catch (e) { res.status(500).json({ error: e.message }); }
    });

    // GET download (coach)
    app.get(BASE_PATH + '/api/profile/:slug/documents/:id', requireAuth, (req, res) => {
        const menteeDir = findMenteeFolder(req.params.slug);
        if (!menteeDir) return res.status(404).json({ error: 'Mentee not found' });
        const docs = readDocIndex(menteeDir);
        const doc = docs.find(d => d.id === req.params.id);
        if (!doc) return res.status(404).json({ error: 'Document not found' });
        const fp = path.join(menteeDir, 'documents', doc.filename);
        if (!fs.existsSync(fp)) return res.status(404).json({ error: 'File not found on disk' });
        const ext = doc.filename.split('.').pop();
        res.setHeader('Content-Type', doc.mimeType || 'application/octet-stream');
        res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(doc.displayName)}.${ext}"`);
        res.sendFile(path.resolve(fp));
    });

    // ── Client document routes ──

    function getClientMenteeDir(req) {
        const s = getClientSession(req);
        return findMenteeFolder(s.clientSlug.split('_')[0]) || path.join(BASE_DIR, 'src', 'db', 'mentees', s.clientSlug);
    }

    app.get(BASE_PATH + '/api/client/documents', requireClientAuth, (req, res) => {
        const menteeDir = getClientMenteeDir(req);
        res.json({ success: true, documents: readDocIndex(menteeDir) });
    });

    app.post(BASE_PATH + '/api/client/documents/upload', requireClientAuth, async (req, res) => {
        const { data, mimeType, originalName } = req.body;
        if (!data || !mimeType || !originalName) return res.status(400).json({ error: 'data, mimeType, originalName required' });
        const buf = Buffer.from(data, 'base64');
        if (buf.length > 1024 * 1024) return res.status(413).json({ error: 'File too large (max 1 MB)' });
        const menteeDir = getClientMenteeDir(req);
        let clientName = 'Client';
        try { const cs = JSON.parse(fs.readFileSync(path.join(menteeDir, 'career-strategy.json'), 'utf8')); clientName = cs.leadName || cs.clientName || clientName; } catch {}
        try {
            const suggestedName = await aiNameDocument(clientName, buf, mimeType, originalName);
            const tempId = crypto.randomUUID();
            docUploadBuffer[tempId] = { buf, mimeType, originalName, menteeDir, clientName };
            setTimeout(() => { delete docUploadBuffer[tempId]; }, 10 * 60 * 1000);
            res.json({ success: true, suggestedName, tempId });
        } catch (e) {
            res.status(500).json({ error: 'AI naming failed: ' + e.message });
        }
    });

    app.post(BASE_PATH + '/api/client/documents/confirm', requireClientAuth, (req, res) => {
        const { tempId, displayName } = req.body;
        if (!tempId || !displayName) return res.status(400).json({ error: 'tempId and displayName required' });
        const session = docUploadBuffer[tempId];
        if (!session) return res.status(404).json({ error: 'Upload session expired, please re-upload' });
        const { buf, mimeType, originalName, menteeDir } = session;
        const ext = (originalName.split('.').pop() || 'bin').toLowerCase();
        const docId = crypto.randomUUID();
        const filename = `${docId}.${ext}`;
        const docsDir = path.join(menteeDir, 'documents');
        if (!fs.existsSync(docsDir)) fs.mkdirSync(docsDir, { recursive: true });
        try {
            fs.writeFileSync(path.join(docsDir, filename), buf);
            const docs = readDocIndex(menteeDir);
            const entry = { id: docId, filename, displayName, originalName, mimeType, size: buf.length, uploadedBy: 'client', uploadedAt: new Date().toISOString() };
            docs.unshift(entry);
            writeDocIndex(menteeDir, docs);
            delete docUploadBuffer[tempId];
            res.json({ success: true, document: entry });
        } catch (e) { res.status(500).json({ error: e.message }); }
    });

    app.delete(BASE_PATH + '/api/client/documents/:id', requireClientAuth, (req, res) => {
        const menteeDir = getClientMenteeDir(req);
        const docs = readDocIndex(menteeDir);
        const idx = docs.findIndex(d => d.id === req.params.id);
        if (idx === -1) return res.status(404).json({ error: 'Document not found' });
        const doc = docs[idx];
        try {
            const fp = path.join(menteeDir, 'documents', doc.filename);
            if (fs.existsSync(fp)) fs.unlinkSync(fp);
            docs.splice(idx, 1);
            writeDocIndex(menteeDir, docs);
            res.json({ success: true });
        } catch (e) { res.status(500).json({ error: e.message }); }
    });

    app.get(BASE_PATH + '/api/client/documents/:id', requireClientAuth, (req, res) => {
        const menteeDir = getClientMenteeDir(req);
        const docs = readDocIndex(menteeDir);
        const doc = docs.find(d => d.id === req.params.id);
        if (!doc) return res.status(404).json({ error: 'Document not found' });
        const fp = path.join(menteeDir, 'documents', doc.filename);
        if (!fs.existsSync(fp)) return res.status(404).json({ error: 'File not found on disk' });
        const ext = doc.filename.split('.').pop();
        res.setHeader('Content-Type', doc.mimeType || 'application/octet-stream');
        res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(doc.displayName)}.${ext}"`);
        res.sendFile(path.resolve(fp));
    });

    // Pass unmatched routes to next app
    app.use((req, res, next) => next());

    module.exports = app;