// Generates PS_Launcher-Documentation-Interne.pptx
const pptxgen = require("pptxgenjs");

const COL = {
    bg:        "000000",
    card:      "161B23",
    elev:      "1E2632",
    border:    "2A2F3A",
    text:      "F2F2F2",
    muted:     "A0A0A8",
    accent:    "3B82F6",
    accentDk:  "1F3A66",
    green:     "16A34A",
    greenDk:   "0E2A1B",
    amber:     "F59E0B",
    amberDk:   "3A2A0E",
    red:       "EF4444",
    redDk:     "2A1414",
    codeBg:    "0A0E14",
};

const F_TITLE = "Calibri";
const F_BODY  = "Calibri";
const F_CODE  = "Consolas";

const pres = new pptxgen();
pres.layout = "LAYOUT_16x9";
pres.author = "ASTERION VR";
pres.title  = "PS_Launcher — Documentation Interne";
pres.company = "ASTERION VR";

const W = 10, H = 5.625;

// ===================== HELPERS =====================
function fillBg(slide) {
    slide.background = { color: COL.bg };
}

function addHeader(slide, title, kicker) {
    // Top accent bar
    slide.addShape(pres.shapes.RECTANGLE, {
        x: 0, y: 0, w: W, h: 0.06, fill: { color: COL.accent }, line: { type: "none" }
    });
    if (kicker) {
        slide.addText(kicker.toUpperCase(), {
            x: 0.5, y: 0.25, w: W - 1, h: 0.3,
            fontFace: F_BODY, fontSize: 11, bold: true,
            color: COL.accent, charSpacing: 4, margin: 0
        });
    }
    slide.addText(title, {
        x: 0.5, y: kicker ? 0.55 : 0.35, w: W - 1, h: 0.7,
        fontFace: F_TITLE, fontSize: 32, bold: true,
        color: COL.text, margin: 0
    });
    // Underline-ish thin separator
    slide.addShape(pres.shapes.LINE, {
        x: 0.5, y: kicker ? 1.3 : 1.15, w: 1.2, h: 0,
        line: { color: COL.accent, width: 2 }
    });
}

function addFooter(slide, page) {
    slide.addText("PS_LAUNCHER  •  Documentation Interne  •  ASTERION VR", {
        x: 0.5, y: H - 0.35, w: W - 2, h: 0.25,
        fontFace: F_BODY, fontSize: 9, color: COL.muted, margin: 0
    });
    if (page) {
        slide.addText(String(page), {
            x: W - 1, y: H - 0.35, w: 0.5, h: 0.25,
            fontFace: F_BODY, fontSize: 9, color: COL.muted, align: "right", margin: 0
        });
    }
}

function addCard(slide, x, y, w, h, options = {}) {
    slide.addShape(pres.shapes.RECTANGLE, {
        x, y, w, h,
        fill: { color: options.fill || COL.card },
        line: { color: options.border || COL.border, width: 1 },
    });
}

function addAccentCard(slide, x, y, w, h, accentColor) {
    addCard(slide, x, y, w, h);
    // left accent strip
    slide.addShape(pres.shapes.RECTANGLE, {
        x, y, w: 0.08, h,
        fill: { color: accentColor }, line: { type: "none" }
    });
}

function addCodeBlock(slide, x, y, w, h, lines) {
    slide.addShape(pres.shapes.RECTANGLE, {
        x, y, w, h, fill: { color: COL.codeBg }, line: { color: COL.border, width: 1 }
    });
    const text = (Array.isArray(lines) ? lines : [lines]).map((line, i, arr) => ({
        text: line,
        options: { breakLine: i < arr.length - 1 }
    }));
    slide.addText(text, {
        x: x + 0.15, y: y + 0.1, w: w - 0.3, h: h - 0.2,
        fontFace: F_CODE, fontSize: 11, color: COL.text, margin: 0,
        valign: "top"
    });
}

function addBullets(slide, x, y, w, h, items, opts = {}) {
    const fontSize = opts.fontSize || 14;
    const color = opts.color || COL.text;
    const arr = items.map((it, i) => ({
        text: it,
        options: { bullet: { code: "25A0" }, breakLine: i < items.length - 1, color }
    }));
    slide.addText(arr, {
        x, y, w, h, fontFace: F_BODY, fontSize, color, margin: 0,
        paraSpaceAfter: 6
    });
}

function addPill(slide, x, y, w, h, label, color) {
    slide.addShape(pres.shapes.ROUNDED_RECTANGLE, {
        x, y, w, h, fill: { color }, line: { type: "none" },
        rectRadius: h / 2
    });
    slide.addText(label, {
        x, y, w, h, fontFace: F_BODY, fontSize: 10, bold: true,
        color: "FFFFFF", align: "center", valign: "middle", margin: 0
    });
}

