// ============================================================
// LEARNING AI ROUTES — AI-assisted content generation for Learning Hub
// ============================================================

var express = require('express');
var router = express.Router();
var multer = require('multer');
var axios = require('axios');
var path = require('path');
var { callAI } = require('../utils/ai');
var { authMiddleware, moderatorMiddleware } = require('../middleware/auth');
var db = require('../db/database');
var cryptoUtil = require('../utils/crypto');
var { assertSafeHttpsUrl } = require('../utils/urlSafety');

router.use(authMiddleware);
router.use(moderatorMiddleware);

var upload = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: 100 * 1024 * 1024, // 100 MB per file (large PDFs supported)
    files: 10 // max 10 files at once
  },
  fileFilter: function(req, file, cb) {
    // Whitelist allowed file types
    var allowed = [
      'application/pdf',
      'text/plain',
      'text/markdown',
      'text/html',
      'text/csv',
      'application/json',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      'application/vnd.openxmlformats-officedocument.presentationml.presentation',
      'application/vnd.oasis.opendocument.text',
      'application/epub+zip'
    ];
    if (allowed.includes(file.mimetype) || file.originalname.match(/\.(pdf|txt|md|html|htm|csv|json|docx|pptx|odt|epub)$/i)) {
      cb(null, true);
    } else {
      cb(new Error('File type not allowed. Supported: PDF, DOCX, PPTX, ODT, EPUB, TXT, MD, HTML, CSV, JSON.'));
    }
  }
});

// ── Text extraction helpers ──────────────────────────────────

async function extractText(buffer, mimetype, filename) {
  var ext = (filename || '').split('.').pop().toLowerCase();

  // PDF
  if (mimetype === 'application/pdf' || ext === 'pdf') {
    try {
      var pdfParse = require('pdf-parse');
      var data = await pdfParse(buffer);
      return data.text || '';
    } catch (e) {
      throw new Error('Could not parse PDF: ' + e.message);
    }
  }

  // Plain text / markdown / HTML
  if (mimetype.startsWith('text/') || ['txt', 'md', 'html', 'htm', 'csv'].includes(ext)) {
    return buffer.toString('utf8');
  }

  // JSON
  if (mimetype === 'application/json' || ext === 'json') {
    return buffer.toString('utf8');
  }

  // DOCX
  if (mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || ext === 'docx') {
    try {
      var mammoth = require('mammoth');
      var result = await mammoth.extractRawText({ buffer: buffer });
      return result.value || '';
    } catch (e) {
      throw new Error('Could not parse DOCX: ' + e.message);
    }
  }

  // PPTX — extract text from slide XML inside the zip
  if (mimetype === 'application/vnd.openxmlformats-officedocument.presentationml.presentation' || ext === 'pptx') {
    try {
      var JSZip = require('jszip');
      var zip = await JSZip.loadAsync(buffer);
      var slides = Object.keys(zip.files).filter(function(f) { return /^ppt\/slides\/slide\d+\.xml$/.test(f); }).sort();
      var texts = [];
      for (var i = 0; i < slides.length; i++) {
        var xml = await zip.files[slides[i]].async('string');
        var slideText = xml.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
        if (slideText) texts.push('--- Slide ' + (i + 1) + ' ---\n' + slideText);
      }
      return texts.join('\n\n');
    } catch (e) {
      throw new Error('Could not parse PPTX: ' + e.message);
    }
  }

  // ODT — extract text from content.xml inside the zip
  if (mimetype === 'application/vnd.oasis.opendocument.text' || ext === 'odt') {
    try {
      var JSZip = require('jszip');
      var zip = await JSZip.loadAsync(buffer);
      var contentXml = await zip.files['content.xml'].async('string');
      return contentXml.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
    } catch (e) {
      throw new Error('Could not parse ODT: ' + e.message);
    }
  }

  // EPUB — extract text from XHTML chapters inside the zip
  if (mimetype === 'application/epub+zip' || ext === 'epub') {
    try {
      var JSZip = require('jszip');
      var zip = await JSZip.loadAsync(buffer);
      var chapters = Object.keys(zip.files).filter(function(f) { return /\.(xhtml|html|htm)$/i.test(f) && !zip.files[f].dir; }).sort();
      var texts = [];
      for (var i = 0; i < chapters.length; i++) {
        var html = await zip.files[chapters[i]].async('string');
        var chapterText = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
        if (chapterText) texts.push(chapterText);
      }
      return texts.join('\n\n');
    } catch (e) {
      throw new Error('Could not parse EPUB: ' + e.message);
    }
  }

  // Fallback — try utf8
  return buffer.toString('utf8');
}