// ===================== SLIDE 1: TITLE =====================
{
    const s = pres.addSlide();
    fillBg(s);

    // Big colored band on the right
    s.addShape(pres.shapes.RECTANGLE, {
        x: 6.5, y: 0, w: 3.5, h: H, fill: { color: COL.accentDk }, line: { type: "none" }
    });

    // Vertical accent line
    s.addShape(pres.shapes.RECTANGLE, {
        x: 6.5, y: 0, w: 0.05, h: H, fill: { color: COL.accent }, line: { type: "none" }
    });

    // Brand label
    s.addText("ASTERION VR", {
        x: 0.6, y: 0.5, w: 4, h: 0.4,
        fontFace: F_BODY, fontSize: 13, bold: true,
        color: COL.accent, charSpacing: 8, margin: 0
    });

    s.addText("PS_Launcher", {
        x: 0.6, y: 1.5, w: 6, h: 1,
        fontFace: F_TITLE, fontSize: 56, bold: true,
        color: COL.text, margin: 0
    });

    s.addText("Documentation Interne", {
        x: 0.6, y: 2.5, w: 6, h: 0.7,
        fontFace: F_TITLE, fontSize: 28,
        color: COL.muted, margin: 0
    });

    s.addText("Déploiement, configuration & exploitation", {
        x: 0.6, y: 3.2, w: 6, h: 0.5,
        fontFace: F_BODY, fontSize: 16, italic: true,
        color: COL.muted, margin: 0
    });

    // Right column big version pill
    s.addText("v0.8", {
        x: 6.7, y: 1.5, w: 3.2, h: 1.2,
        fontFace: F_TITLE, fontSize: 76, bold: true,
        color: COL.text, align: "center", margin: 0
    });
    s.addText("VERSION", {
        x: 6.7, y: 2.6, w: 3.2, h: 0.3,
        fontFace: F_BODY, fontSize: 11,
        color: COL.muted, align: "center", charSpacing: 6, margin: 0
    });

    // Date
    s.addText("Mai 2026", {
        x: 6.7, y: 3.6, w: 3.2, h: 0.4,
        fontFace: F_BODY, fontSize: 14,
        color: COL.text, align: "center", margin: 0
    });

    // Bottom band
    s.addShape(pres.shapes.RECTANGLE, {
        x: 0, y: H - 0.5, w: W, h: 0.5, fill: { color: COL.card }, line: { type: "none" }
    });
    s.addText("Confidentiel — usage interne uniquement", {
        x: 0.5, y: H - 0.45, w: W - 1, h: 0.4,
        fontFace: F_BODY, fontSize: 11, italic: true,
        color: COL.muted, margin: 0
    });
}

// ===================== SLIDE 2: SOMMAIRE =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Sommaire");
    addFooter(s, 2);

    const sections = [
        ["1", "Architecture", "Vue d'ensemble client-serveur"],
        ["2", "Setup OVH initial", "MySQL, config.php, clés Ed25519"],
        ["3", "Tour du backoffice", "Dashboard, Licenses, Versions, Launcher, Audit"],
        ["4", "Workflow de release", "Proserve & launcher"],
        ["5", "Gestion des licenses", "Émission, prolongation, révocation"],
        ["6", "Sécurité", "URLs HMAC, signatures Ed25519, DPAPI"],
        ["7", "Exploitation", "Logs, fichiers, troubleshooting"],
    ];

    let y = 1.6;
    sections.forEach(([num, title, desc]) => {
        // Number circle
        s.addShape(pres.shapes.OVAL, {
            x: 0.5, y, w: 0.5, h: 0.5,
            fill: { color: COL.accent }, line: { type: "none" }
        });
        s.addText(num, {
            x: 0.5, y, w: 0.5, h: 0.5,
            fontFace: F_TITLE, fontSize: 18, bold: true,
            color: "FFFFFF", align: "center", valign: "middle", margin: 0
        });
        // Title
        s.addText(title, {
            x: 1.2, y: y - 0.05, w: 4, h: 0.35,
            fontFace: F_TITLE, fontSize: 16, bold: true,
            color: COL.text, margin: 0
        });
        // Description
        s.addText(desc, {
            x: 1.2, y: y + 0.27, w: 6, h: 0.3,
            fontFace: F_BODY, fontSize: 12,
            color: COL.muted, margin: 0
        });
        y += 0.55;
    });
}

// ===================== SLIDE 3: ARCHITECTURE =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Architecture", "section 1");
    addFooter(s, 3);

    // Three boxes : Client - HTTPS - OVH
    const boxY = 1.8, boxH = 2.2, boxW = 2.7;

    // Client
    addAccentCard(s, 0.5, boxY, boxW, boxH, COL.green);
    s.addText("CLIENT", {
        x: 0.7, y: boxY + 0.1, w: boxW - 0.3, h: 0.3,
        fontFace: F_BODY, fontSize: 10, bold: true, color: COL.muted, charSpacing: 4, margin: 0
    });
    s.addText("PSLauncher.exe", {
        x: 0.7, y: boxY + 0.4, w: boxW - 0.3, h: 0.4,
        fontFace: F_TITLE, fontSize: 18, bold: true, color: COL.text, margin: 0
    });
    addBullets(s, 0.7, boxY + 0.85, boxW - 0.3, 1.3, [
        "WPF / .NET 8",
        "License DPAPI locale",
        "Cache & logs",
        "Updater intégré"
    ], { fontSize: 11, color: COL.muted });

    // Arrow / HTTPS
    s.addShape(pres.shapes.LINE, {
        x: 3.3, y: boxY + 1.1, w: 0.8, h: 0,
        line: { color: COL.accent, width: 2.5 }
    });
    s.addText("HTTPS", {
        x: 3.3, y: boxY + 0.7, w: 0.8, h: 0.3,
        fontFace: F_BODY, fontSize: 11, bold: true, color: COL.accent, align: "center", margin: 0
    });

    // OVH
    addAccentCard(s, 4.15, boxY, boxW, boxH, COL.accent);
    s.addText("SERVEUR", {
        x: 4.35, y: boxY + 0.1, w: boxW - 0.3, h: 0.3,
        fontFace: F_BODY, fontSize: 10, bold: true, color: COL.muted, charSpacing: 4, margin: 0
    });
    s.addText("OVH mutualisé", {
        x: 4.35, y: boxY + 0.4, w: boxW - 0.3, h: 0.4,
        fontFace: F_TITLE, fontSize: 18, bold: true, color: COL.text, margin: 0
    });
    addBullets(s, 4.35, boxY + 0.85, boxW - 0.3, 1.3, [
        "PHP 8.3 + Apache",
        "API REST + backoffice",
        "Manifest signé Ed25519",
        "Builds ZIPs servis HMAC"
    ], { fontSize: 11, color: COL.muted });

    // Arrow / SQL
    s.addShape(pres.shapes.LINE, {
        x: 6.95, y: boxY + 1.1, w: 0.8, h: 0,
        line: { color: COL.accent, width: 2.5 }
    });
    s.addText("PDO", {
        x: 6.95, y: boxY + 0.7, w: 0.8, h: 0.3,
        fontFace: F_BODY, fontSize: 11, bold: true, color: COL.accent, align: "center", margin: 0
    });

    // MySQL
    addAccentCard(s, 7.8, boxY, boxW - 0.5, boxH, COL.amber);
    s.addText("BASE", {
        x: 8, y: boxY + 0.1, w: boxW - 0.7, h: 0.3,
        fontFace: F_BODY, fontSize: 10, bold: true, color: COL.muted, charSpacing: 4, margin: 0
    });
    s.addText("MySQL OVH", {
        x: 8, y: boxY + 0.4, w: boxW - 0.7, h: 0.4,
        fontFace: F_TITLE, fontSize: 18, bold: true, color: COL.text, margin: 0
    });
    addBullets(s, 8, boxY + 0.85, boxW - 0.7, 1.3, [
        "licenses",
        "license_machines",
        "rate_limit",
        "audit_log"
    ], { fontSize: 11, color: COL.muted });

    // Footnote
    s.addText("Le launcher conserve sa license en cache local (DPAPI scope CurrentUser) et fonctionne offline 7 jours après la dernière validation serveur.", {
        x: 0.5, y: 4.4, w: W - 1, h: 0.6,
        fontFace: F_BODY, fontSize: 11, italic: true, color: COL.muted, margin: 0
    });
}

// ===================== SLIDE 4: SETUP OVH =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Setup OVH initial", "section 2 — première fois uniquement");
    addFooter(s, 4);

    const steps = [
        ["1", "Créer la base MySQL", "Manager OVH → Bases de données → Créer.\nNoter host, user, password, nom."],
        ["2", "Jouer la migration", "PhpMyAdmin OVH → onglet SQL → coller migrations/001_init.sql\n(crée 4 tables : licenses, license_machines, rate_limit, audit_log)"],
        ["3", "Uploader le code en SFTP", "server/ → www/PS_Launcher/\nRespecter l'arborescence : api/, admin/, manifest/, builds/, tools/, migrations/"],
        ["4", "Créer api/config.php", "Copier api/config.example.php → api/config.php\nÉditer DSN MySQL, secrets HMAC/JWT, hash bcrypt admin, paire Ed25519"],
    ];

    let y = 1.5;
    steps.forEach(([num, title, desc]) => {
        addCard(s, 0.5, y, W - 1, 0.85);
        // Number
        s.addShape(pres.shapes.OVAL, {
            x: 0.7, y: y + 0.2, w: 0.45, h: 0.45,
            fill: { color: COL.accent }, line: { type: "none" }
        });
        s.addText(num, {
            x: 0.7, y: y + 0.2, w: 0.45, h: 0.45,
            fontFace: F_TITLE, fontSize: 16, bold: true,
            color: "FFFFFF", align: "center", valign: "middle", margin: 0
        });
        // Title
        s.addText(title, {
            x: 1.4, y: y + 0.1, w: 7.5, h: 0.3,
            fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
        });
        // Desc
        s.addText(desc, {
            x: 1.4, y: y + 0.4, w: 7.5, h: 0.4,
            fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
        });
        y += 0.95;
    });
}