// ── Build AI prompt ──────────────────────────────────────────

function buildGeneratePrompt(opts) {
  var { topic, docText, contentType, questionCount, refinement, wordCount, slideCount, existingCategories } = opts;

  var source = docText
    ? 'Based on the following document/resource text, generate educational content.\n\nDOCUMENT:\n"""\n' + docText.substring(0, 50000) + '\n"""\n'
    : 'Generate educational content on the following topic for a medical professional audience (pediatrics / primary care).\n\nTOPIC: ' + topic + '\n';

  var refineInstr = refinement ? '\n\nAdditional instructions for tone/style/focus: ' + refinement : '';
  var categoryInstr = buildCategoryInstruction(existingCategories);

  // ── Presentation ──
  if (contentType === 'presentation') {
    var slideHint = slideCount ? slideCount + ' slides' : '8-12 slides';
    var hasQuestions = parseInt(questionCount) > 0;

    if (!hasQuestions) {
      // No questions — return raw Marp markdown only
      return source + '\n' +
        'Create a professional Marp presentation (' + slideHint + ') suitable for medical education. ' +
        'Each slide should be focused and readable.' + refineInstr + `

Return ONLY valid Marp markdown (no JSON, no code fences). Start with frontmatter:
---
marp: true
theme: default
---

Then each slide separated by ---. Guidelines:
- First slide: title slide with presentation name and brief subtitle
- Use # for slide titles
- Use bullet points (- ) for lists, keep them concise (max 5 bullets per slide)
- Include a summary/key takeaways slide at the end
- Do NOT include HTML tags or inline styles`;
    }

    // With questions — return JSON containing both Marp markdown and questions
    return source + '\n' +
      'Create a professional Marp presentation (' + slideHint + ') suitable for medical education, ' +
      'AND ' + questionCount + ' quiz questions based on the content.' + categoryInstr + refineInstr + `

Return ONLY a valid JSON object (no markdown, no code fences) with this exact structure:
{
  "title": "string",
  "category_name": "string (best existing category name, or a concise new category if none fit)",
  "marpMarkdown": "string (the complete Marp markdown starting with frontmatter ---\\nmarp: true\\ntheme: default\\n---)",
  "questions": [
    {
      "question_text": "string",
      "question_type": "mcq",
      "explanation": "string",
      "options": [
        { "option_text": "string", "is_correct": false, "explanation": "string" }
      ]
    }
  ]
}

Marp guidelines inside marpMarkdown:
- Start with: ---\\nmarp: true\\ntheme: default\\n---
- Separate slides with \\n---\\n
- First slide: title + subtitle. Use # for titles, bullets for content.
- Each MCQ must have exactly 4 options, exactly 1 marked is_correct: true`;
  }

  var wordHint = wordCount ? ' Target approximately ' + wordCount + ' words for the body.' : '';
  var qInstr = parseInt(questionCount) > 0
    ? 'then generate ' + questionCount + ' quiz questions.'
    : 'Do NOT include quiz questions (questions array should be empty []).';

  var typeInstr = '';
  if (contentType === 'quiz') {
    typeInstr = 'This is a quiz-only resource. Write a brief introductory body (1-2 paragraphs),' + wordHint + ' ' + qInstr;
  } else if (contentType === 'pearl') {
    typeInstr = 'This is a clinical pearl. Write a concise, high-impact body (2-4 paragraphs focusing on key takeaways).' + wordHint + ' ' + qInstr;
  } else {
    typeInstr = 'This is an article. Write a comprehensive, well-structured body.' + wordHint + ' ' + qInstr;
  }

  return source + '\n' + typeInstr + categoryInstr + refineInstr + `

Return ONLY a valid JSON object (no markdown, no code fences) with this exact structure:
{
  "title": "string",
  "category_name": "string (best existing category name, or a concise new category if none fit)",
  "subject": "string (1-3 word sub-topic label)",
  "body": "string (valid HTML using only: p, h2, h3, ul, ol, li, strong, em, blockquote, code — no inline styles)",
  "questions": [
    {
      "question_text": "string",
      "question_type": "mcq",
      "explanation": "string (general explanation shown after answering)",
      "options": [
        { "option_text": "string", "is_correct": false, "explanation": "string (shown if this wrong option chosen)" }
      ]
    }
  ]
}

Rules:
- Each MCQ must have exactly 4 options, exactly 1 marked is_correct: true
- Prefer an existing category_name when it fits; create a short broad category_name only when no existing category fits
- question_type must be "mcq" or "true_false" (true_false has exactly 2 options: "True" and "False")
- body must be clean HTML, no raw markdown
- Do not include any text outside the JSON object`;
}