// ===================== SLIDE 5: CONFIG.PHP =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "config.php — secrets serveur", "section 2");
    addFooter(s, 5);

    addCodeBlock(s, 0.5, 1.5, W - 1, 2.6, [
        "<?php return [",
        "    'base_url' => 'https://asterionvr.com/PS_Launcher',",
        "    'db' => [",
        "        'dsn'      => 'mysql:host=xxx.mysql.db;dbname=xxx;charset=utf8mb4',",
        "        'username' => '...', 'password' => '...',",
        "    ],",
        "    'hmac_secret' => '<64 hex chars>',                  // bin2hex(random_bytes(32))",
        "    'ed25519' => [",
        "        'private_key_hex' => '<128 hex>',                // tools/generate-keypair.php",
        "        'public_key_hex'  => '<64 hex>',                 // → idem + Resources/server-pubkey.txt",
        "    ],",
        "    'jwt_secret' => '<64 hex>',",
        "    'admin_password_hash' => '$2y$10$...'                // password_hash('...')",
        "];",
    ]);

    // Tip card
    addAccentCard(s, 0.5, 4.3, W - 1, 0.85, COL.amber);
    s.addText("⚠  Point critique", {
        x: 0.7, y: 4.4, w: 3, h: 0.3,
        fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.amber, margin: 0
    });
    s.addText("public_key_hex doit être recopié à l'identique dans Resources/server-pubkey.txt côté projet C# avant un nouveau publish — sinon le launcher refusera la signature serveur.", {
        x: 0.7, y: 4.7, w: W - 1.4, h: 0.5,
        fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
    });
}

// ===================== SLIDE 6: GENERATE KEYS =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Génération des secrets", "section 2");
    addFooter(s, 6);

    // Two columns
    const c1 = 0.5, c2 = 5.2, cw = 4.3;

    // Col 1 : Ed25519
    s.addText("🔐  Keypair Ed25519", {
        x: c1, y: 1.4, w: cw, h: 0.4,
        fontFace: F_TITLE, fontSize: 15, bold: true, color: COL.text, margin: 0
    });
    s.addText("Signe le manifest et la réponse de validation license.", {
        x: c1, y: 1.7, w: cw, h: 0.4,
        fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
    });
    addCodeBlock(s, c1, 2.1, cw, 0.7, [
        "$ cd ~/www/PS_Launcher",
        "$ php tools/generate-keypair.php",
    ]);
    addBullets(s, c1, 2.95, cw, 1.6, [
        "private_key_hex → api/config.php uniquement",
        "public_key_hex  → api/config.php ET",
        "    Resources/server-pubkey.txt",
    ], { fontSize: 11 });

    // Col 2 : Admin password
    s.addText("🔑  Mot de passe admin", {
        x: c2, y: 1.4, w: cw, h: 0.4,
        fontFace: F_TITLE, fontSize: 15, bold: true, color: COL.text, margin: 0
    });
    s.addText("Bcrypt — pour la connexion à /admin/.", {
        x: c2, y: 1.7, w: cw, h: 0.4,
        fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
    });
    addCodeBlock(s, c2, 2.1, cw, 1.3, [
        "$ php -r 'echo password_hash(",
        "    \"<motdepasse>\",",
        "    PASSWORD_DEFAULT) . \"\\n\";'",
    ]);
    addBullets(s, c2, 3.55, cw, 1.0, [
        "Hash dans api/config.php",
        "    → admin_password_hash",
    ], { fontSize: 11 });

    // hmac/jwt
    addCard(s, 0.5, 4.7, W - 1, 0.65);
    s.addText("🎲  HMAC & JWT secrets", {
        x: 0.7, y: 4.8, w: 4, h: 0.25,
        fontFace: F_TITLE, fontSize: 12, bold: true, color: COL.text, margin: 0
    });
    s.addText("Générer 64 hex chars : php -r \"echo bin2hex(random_bytes(32));\"", {
        x: 0.7, y: 5.0, w: W - 1.4, h: 0.3,
        fontFace: F_CODE, fontSize: 11, color: COL.muted, margin: 0
    });
}

// ===================== SLIDE 7: BACKOFFICE - DASHBOARD =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Backoffice — Dashboard", "section 3 · vue d'ensemble");
    addFooter(s, 7);

    s.addText("URL : https://asterionvr.com/PS_Launcher/admin/", {
        x: 0.5, y: 1.4, w: W - 1, h: 0.3,
        fontFace: F_CODE, fontSize: 12, color: COL.accent, margin: 0
    });

    // KPI grid 5 cells
    const kpiW = 1.85, kpiH = 1.0, kpiY = 1.95, gap = 0.05;
    const kpis = [
        ["Licenses actives",   "•",  COL.green],
        ["Expirées",           "•",  COL.amber],
        ["Révoquées",          "•",  COL.red],
        ["Machines vues 30j",  "•",  COL.text],
        ["Validations 24h",    "•",  COL.text],
    ];
    kpis.forEach((kpi, i) => {
        const [label, val, color] = kpi;
        const x = 0.5 + i * (kpiW + gap);
        addCard(s, x, kpiY, kpiW, kpiH);
        s.addText(label.toUpperCase(), {
            x: x + 0.1, y: kpiY + 0.1, w: kpiW - 0.2, h: 0.3,
            fontFace: F_BODY, fontSize: 9, bold: true, color: COL.muted, charSpacing: 2, margin: 0
        });
        s.addText(val, {
            x: x + 0.1, y: kpiY + 0.35, w: kpiW - 0.2, h: 0.55,
            fontFace: F_TITLE, fontSize: 32, bold: true, color, margin: 0
        });
    });

    // Liste features
    s.addText("Ce que tu y vois", {
        x: 0.5, y: 3.2, w: W - 1, h: 0.3,
        fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
    });
    addBullets(s, 0.5, 3.5, W - 1, 1.3, [
        "Compteurs licenses + machines + validations en temps réel",
        "État du dépôt de versions (manifest signé / non signé, version latest)",
        "10 dernières lignes de l'audit log avec leur sévérité (validate_ok, expired, revoked, …)",
    ], { fontSize: 12 });

    // Bottom callout
    addAccentCard(s, 0.5, 4.85, W - 1, 0.5, COL.accent);
    s.addText("✓  Page d'entrée — pas d'action destructive ici, c'est le tableau de bord lecture seule.", {
        x: 0.7, y: 4.9, w: W - 1.4, h: 0.4,
        fontFace: F_BODY, fontSize: 11, color: COL.text, margin: 0
    });
}

// ===================== SLIDE 8: BACKOFFICE - LICENSES =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Backoffice — Licenses", "section 3 · gestion clients");
    addFooter(s, 8);

    s.addText("Émettre, prolonger, révoquer, libérer les machines occupées.", {
        x: 0.5, y: 1.4, w: W - 1, h: 0.3,
        fontFace: F_BODY, fontSize: 13, italic: true, color: COL.muted, margin: 0
    });

    // Workflow boxes
    const flow = [
        ["Émettre", "Formulaire en haut\nNom client, date d'expiration, max machines, notes\n→ Clé PRSRV-XXXX-XXXX-XXXX-XXXX affichée UNE FOIS", COL.green],
        ["Prolonger", "Bouton Prolonger / ligne\n→ Champ date → la nouvelle date d'expiration prend effet immédiatement côté client à la prochaine validation", COL.accent],
        ["Révoquer", "Bouton rouge / ligne\n→ revoked_at = NOW()\nLe client perd ses droits de DL au prochain check, le launcher reste utilisable sur les versions déjà installées", COL.red],
        ["Libérer machines", "Si un client change de PC, ses anciennes machines occupent un slot\n→ Bouton Libérer machines vide la table license_machines pour cette license", COL.amber],
    ];

    let y = 1.85;
    flow.forEach(([title, desc, color]) => {
        addAccentCard(s, 0.5, y, W - 1, 0.75, color);
        s.addText(title, {
            x: 0.75, y: y + 0.1, w: 2, h: 0.3,
            fontFace: F_TITLE, fontSize: 14, bold: true, color, margin: 0
        });
        s.addText(desc, {
            x: 2.7, y: y + 0.05, w: W - 3.2, h: 0.7,
            fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
        });
        y += 0.85;
    });
}

// ===================== SLIDE 9: BACKOFFICE - VERSIONS =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Backoffice — Versions Proserve", "section 3 · catalogue des builds");
    addFooter(s, 9);

    s.addText("Édite manifest/versions.json depuis le web (sans toucher au JSON à la main).", {
        x: 0.5, y: 1.4, w: W - 1, h: 0.3,
        fontFace: F_BODY, fontSize: 13, italic: true, color: COL.muted, margin: 0
    });

    // Workflow numbered
    const steps = [
        ["1", "Ajouter version", "Formulaire haut → Numéro X.Y.Z, date min de license requise, release notes en Markdown."],
        ["2", "Upload SFTP du ZIP", "Dans builds/proserve-X.Y.Z.zip — taille typique 14 Go par build UE5."],
        ["3", "Hasher + signer", "Bouton bleu 🔁 Hasher les versions + signer.\nCalcule sizeBytes + sha256, bump latest, signe Ed25519."],
        ["4", "Édition fine", "Méta (date, minLicenseDate), Notes (Markdown), toggle disponibilité, retirer du manifest."],
    ];

    let y = 1.85;
    steps.forEach(([num, title, desc]) => {
        addCard(s, 0.5, y, W - 1, 0.75);
        s.addShape(pres.shapes.OVAL, {
            x: 0.7, y: y + 0.18, w: 0.4, h: 0.4,
            fill: { color: COL.accent }, line: { type: "none" }
        });
        s.addText(num, {
            x: 0.7, y: y + 0.18, w: 0.4, h: 0.4,
            fontFace: F_TITLE, fontSize: 14, bold: true,
            color: "FFFFFF", align: "center", valign: "middle", margin: 0
        });
        s.addText(title, {
            x: 1.3, y: y + 0.1, w: 8, h: 0.3,
            fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.text, margin: 0
        });
        s.addText(desc, {
            x: 1.3, y: y + 0.4, w: 8, h: 0.35,
            fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
        });
        y += 0.85;
    });
}