function buildCategoryInstruction(existingCategories) {
  var names = (existingCategories || []).map(function(c) { return c.name; }).filter(Boolean);
  if (!names.length) {
    return '\nAssign a short, broad category_name for this content.';
  }
  return '\nChoose the best category_name from this existing list when appropriate: ' + names.join(', ') + '. If none fit, create one short broad category_name.';
}

// ── POST /api/admin/learning/ai-generate ────────────────────
// Accepts: multipart/form-data OR application/json

router.post('/ai-generate', upload.array('files', 10), async function(req, res) {
  try {
    var topic        = req.body.topic || '';
    var contentType  = req.body.contentType || 'article';
    var questionCount = Math.min(parseInt(req.body.questionCount) || 0, 20);
    var model        = req.body.model || null;
    var refinement   = req.body.refinement || '';
    var webdavPath   = req.body.webdavPath || '';
    var wordCount    = parseInt(req.body.wordCount) || 0;
    var slideCount   = parseInt(req.body.slideCount) || 0;

    var docText = '';
    var fileCount = 0;

    // 1 — Uploaded files (multiple)
    if (req.files && req.files.length > 0) {
      var allTexts = [];
      for (var i = 0; i < req.files.length; i++) {
        var file = req.files[i];
        try {
          var text = await extractText(file.buffer, file.mimetype, file.originalname);
          allTexts.push('### Source File: ' + file.originalname + '\n\n' + text);
          fileCount++;
        } catch (e) {
          console.error('[LearningAI] Failed to extract uploaded file:', e.message);
          // Continue with other files even if one fails
        }
      }
      docText = allTexts.join('\n\n---\n\n');
    }

    // 2 — Nextcloud WebDAV path
    else if (webdavPath) {
      var user = await db.get(
        'SELECT nextcloud_url, nextcloud_user, nextcloud_token FROM users WHERE id = ?',
        [req.user.id]
      );
      if (!user || !user.nextcloud_url) {
        return res.status(400).json({ error: 'Nextcloud not connected. Go to Settings first.' });
      }
      await assertSafeHttpsUrl(user.nextcloud_url, 'Nextcloud URL');
      var ncPassword;
      try { ncPassword = cryptoUtil.decryptString(user.nextcloud_token); }
      catch (decErr) { return res.status(400).json({ error: 'Nextcloud credentials invalid. Please reconnect.' }); }
      if (!webdavPath.startsWith('/')) webdavPath = '/' + webdavPath;
      var fileUrl = user.nextcloud_url + '/remote.php/dav/files/' + encodeURIComponent(user.nextcloud_user) + webdavPath;
      var response = await axios.get(fileUrl, {
        auth: { username: user.nextcloud_user, password: ncPassword },
        responseType: 'arraybuffer',
        timeout: 30000,
        maxRedirects: 0
      });
      var mimeType = response.headers['content-type'] || 'text/plain';
      var fname    = path.basename(webdavPath);
      docText = await extractText(Buffer.from(response.data), mimeType, fname);
    }

    // 3 — Topic only (no file)
    else if (!topic.trim()) {
      return res.status(400).json({ error: 'Provide a topic or upload a file.' });
    }

    var existingCategories = await db.all('SELECT name FROM learning_categories ORDER BY sort_order ASC, name ASC', []);
    var prompt = buildGeneratePrompt({ topic, docText, contentType, questionCount, refinement, wordCount, slideCount, existingCategories });

    var result = await callAI(
      [
        { role: 'system', content: 'You are a medical education content generator. Return ONLY the requested JSON or Marp markdown — no preamble, no commentary, no code fences, no thinking. Start your response with { or --- as appropriate.' },
        { role: 'user', content: prompt }
      ],
      { model: model, temperature: 0.4, maxTokens: 8000 }
    );

    var raw = result.content.trim();
    // Strip any leading text before the first { or --- (models sometimes add preamble)
    if (contentType !== 'presentation' || parseInt(questionCount) > 0) {
      var jsonStart = raw.indexOf('{');
      if (jsonStart > 0) raw = raw.substring(jsonStart);
    }

    // ── Presentation ──
    if (contentType === 'presentation') {
      if (questionCount > 0) {
        // JSON response with marpMarkdown + questions
        var cleanRaw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '');
        var parsedPres;
        try { parsedPres = JSON.parse(cleanRaw); }
        catch(e) {
          var m = cleanRaw.match(/\{[\s\S]*\}/);
          try { parsedPres = m ? JSON.parse(m[0]) : null; } catch(e2) { parsedPres = null; }
        }
        if (parsedPres && parsedPres.marpMarkdown) {
          return res.json({ success: true, contentType: 'presentation', marpMarkdown: parsedPres.marpMarkdown, category_name: parsedPres.category_name || '', questions: parsedPres.questions || [], model: result.model });
        }
      }
      // Plain Marp markdown (no questions requested, or parse failed)
      var marpMd = raw.replace(/^```(?:markdown|marp)?\s*/i, '').replace(/\s*```\s*$/, '');
      return res.json({ success: true, contentType: 'presentation', marpMarkdown: marpMd, questions: [], model: result.model, docLength: docText.length });
    }

    // Strip code fences and any trailing text after JSON
    raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '');
    // Trim trailing non-JSON text (e.g. "Here is the JSON..." after closing brace)
    var lastBrace = raw.lastIndexOf('}');
    if (lastBrace !== -1 && lastBrace < raw.length - 1) raw = raw.substring(0, lastBrace + 1);

    // Fix unescaped control characters inside JSON string values.
    // Models sometimes output literal newlines/tabs inside strings instead of \n \t.
    function sanitizeJsonString(s) {
      var inStr = false, escaped = false, out = '';
      for (var i = 0; i < s.length; i++) {
        var c = s[i];
        if (escaped) { out += c; escaped = false; continue; }
        if (c === '\\' && inStr) { out += c; escaped = true; continue; }
        if (c === '"') { inStr = !inStr; out += c; continue; }
        if (inStr && c === '\n') { out += '\\n'; continue; }
        if (inStr && c === '\r') { continue; }
        if (inStr && c === '\t') { out += '\\t'; continue; }
        out += c;
      }
      return out;
    }

    var parsed;
    try {
      parsed = JSON.parse(raw);
    } catch (e) {
      console.error('[LearningAI] Direct parse failed:', e.message, '| Pos:', e.message.match(/position (\d+)/)?.[1]);
      // Attempt 2: sanitize control chars and retry
      try { parsed = JSON.parse(sanitizeJsonString(raw)); }
      catch (e2) {
        // Attempt 3: extract first { to last }
        var firstBrace = raw.indexOf('{');
        var extractedJson = firstBrace >= 0 ? raw.substring(firstBrace, lastBrace + 1) : null;
        if (extractedJson) {
          try { parsed = JSON.parse(sanitizeJsonString(extractedJson)); }
          catch (e3) {
            console.error('[LearningAI] All parse attempts failed:', e3.message);
            return res.status(500).json({ error: 'AI returned invalid JSON. Try again.' });
          }
        } else {
          console.error('[LearningAI] No JSON braces found');
          return res.status(500).json({ error: 'AI returned invalid JSON. Try again.' });
        }
      }
    }

    res.json({
      success: true,
      content: parsed,
      model: result.model,
      docLength: docText.length,
      fileCount: fileCount || (webdavPath ? 1 : 0)
    });

  } catch (err) {
    console.error('[LearningAI]', err.message);
    res.status(500).json({ error: 'Generation failed' });
  }
});