// ===================== SLIDE 10: BACKOFFICE - LAUNCHER =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Backoffice — Launcher (auto-update)", "section 3 · MAJ du launcher lui-même");
    addFooter(s, 10);

    s.addText("Quand tu pousses une nouvelle version de PSLauncher.exe, les launchers déjà déployés se mettent à jour seuls. Cette page pilote ce flux.", {
        x: 0.5, y: 1.4, w: W - 1, h: 0.45,
        fontFace: F_BODY, fontSize: 12, italic: true, color: COL.muted, margin: 0
    });

    // Two columns: Status & Workflow
    const colY = 2.0;

    // Status card
    addCard(s, 0.5, colY, 4.3, 2.6);
    s.addText("État affiché en haut", {
        x: 0.7, y: colY + 0.15, w: 4, h: 0.3,
        fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
    });
    addBullets(s, 0.7, colY + 0.55, 4, 2, [
        "Version annoncée (ex : v0.8.0)",
        "minRequired (plancher de compatibilité)",
        "Exe présent / absent dans builds/launcher/",
        "Hash calculé / à calculer",
        "Manifest signé / non signé",
    ], { fontSize: 11 });

    // Workflow card
    addCard(s, 5.2, colY, 4.3, 2.6);
    s.addText("Workflow nouvelle version", {
        x: 5.4, y: colY + 0.15, w: 4, h: 0.3,
        fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
    });
    addBullets(s, 5.4, colY + 0.55, 4, 2, [
        "build-launcher.bat (publie l'exe)",
        "Renommer en PSLauncher-X.Y.Z.exe",
        "Upload SFTP dans builds/launcher/",
        "Définir version dans le formulaire",
        "Bouton bleu : 🔁 Hasher le launcher + signer",
    ], { fontSize: 11 });

    // Bottom note
    addAccentCard(s, 0.5, 4.7, W - 1, 0.55, COL.accent);
    s.addText("ℹ  Les clients en Program Files passeront un UAC à la mise à jour ; les nouveaux installs (user-mode) n'en demanderont jamais.", {
        x: 0.7, y: 4.78, w: W - 1.4, h: 0.4,
        fontFace: F_BODY, fontSize: 11, color: COL.text, margin: 0
    });
}

// ===================== SLIDE 11: BACKOFFICE - AUDIT =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Backoffice — Audit log", "section 3 · traçabilité");
    addFooter(s, 11);

    s.addText("Toutes les validations license + émissions d'URL signées sont enregistrées avec horodatage, IP et détail JSON.", {
        x: 0.5, y: 1.4, w: W - 1, h: 0.4,
        fontFace: F_BODY, fontSize: 12, italic: true, color: COL.muted, margin: 0
    });

    // Event types table
    addCard(s, 0.5, 1.95, W - 1, 3);
    s.addText("Événements possibles", {
        x: 0.7, y: 2.05, w: W - 1.4, h: 0.3,
        fontFace: F_TITLE, fontSize: 14, bold: true, color: COL.text, margin: 0
    });

    const events = [
        ["validate_ok",            "License validée avec succès",          COL.green],
        ["validate_expired",       "Clé connue mais date dépassée",        COL.amber],
        ["validate_revoked",       "License révoquée par l'admin",         COL.red],
        ["validate_invalid",       "Clé inconnue (faute de frappe)",       COL.red],
        ["machine_limit",          "Slot machines max atteint",            COL.red],
        ["download_url_issued",    "URL HMAC signée pour DL d'un ZIP",     COL.accent],
    ];

    let y = 2.45;
    events.forEach(([code, desc, color]) => {
        // Pill
        addPill(s, 0.7, y, 2.0, 0.28, code, color);
        // Description
        s.addText(desc, {
            x: 2.85, y: y - 0.04, w: 6, h: 0.35,
            fontFace: F_BODY, fontSize: 11, color: COL.muted, margin: 0
        });
        y += 0.4;
    });
}

// ===================== SLIDE 12: WORKFLOW PROSERVE =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Workflow release — Proserve", "section 4");
    addFooter(s, 12);

    // Big timeline
    const stepW = 1.7, stepGap = 0.18, sy = 1.7;
    const steps = [
        ["1", "Package", "Unreal\n→ ZIP de\nla version"],
        ["2", "SFTP", "Upload dans\nbuilds/\n(14 Go)"],
        ["3", "Backoffice", "Onglet Versions\nAjouter X.Y.Z\nRelease notes"],
        ["4", "Sign", "Bouton bleu\n🔁 Hasher\n+ signer"],
        ["5", "✓", "Manifest signé\nClients voient\nla MAJ"],
    ];
    steps.forEach((step, i) => {
        const x = 0.5 + i * (stepW + stepGap);
        const [num, title, desc] = step;
        const isLast = i === steps.length - 1;
        addCard(s, x, sy, stepW, 2.6, {
            fill: isLast ? COL.greenDk : COL.card,
            border: isLast ? COL.green : COL.border
        });
        s.addShape(pres.shapes.OVAL, {
            x: x + (stepW - 0.55) / 2, y: sy + 0.2, w: 0.55, h: 0.55,
            fill: { color: isLast ? COL.green : COL.accent }, line: { type: "none" }
        });
        s.addText(num, {
            x: x + (stepW - 0.55) / 2, y: sy + 0.2, w: 0.55, h: 0.55,
            fontFace: F_TITLE, fontSize: 18, bold: true,
            color: "FFFFFF", align: "center", valign: "middle", margin: 0
        });
        s.addText(title, {
            x: x + 0.1, y: sy + 0.85, w: stepW - 0.2, h: 0.35,
            fontFace: F_TITLE, fontSize: 13, bold: true,
            color: COL.text, align: "center", margin: 0
        });
        s.addText(desc, {
            x: x + 0.1, y: sy + 1.25, w: stepW - 0.2, h: 1.2,
            fontFace: F_BODY, fontSize: 11, color: COL.muted, align: "center", margin: 0
        });

        // Connecting arrow
        if (i < steps.length - 1) {
            s.addShape(pres.shapes.LINE, {
                x: x + stepW, y: sy + 0.475, w: stepGap, h: 0,
                line: { color: COL.border, width: 1.5 }
            });
        }
    });

    // Notes
    addAccentCard(s, 0.5, 4.6, W - 1, 0.65, COL.amber);
    s.addText("⚠  Tant que le ZIP n'est pas hashé/signé, les clients ne le verront pas — la signature Ed25519 prouve l'intégrité.", {
        x: 0.7, y: 4.7, w: W - 1.4, h: 0.5,
        fontFace: F_BODY, fontSize: 11, color: COL.text, margin: 0
    });
}