// ── POST /api/admin/learning/ai-refine ───────────────────────
// Refine an existing body or specific question

router.post('/ai-refine', async function(req, res) {
  try {
    var { content, instructions, model } = req.body;
    if (!content || !instructions) return res.status(400).json({ error: 'content and instructions required' });

    var prompt = `You are editing educational medical content. Refine the following HTML body according to the instructions below.

INSTRUCTIONS: ${instructions}

CURRENT CONTENT:
${content.substring(0, 8000)}

Return ONLY the refined HTML body (same structure, no JSON wrapper, no markdown fences). Keep all HTML tags intact.`;

    var result = await callAI(
      [{ role: 'user', content: prompt }],
      { model: model, temperature: 0.3, maxTokens: 4000 }
    );

    var refined = result.content.trim().replace(/^```(?:html)?\s*/i, '').replace(/\s*```\s*$/, '');
    res.json({ success: true, refined, model: result.model });
  } catch (err) {
    console.error('[LearningAI]', err.message);
    res.status(500).json({ error: 'Request failed' });
  }
});

// ── GET /api/admin/learning/webdav-browse ────────────────────
// Browse user's Nextcloud folder

router.get('/webdav-browse', async function(req, res) {
  try {
    var user = await db.get(
      'SELECT nextcloud_url, nextcloud_user, nextcloud_token, nextcloud_folder, webdav_learning_path FROM users WHERE id = ?',
      [req.user.id]
    );
    if (!user || !user.nextcloud_url) {
      return res.status(400).json({ error: 'Nextcloud not connected' });
    }
    await assertSafeHttpsUrl(user.nextcloud_url, 'Nextcloud URL');

    var ncPassword;
    try { ncPassword = cryptoUtil.decryptString(user.nextcloud_token); }
    catch (decErr) { return res.status(400).json({ error: 'Nextcloud credentials invalid. Please reconnect.' }); }

    var browsePath = req.query.path || user.webdav_learning_path || user.nextcloud_folder || '/';
    // Ensure it starts with /
    if (!browsePath.startsWith('/')) browsePath = '/' + browsePath;

    var davUrl = user.nextcloud_url + '/remote.php/dav/files/' + encodeURIComponent(user.nextcloud_user) + browsePath;

    var davResponse = await axios({
      method: 'PROPFIND',
      url: davUrl,
      auth: { username: user.nextcloud_user, password: ncPassword },
      headers: { Depth: '1', 'Content-Type': 'application/xml' },
      data: `<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:displayname/><d:getcontenttype/><d:getcontentlength/><d:resourcetype/></d:prop></d:propfind>`,
      timeout: 15000,
      maxRedirects: 0
    });

    // Parse WebDAV XML response
    var xml = davResponse.data;
    var items = [];
    var responseRegex = /<d:response>([\s\S]*?)<\/d:response>/g;
    var match;
    while ((match = responseRegex.exec(xml)) !== null) {
      var block = match[1];
      var hrefMatch = block.match(/<d:href>([^<]+)<\/d:href>/);
      var nameMatch = block.match(/<d:displayname>([^<]*)<\/d:displayname>/);
      var typeMatch = block.match(/<d:getcontenttype>([^<]*)<\/d:getcontenttype>/);
      var sizeMatch = block.match(/<d:getcontentlength>([^<]*)<\/d:getcontentlength>/);
      var isCollMatch = block.includes('<d:collection');

      if (!hrefMatch) continue;

      var href = decodeURIComponent(hrefMatch[1]);
      // Strip the /remote.php/dav/files/username prefix to get the relative path
      var relPath = href.replace(/^.*\/remote\.php\/dav\/files\/[^/]+/, '') || '/';

      items.push({
        path: relPath,
        name: nameMatch ? nameMatch[1] : path.basename(relPath) || relPath,
        isDir: !!isCollMatch,
        contentType: typeMatch ? typeMatch[1] : '',
        size: sizeMatch ? parseInt(sizeMatch[1]) : 0
      });
    }

    // Filter: skip the parent directory entry (same path as requested)
    var normBrowse = browsePath.replace(/\/$/, '');
    items = items.filter(function(item) {
      var normPath = item.path.replace(/\/$/, '');
      return normPath !== normBrowse;
    });

    // Sort: directories first, then files
    items.sort(function(a, b) {
      if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
      return a.name.localeCompare(b.name);
    });

    // Compute parent path
    var parent = browsePath.replace(/\/$/, '');
    var parentPath = parent.includes('/') ? parent.substring(0, parent.lastIndexOf('/')) || '/' : '/';

    res.json({ success: true, path: browsePath, parentPath, items });
  } catch (err) {
    console.error('[WebDAV]', err.message);
    res.status(500).json({ error: 'WebDAV browse failed' });
  }
});

// ── POST /api/admin/learning/webdav-path ─────────────────────
// Save user's preferred WebDAV learning path

router.post('/webdav-path', async function(req, res) {
  try {
    var { path: wPath } = req.body;
    await db.run('UPDATE users SET webdav_learning_path = ? WHERE id = ?', [wPath || null, req.user.id]);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: 'Request failed' });
  }
});

// ── POST /api/admin/learning/generate-pptx ──────────────────
// Convert Marp markdown to PPTX using pptxgenjs (no browser needed)

router.post('/generate-pptx', async function(req, res) {
  try {
    var { markdown, title } = req.body;
    if (!markdown) return res.status(400).json({ error: 'markdown required' });

    var PptxGenJS = require('pptxgenjs');
    var pptx = new PptxGenJS();

    pptx.layout = 'LAYOUT_WIDE'; // 16:9

    // ── Helpers ────────────────────────────────────────────────

    // Parse inline markdown (**bold**, *italic*, `code`, ***both***) into pptxgenjs text objects
    function parseInline(text, defaults) {
      var parts = [];
      var re = /(\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`)/g;
      var last = 0;
      var m;
      while ((m = re.exec(text)) !== null) {
        if (m.index > last) parts.push({ text: text.slice(last, m.index), options: Object.assign({}, defaults) });
        if (m[2]) parts.push({ text: m[2], options: Object.assign({}, defaults, { bold: true, italic: true }) });
        else if (m[3]) parts.push({ text: m[3], options: Object.assign({}, defaults, { bold: true }) });
        else if (m[4]) parts.push({ text: m[4], options: Object.assign({}, defaults, { italic: true }) });
        else if (m[5]) parts.push({ text: m[5], options: Object.assign({}, defaults, { fontFace: 'Courier New', color: '7c3aed' }) });
        last = m.index + m[0].length;
      }
      if (last < text.length) parts.push({ text: text.slice(last), options: Object.assign({}, defaults) });
      return parts.length ? parts : [{ text: text, options: Object.assign({}, defaults) }];
    }

    // Parse a markdown table block into { headers: string[], rows: string[][] }
    function parseTable(lines) {
      var headers = lines[0].split('|').map(function(c) { return c.trim(); }).filter(Boolean);
      var rows = [];
      for (var i = 2; i < lines.length; i++) {
        var cells = lines[i].split('|').map(function(c) { return c.trim(); }).filter(Boolean);
        if (cells.length) rows.push(cells);
      }
      return { headers: headers, rows: rows };
    }

    // Classify a line by type
    function classifyLine(line) {
      if (/^\|.+\|/.test(line)) return 'table';
      if (/^\|[\s:-]+\|/.test(line)) return 'table-sep';
      if (/^>\s+/.test(line)) return 'blockquote';
      if (/^```/.test(line)) return 'code-fence';
      if (/^\d+\.\s+/.test(line)) return 'ordered';
      if (/^[-*]\s+/.test(line)) return 'bullet';
      if (/^#{1,3}\s+/.test(line)) return 'heading';
      if (line.trim() === '') return 'blank';
      return 'paragraph';
    }

    // ── Parse slides ───────────────────────────────────────────

    var md = markdown.replace(/^---[\s\S]*?---\n?/, '').trim();
    var rawSlides = md.split(/\n---\n/);

    rawSlides.forEach(function(slideText, slideIdx) {
      slideText = slideText.trim();
      if (!slideText) return;

      var slide = pptx.addSlide();
      slide.background = { color: 'FFFFFF' };

      // Extract slide title (first # heading)
      var titleMatch = slideText.match(/^#{1,3}\s+(.+)$/m);
      var slideTitle = titleMatch ? titleMatch[1].trim() : '';
      var body = slideText.replace(/^#{1,3}\s+.+$/m, '').trim();

      if (slideTitle) {
        slide.addText(parseInline(slideTitle, { fontSize: 28, bold: true, color: '1e40af', fontFace: 'Calibri' }), {
          x: 0.5, y: 0.3, w: '90%', h: 0.8, valign: 'middle'
        });
      }

      if (!body) {
        slide.addText((slideIdx + 1).toString(), { x: '90%', y: '92%', w: '8%', h: 0.3, fontSize: 9, color: 'bbbbbb', align: 'right', fontFace: 'Calibri' });
        return;
      }

      var contentY = slideTitle ? 1.3 : 0.6;
      var maxH = 5.2 - contentY;
      var lines = body.split('\n');
      var i = 0;
      var inCodeBlock = false;
      var codeLines = [];

      while (i < lines.length) {
        var line = lines[i];
        var type = classifyLine(line);

        // ── Code block ──
        if (type === 'code-fence' || inCodeBlock) {
          if (type === 'code-fence' && !inCodeBlock) {
            inCodeBlock = true; codeLines = []; i++; continue;
          }
          if (type === 'code-fence' && inCodeBlock) {
            inCodeBlock = false;
            if (codeLines.length) {
              var codeText = codeLines.join('\n');
              var codeH = Math.min(Math.max(codeLines.length * 0.28, 0.6), maxH - (contentY - (slideTitle ? 1.3 : 0.6)));
              slide.addShape(pptx.ShapeType.rect, { x: 0.5, y: contentY, w: '90%', h: codeH, fill: { color: 'f3f4f6' }, rectRadius: 0.08 });
              slide.addText(codeText, {
                x: 0.65, y: contentY + 0.08, w: '86%', h: codeH - 0.16,
                fontSize: 13, fontFace: 'Courier New', color: '1f2937', valign: 'top', wrap: true
              });
              contentY += codeH + 0.2;
              codeLines = [];
            }
            i++; continue;
          }
          codeLines.push(line);
          i++; continue;
        }

        // ── Table ──
        if (type === 'table') {
          var tableLines = [];
          while (i < lines.length && /^\|/.test(lines[i])) { tableLines.push(lines[i]); i++; }
          var tbl = parseTable(tableLines);
          if (tbl.headers.length) {
            var colW = (12 / tbl.headers.length);
            var tblRows = [];
            // Header row
            tblRows.push(tbl.headers.map(function(h) {
              return { text: h, options: { bold: true, fontSize: 13, color: 'ffffff', fill: { color: '1e40af' }, fontFace: 'Calibri', align: 'center', valign: 'middle' } };
            }));
            // Data rows
            tbl.rows.forEach(function(row, ri) {
              tblRows.push(row.map(function(cell) {
                return { text: cell, options: { fontSize: 12, color: '374151', fill: { color: ri % 2 === 0 ? 'f9fafb' : 'ffffff' }, fontFace: 'Calibri', valign: 'middle' } };
              }));
            });
            var tblH = Math.min((tblRows.length * 0.38) + 0.1, maxH - (contentY - (slideTitle ? 1.3 : 0.6)));
            slide.addTable(tblRows, {
              x: 0.5, y: contentY, w: 12,
              colW: Array(tbl.headers.length).fill(colW),
              border: { type: 'solid', pt: 0.5, color: 'dee2e6' },
              rowH: 0.36,
              autoPage: false
            });
            contentY += tblH + 0.15;
          }
          continue;
        }

        // ── Blockquote ──
        if (type === 'blockquote') {
          var quoteText = line.replace(/^>\s*/, '').trim();
          i++;
          while (i < lines.length && /^>\s*/.test(lines[i])) { quoteText += '\n' + lines[i].replace(/^>\s*/, '').trim(); i++; }
          var qH = Math.max(0.5, Math.ceil(quoteText.length / 100) * 0.35);
          slide.addShape(pptx.ShapeType.rect, { x: 0.5, y: contentY, w: '90%', h: qH, fill: { color: 'eff6ff' }, rectRadius: 0.06 });
          slide.addShape(pptx.ShapeType.rect, { x: 0.5, y: contentY, w: 0.06, h: qH, fill: { color: '3b82f6' } });
          slide.addText(parseInline(quoteText, { fontSize: 16, italic: true, color: '1e40af', fontFace: 'Calibri' }), {
            x: 0.8, y: contentY + 0.06, w: '85%', h: qH - 0.12, valign: 'middle', wrap: true
          });
          contentY += qH + 0.15;
          continue;
        }

        // ── Bullets (unordered) ──
        if (type === 'bullet') {
          var bulletItems = [];
          while (i < lines.length && /^[-*]\s+/.test(lines[i])) {
            bulletItems.push(lines[i].replace(/^[-*]\s+/, '').trim());
            i++;
          }
          var bulletObjs = [];
          bulletItems.forEach(function(b) {
            var inlineParts = parseInline(b, { fontSize: 17, color: '374151', fontFace: 'Calibri' });
            inlineParts[0].options.bullet = { type: 'bullet' };
            inlineParts[0].options.paraSpaceBefore = 4;
            bulletObjs.push(inlineParts);
          });
          var flat = []; bulletObjs.forEach(function(arr) { arr.forEach(function(p) { flat.push(p); }); });
          var bH = Math.min(Math.max(bulletItems.length * 0.35, 0.6), maxH - (contentY - (slideTitle ? 1.3 : 0.6)));
          slide.addText(flat, { x: 0.6, y: contentY, w: '88%', h: bH, fontFace: 'Calibri', valign: 'top' });
          contentY += bH + 0.1;
          continue;
        }

        // ── Numbered list ──
        if (type === 'ordered') {
          var orderedItems = [];
          while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
            orderedItems.push(lines[i].replace(/^\d+\.\s+/, '').trim());
            i++;
          }
          var orderedObjs = [];
          orderedItems.forEach(function(item, idx) {
            var inlineParts = parseInline(item, { fontSize: 17, color: '374151', fontFace: 'Calibri' });
            inlineParts[0].options.bullet = { type: 'number', numberStartAt: idx === 0 ? 1 : undefined };
            inlineParts[0].options.paraSpaceBefore = 4;
            orderedObjs.push(inlineParts);
          });
          var flatOrd = []; orderedObjs.forEach(function(arr) { arr.forEach(function(p) { flatOrd.push(p); }); });
          var oH = Math.min(Math.max(orderedItems.length * 0.35, 0.6), maxH - (contentY - (slideTitle ? 1.3 : 0.6)));
          slide.addText(flatOrd, { x: 0.6, y: contentY, w: '88%', h: oH, fontFace: 'Calibri', valign: 'top' });
          contentY += oH + 0.1;
          continue;
        }

        // ── Sub-heading (## or ### inside body) ──
        if (type === 'heading') {
          var hText = line.replace(/^#{1,3}\s+/, '').trim();
          slide.addText(parseInline(hText, { fontSize: 22, bold: true, color: '1e3a5f', fontFace: 'Calibri' }), {
            x: 0.5, y: contentY, w: '90%', h: 0.45, valign: 'bottom'
          });
          contentY += 0.5;
          i++; continue;
        }

        // ── Paragraph ──
        if (type === 'paragraph') {
          var paraText = line.trim();
          i++;
          // Merge consecutive paragraph lines
          while (i < lines.length && classifyLine(lines[i]) === 'paragraph') { paraText += ' ' + lines[i].trim(); i++; }
          var pParts = parseInline(paraText, { fontSize: 17, color: '374151', fontFace: 'Calibri' });
          var pH = Math.max(0.4, Math.ceil(paraText.length / 110) * 0.3);
          slide.addText(pParts, { x: 0.5, y: contentY, w: '90%', h: pH, valign: 'top', wrap: true });
          contentY += pH + 0.1;
          continue;
        }

        // ── Blank / skip ──
        i++;
      }

      // Slide number
      slide.addText((slideIdx + 1).toString(), { x: '90%', y: '92%', w: '8%', h: 0.3, fontSize: 9, color: 'bbbbbb', align: 'right', fontFace: 'Calibri' });
    });

    // Write to buffer and send
    var pptxBuffer = await pptx.write({ outputType: 'nodebuffer' });
    var safeTitle = (title || 'presentation').replace(/[^a-zA-Z0-9-_\s]/g, '').replace(/\s+/g, '-').toLowerCase();

    res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.presentationml.presentation');
    res.setHeader('Content-Disposition', 'attachment; filename="' + safeTitle + '.pptx"');
    res.send(pptxBuffer);

  } catch (err) {
    console.error('[PPTX]', err.message);
    res.status(500).json({ error: 'Request failed' });
  }
});

// ── POST /api/admin/learning/preview-slides ──────────────────
// Render Marp markdown to HTML for in-browser preview

router.post('/preview-slides', async function(req, res) {
  try {
    var { markdown } = req.body;
    if (!markdown) return res.status(400).json({ error: 'markdown required' });

    var { Marp } = require('@marp-team/marp-core');
    var marp = new Marp({ html: false });
    var { html, css } = marp.render(markdown);

    // Extract individual <section> elements for page-by-page navigation
    var slides = [];
    var sectionReg = /<section[^>]*>[\s\S]*?<\/section>/g;
    var match;
    while ((match = sectionReg.exec(html)) !== null) {
      slides.push(match[0]);
    }
    if (slides.length === 0) slides.push(html); // fallback

    res.json({ success: true, css: css, slides: slides });
  } catch (err) {
    res.status(500).json({ error: 'Request failed' });
  }
});

module.exports = router;