// ===================== SLIDE 13: WORKFLOW LAUNCHER =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Workflow release — Launcher", "section 4");
    addFooter(s, 13);

    s.addText("Pour pousser une nouvelle version du launcher elle-même (auto-update transparent côté client).", {
        x: 0.5, y: 1.4, w: W - 1, h: 0.3,
        fontFace: F_BODY, fontSize: 12, italic: true, color: COL.muted, margin: 0
    });

    // Two columns: scripts / steps
    addCard(s, 0.5, 1.85, 4.3, 3.3);
    s.addText("Scripts disponibles à la racine", {
        x: 0.7, y: 1.95, w: 4, h: 0.3,
        fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.text, margin: 0
    });
    const scripts = [
        ["build-launcher.bat", "Publie PSLauncher.exe (~78 Mo)"],
        ["build-updater.bat",  "Publie PSLauncher.Updater.exe"],
        ["build-all.bat",      "Les deux d'un coup"],
        ["build-installer.bat","Tout + Inno Setup .exe"],
    ];
    let y = 2.35;
    scripts.forEach(([cmd, desc]) => {
        s.addText(cmd, {
            x: 0.7, y, w: 4, h: 0.25,
            fontFace: F_CODE, fontSize: 11, color: COL.accent, margin: 0
        });
        s.addText(desc, {
            x: 0.7, y: y + 0.25, w: 4, h: 0.3,
            fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
        });
        y += 0.65;
    });

    // Right: workflow
    addCard(s, 5.2, 1.85, 4.3, 3.3);
    s.addText("Étapes d'une release", {
        x: 5.4, y: 1.95, w: 4, h: 0.3,
        fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.text, margin: 0
    });
    const wsteps = [
        "Bumper <Version> dans PSLauncher.App.csproj",
        "build-launcher.bat",
        "Renommer PSLauncher.exe → PSLauncher-X.Y.Z.exe",
        "SFTP → builds/launcher/",
        "Backoffice → Launcher → Définir X.Y.Z",
        "Bouton bleu 🔁 Hasher le launcher + signer",
    ];
    addBullets(s, 5.4, 2.4, 4, 2.5, wsteps, { fontSize: 11 });
}

// ===================== SLIDE 14: SECURITY =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Sécurité", "section 6");
    addFooter(s, 14);

    const blocks = [
        ["🔏  Ed25519",    "Signe le manifest et la réponse de validation license. La clé publique est embarquée dans le launcher, donc même un MITM sur HTTPS ne peut pas falsifier. Sodium PHP natif côté serveur, NSec.Cryptography côté C#.", COL.accent],
        ["🔗  HMAC-SHA256", "URLs de download présignées par le serveur (path|exp|licId|secret). Apache route via gate.php qui vérifie en constant-time avant de servir le ZIP. Validité 1h, lié à la license.", COL.amber],
        ["💾  DPAPI local", "La clé license est stockée chiffrée dans %LocalAppData%/PSLauncher/config.json via ProtectedData.Protect (scope CurrentUser). Copier ce fichier sur une autre machine ou un autre user → ne marche pas.", COL.green],
    ];

    let y = 1.55;
    blocks.forEach(([title, desc, color]) => {
        addAccentCard(s, 0.5, y, W - 1, 1.05, color);
        s.addText(title, {
            x: 0.75, y: y + 0.15, w: 3, h: 0.35,
            fontFace: F_TITLE, fontSize: 15, bold: true, color, margin: 0
        });
        s.addText(desc, {
            x: 0.75, y: y + 0.5, w: W - 1.2, h: 0.55,
            fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
        });
        y += 1.15;
    });
}

// ===================== SLIDE 15: FILES & LOGS =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Fichiers & logs", "section 7 · localisation");
    addFooter(s, 15);

    // Server side
    s.addText("Côté serveur OVH", {
        x: 0.5, y: 1.4, w: 4.3, h: 0.3,
        fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.accent, margin: 0
    });
    addCodeBlock(s, 0.5, 1.75, 4.3, 3.3, [
        "www/PS_Launcher/",
        "├── api/",
        "│   ├── config.php  (gitignored)",
        "│   ├── index.php",
        "│   ├── lib/",
        "│   └── routes/",
        "├── admin/  (backoffice)",
        "├── manifest/versions.json",
        "├── releasenotes/X.Y.Z.md",
        "├── builds/",
        "│   ├── proserve-X.Y.Z.zip",
        "│   └── launcher/",
        "│       └── PSLauncher-X.Y.Z.exe",
        "├── tools/sign-manifest.php",
        "└── migrations/001_init.sql",
    ]);

    // Client side
    s.addText("Côté client Windows", {
        x: 5.2, y: 1.4, w: 4.3, h: 0.3,
        fontFace: F_TITLE, fontSize: 13, bold: true, color: COL.accent, margin: 0
    });
    addCodeBlock(s, 5.2, 1.75, 4.3, 3.3, [
        "%LocalAppData%/PSLauncher/",
        "├── config.json",
        "│     (license chiffrée DPAPI)",
        "├── logs/",
        "│   ├── app-YYYYMMDD.log",
        "│   └── updater.log",
        "├── downloads/",
        "│   ├── *.partial",
        "│   └── *.state.json",
        "└── selfupdate/",
        "",
        "C:\\ASTERION_VR/",
        "  (default installRoot)",
        "  ├── Proserve v1.4.5/",
        "  ├── Proserve v1.4.6/",
        "  └── ...",
    ]);
}

// ===================== SLIDE 16: TROUBLESHOOTING =====================
{
    const s = pres.addSlide();
    fillBg(s);
    addHeader(s, "Troubleshooting", "section 7 · pannes courantes");
    addFooter(s, 16);

    const issues = [
        ["⛔", "500 sur l'API", "Vérifier api/config.php existe + valeurs renseignées (debug endpoint /api/debug). Logs PHP : Manager OVH → Logs.", COL.red],
        ["⚠", "Hash incorrect côté client", "Le ZIP a changé sans resync. Backoffice → Versions → bouton bleu 🔁 pour resigner.", COL.amber],
        ["⚠", "License invalide après désactivation", "Cache local toujours valide 7 jours. Désactiver/réactiver depuis Settings → ⚙ ou supprimer config.json.", COL.amber],
        ["⛔", "Updater ne relance pas", "Vérifier %LocalAppData%/PSLauncher/logs/updater.log. Si UAC refusé en Program Files : copier manuellement le binaire.", COL.red],
        ["✓",  "Tout marche mais pas d'auto-update", "Section 'launcher' absente du manifest. Onglet Launcher → Définir + 🔁.", COL.green],
    ];

    let y = 1.55;
    issues.forEach(([icon, title, desc, color]) => {
        addCard(s, 0.5, y, W - 1, 0.65);
        s.addText(icon, {
            x: 0.7, y: y + 0.1, w: 0.5, h: 0.45,
            fontFace: F_TITLE, fontSize: 18, bold: true, color, align: "center", margin: 0
        });
        s.addText(title, {
            x: 1.2, y: y + 0.07, w: 3, h: 0.3,
            fontFace: F_TITLE, fontSize: 12, bold: true, color: COL.text, margin: 0
        });
        s.addText(desc, {
            x: 1.2, y: y + 0.32, w: W - 1.7, h: 0.32,
            fontFace: F_BODY, fontSize: 10, color: COL.muted, margin: 0
        });
        y += 0.72;
    });
}

// ===================== SLIDE 17: CLOSING =====================
{
    const s = pres.addSlide();
    fillBg(s);

    // Diagonal band
    s.addShape(pres.shapes.RECTANGLE, {
        x: 0, y: 1.3, w: W, h: 1.5, fill: { color: COL.accentDk }, line: { type: "none" }
    });
    s.addShape(pres.shapes.RECTANGLE, {
        x: 0, y: 1.3, w: W, h: 0.05, fill: { color: COL.accent }, line: { type: "none" }
    });

    s.addText("Fin de la documentation interne", {
        x: 0.5, y: 1.55, w: W - 1, h: 0.6,
        fontFace: F_TITLE, fontSize: 30, bold: true, color: COL.text, margin: 0
    });
    s.addText("Pour le guide utilisateur destiné aux clients : voir document séparé.", {
        x: 0.5, y: 2.1, w: W - 1, h: 0.4,
        fontFace: F_BODY, fontSize: 14, italic: true, color: COL.muted, margin: 0
    });

    // Three quick-reference cards
    const ix = 0.5, iy = 3.4, iw = 3.0, gap = 0.15;
    const refs = [
        ["📂  Repo",    "C:\\ASTERION\\GIT\\PS_Launcher", COL.accent],
        ["🌐  Backoffice", "asterionvr.com/PS_Launcher/admin/", COL.green],
        ["📋  Logs",    "%LocalAppData%\\PSLauncher\\logs\\", COL.amber],
    ];
    refs.forEach(([title, path, color], i) => {
        const x = ix + i * (iw + gap);
        addAccentCard(s, x, iy, iw, 1.1, color);
        s.addText(title, {
            x: x + 0.2, y: iy + 0.15, w: iw - 0.4, h: 0.3,
            fontFace: F_TITLE, fontSize: 13, bold: true, color, margin: 0
        });
        s.addText(path, {
            x: x + 0.2, y: iy + 0.5, w: iw - 0.4, h: 0.55,
            fontFace: F_CODE, fontSize: 10, color: COL.text, margin: 0
        });
    });

    // Footer brand
    s.addShape(pres.shapes.RECTANGLE, {
        x: 0, y: H - 0.5, w: W, h: 0.5, fill: { color: COL.card }, line: { type: "none" }
    });
    s.addText("© 2026 ASTERION VR — All rights reserved", {
        x: 0.5, y: H - 0.45, w: W - 1, h: 0.4,
        fontFace: F_BODY, fontSize: 11, color: COL.muted, align: "center", margin: 0
    });
}

pres.writeFile({ fileName: "C:\\ASTERION\\GIT\\PS_Launcher\\docs\\PS_Launcher-Documentation-Interne.pptx" })
    .then(f => console.log("Generated:", f));
