/* ============================================================
   DGCA submission packet builder (instructor side)
   ------------------------------------------------------------
   Assembles every artefact in the student's file into a single
   PDF in the DGCA-prescribed order, plus an optional zip of just
   the verified uploaded files.

   Auto-rendered sections (registration, attendance, simulator /
   RPA / skill / theory reports) are drawn directly on PDF pages
   from the existing record fields, so the packet is consistent
   even if the student never printed/scanned a paper form.

   Uploaded artefacts (marksheet, Aadhaar, photo ID, medical,
   IDA cert, eGCA RPC, logbook, extras) are fetched from Cloud
   Storage and concatenated: PDFs copy as-is; JPG/PNG embed as
   full-page images.

   Order matches the user's spec (1 Registration → 13 Logbook);
   drag-to-nudge and include/exclude per item.
   ============================================================ */

(function () {

  /* ---------- helpers ---------- */

  // pdf-lib uses winansi by default — strip characters it can't encode
  // (most non-ASCII trips drawText with WinAnsi). Cheap fallback for
  // non-Latin text (Telugu titles etc).
  const safe = (s) => String(s == null ? '' : s)
    .replace(/[–—]/g, '-')   // en/em-dash → hyphen
    .replace(/[‘’]/g, "'")    // curly singles
    .replace(/[“”]/g, '"')    // curly doubles
    .replace(/[ ]/g, ' ')          // nbsp
    // strip everything outside printable ASCII
    .replace(/[^\x20-\x7E\n]/g, '');

  const fmtDate = (s) => {
    if (!s) return '—';
    try { return window.UI && window.UI.fmtDate ? window.UI.fmtDate(s) : String(s).slice(0, 10); }
    catch (_) { return String(s); }
  };

  const dlAs = (bytes, filename, mime) => {
    const blob = new Blob([bytes], { type: mime || 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = filename;
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 4000);
  };

  // Fetch a file from Cloud Storage as Uint8Array. By default Firebase
  // Storage download URLs are NOT served with CORS headers, so a plain
  // fetch() from the dashboard origin fails. The Storage SDK's
  // authenticated download (getBlob / getBytes) bypasses CORS entirely
  // — try that first, fall back to fetch() only if the SDK can't help.
  async function fetchBytes(file) {
    if (!file) throw new Error('No file');

    // Path through the authenticated SDK (no CORS preflight).
    if (file.path && typeof firebase !== 'undefined' && firebase.storage) {
      try {
        const ref = firebase.storage().ref(file.path);
        if (typeof ref.getBytes === 'function') {
          const bytes = await ref.getBytes();
          return new Uint8Array(bytes);
        }
        if (typeof ref.getBlob === 'function') {
          const blob = await ref.getBlob();
          return new Uint8Array(await blob.arrayBuffer());
        }
      } catch (e) {
        // Auth/storage error — fall through to URL fetch so we can at
        // least surface a useful HTTP-level error.
        console.warn('Storage SDK download failed for', file.path, e);
      }
    }

    // Last resort: hit the public download URL. Needs bucket CORS to
    // be configured (see storage-cors.json + README).
    const url = file.url;
    if (!url) throw new Error(`No URL or path for ${file.name || 'file'}`);
    const res = await fetch(url, { mode: 'cors' });
    if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${file.name}`);
    return new Uint8Array(await res.arrayBuffer());
  }

  /* ---------- page-drawing helpers ---------- */

  const A4 = { w: 595.28, h: 841.89 };

  function drawHeader(page, font, boldFont, title, rec) {
    const { w, h } = A4;
    // Subtle top band with title
    page.drawRectangle({ x: 0, y: h - 70, width: w, height: 70,
      color: window.PDFLib.rgb(0.96, 0.96, 0.96) });
    page.drawText(safe('INDIA DRONE ACADEMY'), {
      x: 36, y: h - 30, size: 11, font: boldFont,
      color: window.PDFLib.rgb(0.20, 0.20, 0.20) });
    page.drawText(safe(title), {
      x: 36, y: h - 50, size: 16, font: boldFont,
      color: window.PDFLib.rgb(0.10, 0.10, 0.10) });
    if (rec && rec.profile && rec.profile.fullName) {
      page.drawText(safe(`Student: ${rec.profile.fullName}    Roll: ${rec.profile.rollNo || '—'}    Batch: ${rec.profile.batch || '—'}`), {
        x: 36, y: h - 66, size: 9, font: font,
        color: window.PDFLib.rgb(0.30, 0.30, 0.30) });
    }
    return h - 90;     // y-cursor below header
  }

  function drawFooter(page, font, label) {
    page.drawText(safe(label), {
      x: 36, y: 24, size: 8, font: font,
      color: window.PDFLib.rgb(0.5, 0.5, 0.5) });
  }

  function drawKVRows(page, font, boldFont, startY, rows) {
    let y = startY;
    rows.forEach(([k, v]) => {
      page.drawText(safe(k + ':'), {
        x: 36, y, size: 10, font: boldFont,
        color: window.PDFLib.rgb(0.20, 0.20, 0.20) });
      const lines = wrapText(safe(v || '—'), 78);
      lines.forEach((line, i) => {
        page.drawText(line, { x: 160, y: y - i * 13, size: 10, font: font,
          color: window.PDFLib.rgb(0.10, 0.10, 0.10) });
      });
      y -= 14 * Math.max(lines.length, 1) + 4;
    });
    return y;
  }

  // Crude text wrap by character count (uniform width estimate; OK for
  // a one-shot report layout).
  function wrapText(text, maxChars) {
    const words = text.split(/\s+/);
    const lines = [];
    let line = '';
    words.forEach(w => {
      if ((line + ' ' + w).trim().length > maxChars) {
        if (line) lines.push(line);
        line = w;
      } else {
        line = line ? line + ' ' + w : w;
      }
    });
    if (line) lines.push(line);
    return lines.length ? lines : [''];
  }

  async function embedSignature(pdfDoc, page, dataUrl, x, y, width) {
    if (!dataUrl) return;
    try {
      const m = /^data:image\/(png|jpe?g);base64,(.+)$/.exec(dataUrl);
      if (!m) return;
      const raw = atob(m[2]);
      const bytes = new Uint8Array(raw.length);
      for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
      const img = (m[1] === 'png')
        ? await pdfDoc.embedPng(bytes)
        : await pdfDoc.embedJpg(bytes);
      const scaled = img.scaleToFit(width, 60);
      page.drawImage(img, { x, y, width: scaled.width, height: scaled.height });
    } catch (e) { /* signature optional */ }
  }

  /* ---------- per-section renderers ---------- */

  async function renderRegistration(pdfDoc, rec, font, boldFont, instructorSig) {
    const page = pdfDoc.addPage([A4.w, A4.h]);
    let y = drawHeader(page, font, boldFont, 'Drone Pilot Course — Registration Form', rec);
    const p = rec.profile || {};
    const reg = rec.registration || {};
    y -= 8;
    page.drawText('CANDIDATE INFORMATION', { x: 36, y, size: 11, font: boldFont,
      color: window.PDFLib.rgb(0.15, 0.15, 0.15) });
    y -= 18;
    y = drawKVRows(page, font, boldFont, y, [
      ['Full name (as per 10th)', p.fullName],
      ["Father's name", p.fathersName],
      ['Date of birth', p.dob ? fmtDate(p.dob) : ''],
      ['Permanent address', p.address],
      ['Pincode', p.pincode],
      ['Telephone', p.phone],
      ['Email', p.email],
      ['Maximum qualification', p.qualification],
      ['Aadhaar number', p.aadhaar],
      ['Passport number', p.passport],
      ['DL / Voter ID / Ration Card', p.govtId],
      ['Organisation / individual', p.organisation],
      ['Course', p.course],
      ['Start date', p.startDate ? fmtDate(p.startDate) : ''],
      ['Roll / Student No.', p.rollNo],
      ['Batch', p.batch],
      ['Height / Weight / Chest', `${p.height || '—'} / ${p.weight || '—'} / ${p.chest || '—'}`],
    ]);
    y -= 10;
    page.drawText('DECLARATION', { x: 36, y, size: 11, font: boldFont });
    y -= 16;
    const decl =
      `I, ${safe(p.fullName || '__________')}, hereby declare that the above information is completely ` +
      `true, and I understand that any misinformation can lead to legal actions against me. I shall ` +
      `abide by the rules of the RPTO and DGCA, and shall handle drone and drone-related equipment as ` +
      `instructed.`;
    wrapText(decl, 92).forEach(line => {
      page.drawText(line, { x: 36, y, size: 9, font });
      y -= 12;
    });
    y -= 10;
    y = drawKVRows(page, font, boldFont, y, [
      ['Place', reg.place],
      ['Date', reg.date ? fmtDate(reg.date) : (reg.submittedAt ? fmtDate(reg.submittedAt) : '')],
      ['Submitted on', reg.submittedAt ? fmtDate(reg.submittedAt) : ''],
      ['Verified by office', reg.verifiedByOfficer ? 'Yes' : 'Pending'],
    ]);
    y -= 8;
    page.drawText('Signature:', { x: 36, y, size: 10, font: boldFont });
    if (reg.signature) {
      await embedSignature(pdfDoc, page, reg.signature, 110, y - 50, 180);
    } else {
      page.drawLine({ start: { x: 110, y: y - 30 }, end: { x: 280, y: y - 30 },
        thickness: 0.6, color: window.PDFLib.rgb(0.6, 0.6, 0.6) });
    }
    drawFooter(page, font, '1 — Registration form');
  }

  async function renderAttendance(pdfDoc, rec, font, boldFont, instructorSig) {
    const page = pdfDoc.addPage([A4.w, A4.h]);
    let y = drawHeader(page, font, boldFont, 'Attendance Record', rec);
    const sched = window.ATTENDANCE_SCHEDULE || [];
    const att = rec.attendance || {};
    const start = rec.profile && rec.profile.startDate;
    y -= 6;
    // table headers
    const cols = [
      { x: 36,  w: 30,  label: '#'        },
      { x: 70,  w: 70,  label: 'Day'      },
      { x: 145, w: 65,  label: 'Kind'     },
      { x: 215, w: 240, label: 'Session'  },
      { x: 460, w: 60,  label: 'Status'   },
    ];
    cols.forEach(c => page.drawText(safe(c.label), { x: c.x, y, size: 10, font: boldFont }));
    y -= 12;
    page.drawLine({ start: { x: 36, y }, end: { x: A4.w - 36, y },
      thickness: 0.4, color: window.PDFLib.rgb(0.7, 0.7, 0.7) });
    y -= 8;
    sched.forEach((s, ix) => {
      if (y < 80) return;       // overflow guard — single page
      const cell = att[s.id] || { status: 'pending' };
      const dateLabel = window.sessionDate ? window.sessionDate(start, s.day) || `Day ${s.day}` : `Day ${s.day}`;
      [
        String(ix + 1),
        dateLabel,
        s.kind,
        s.title,
        (cell.status || 'pending').toUpperCase(),
      ].forEach((val, ci) => {
        const text = wrapText(safe(val), Math.floor(cols[ci].w / 5))[0];
        page.drawText(text, { x: cols[ci].x, y, size: 9, font });
      });
      y -= 14;
    });
    // instructor sign block
    y = Math.max(y - 16, 110);
    page.drawText('Instructor signature:', { x: 36, y, size: 10, font: boldFont });
    if (instructorSig) {
      await embedSignature(pdfDoc, page, instructorSig, 170, y - 50, 200);
    } else {
      page.drawLine({ start: { x: 170, y: y - 30 }, end: { x: 360, y: y - 30 },
        thickness: 0.6, color: window.PDFLib.rgb(0.6, 0.6, 0.6) });
    }
    drawFooter(page, font, '2 — Attendance record');
  }

  function renderProgressOrSkill(pdfDoc, rec, font, boldFont, key, label, footerLabel, instructorSig) {
    const page = pdfDoc.addPage([A4.w, A4.h]);
    let y = drawHeader(page, font, boldFont, label, rec);
    const t = (rec.tests && rec.tests[key]) || { items: {} };
    page.drawText(safe(`Examiner: ${t.examinerName || '—'}        Examiner ID: ${t.examinerId || '—'}        Date: ${t.date ? fmtDate(t.date) : '—'}`),
      { x: 36, y, size: 9, font });
    y -= 18;

    let items = [];
    if (key === 'skill') {
      items = window.SKILL_ITEMS || [];
    } else {
      const pi = window.PROGRESS_ITEMS || {};
      items = [...(pi.preflight || []), ...(pi.performance || []), ...(pi.summary || [])];
    }

    page.drawText('Item', { x: 36, y, size: 10, font: boldFont });
    page.drawText('Result', { x: 400, y, size: 10, font: boldFont });
    page.drawText('Remark', { x: 470, y, size: 10, font: boldFont });
    y -= 10;
    page.drawLine({ start: { x: 36, y }, end: { x: A4.w - 36, y },
      thickness: 0.4, color: window.PDFLib.rgb(0.7, 0.7, 0.7) });
    y -= 10;
    items.forEach((it) => {
      if (y < 110) return;
      const st = (t.items && t.items[it.id]) || {};
      const result = st.status === 'sat' ? 'Sat.' : st.status === 'unsat' ? 'Unsat.' : st.status === 'na' ? 'N/A' : '—';
      page.drawText(safe(it.label), { x: 36, y, size: 9, font });
      page.drawText(result, { x: 400, y, size: 9, font });
      const remark = wrapText(safe(st.remark || ''), 20)[0];
      page.drawText(remark, { x: 470, y, size: 9, font });
      y -= 13;
    });

    if (key === 'skill' && t.result) {
      y -= 8;
      page.drawText(safe('Overall result: ' + t.result.toUpperCase()), {
        x: 36, y, size: 12, font: boldFont,
        color: t.result === 'pass'
          ? window.PDFLib.rgb(0.10, 0.55, 0.22)
          : window.PDFLib.rgb(0.75, 0.20, 0.20) });
      y -= 18;
    }

    if (t.comments) {
      y -= 4;
      page.drawText('Examiner comments:', { x: 36, y, size: 10, font: boldFont });
      y -= 14;
      wrapText(safe(t.comments), 88).forEach(line => {
        page.drawText(line, { x: 36, y, size: 9, font });
        y -= 12;
      });
    }

    y = Math.max(y - 20, 110);
    page.drawText('Examiner signature:', { x: 36, y, size: 10, font: boldFont });
    // pdf-lib doesn't support promises inline; defer with returned task.
    return { page, sigSlot: { y: y - 30, x: 170 }, footerLabel, sig: t.examinerSig || instructorSig };
  }

  async function applyProgressSignatures(pdfDoc, results) {
    for (const r of results) {
      if (r && r.sig) await embedSignature(pdfDoc, r.page, r.sig, r.sigSlot.x, r.sigSlot.y, 200);
      else if (r) r.page.drawLine({ start: { x: 170, y: r.sigSlot.y + 30 },
        end: { x: 360, y: r.sigSlot.y + 30 }, thickness: 0.6,
        color: window.PDFLib.rgb(0.6, 0.6, 0.6) });
      drawFooter(r.page, await pdfDoc.embedFont(window.PDFLib.StandardFonts.Helvetica), r.footerLabel);
    }
  }

  function renderTheory(pdfDoc, rec, font, boldFont, footerLabel) {
    const page = pdfDoc.addPage([A4.w, A4.h]);
    let y = drawHeader(page, font, boldFont, 'Theory Test — DGCA', rec);
    const th = rec.theory || {};
    const subs = window.THEORY_SUBJECTS || [];
    const pass = (window.Store && window.Store.getSettings) ? (window.Store.getSettings().theoryPassPct || 70) : 70;
    page.drawText(safe(`Exam date: ${th.examDate ? fmtDate(th.examDate) : '—'}     Overall: ${th.overallPct == null ? '—' : th.overallPct + '%'}     Pass mark: ${pass}%     Result: ${th.pass === true ? 'PASS' : th.pass === false ? 'FAIL' : '—'}`),
      { x: 36, y, size: 10, font });
    y -= 22;
    page.drawText('Subject', { x: 36, y, size: 10, font: boldFont });
    page.drawText('Score', { x: 460, y, size: 10, font: boldFont });
    y -= 10;
    page.drawLine({ start: { x: 36, y }, end: { x: A4.w - 36, y },
      thickness: 0.4, color: window.PDFLib.rgb(0.7, 0.7, 0.7) });
    y -= 12;
    subs.forEach(s => {
      const sc = th.scores && th.scores[s.id];
      page.drawText(safe(s.label), { x: 36, y, size: 9, font });
      page.drawText(sc == null ? '—' : (sc + '%'),
        { x: 460, y, size: 9, font });
      y -= 14;
    });
    drawFooter(page, font, footerLabel);
  }

  // Embed an uploaded file as one or more pages. PDFs copy as-is.
  async function appendUploadFile(targetPdf, file, footerLabel, font) {
    let bytes;
    try { bytes = await fetchBytes(file); }
    catch (e) {
      const page = targetPdf.addPage([A4.w, A4.h]);
      page.drawText('Could not fetch file: ' + safe(file.name), { x: 36, y: A4.h - 80, size: 12, font });
      page.drawText(safe(e.message || String(e)), { x: 36, y: A4.h - 100, size: 9, font });
      drawFooter(page, font, footerLabel);
      return;
    }
    const lower = (file.name || '').toLowerCase();
    if (lower.endsWith('.pdf')) {
      try {
        const src = await window.PDFLib.PDFDocument.load(bytes);
        const pages = await targetPdf.copyPages(src, src.getPageIndices());
        pages.forEach(p => targetPdf.addPage(p));
      } catch (e) {
        const page = targetPdf.addPage([A4.w, A4.h]);
        page.drawText('PDF could not be embedded: ' + safe(file.name),
          { x: 36, y: A4.h - 80, size: 12, font });
        drawFooter(page, font, footerLabel);
      }
    } else {
      // jpg/jpeg/png
      try {
        const img = lower.endsWith('.png')
          ? await targetPdf.embedPng(bytes)
          : await targetPdf.embedJpg(bytes);
        const page = targetPdf.addPage([A4.w, A4.h]);
        const margin = 36;
        const maxW = A4.w - margin * 2;
        const maxH = A4.h - margin * 2;
        const scaled = img.scaleToFit(maxW, maxH);
        page.drawImage(img, {
          x: (A4.w - scaled.width) / 2,
          y: (A4.h - scaled.height) / 2,
          width: scaled.width, height: scaled.height });
        drawFooter(page, font, footerLabel);
      } catch (e) {
        const page = targetPdf.addPage([A4.w, A4.h]);
        page.drawText('Image could not be embedded: ' + safe(file.name),
          { x: 36, y: A4.h - 80, size: 12, font });
        drawFooter(page, font, footerLabel);
      }
    }
  }

  /* ---------- default packet items ---------- */

  function defaultItems(rec) {
    const docs = rec.documents || {};
    // Defensive: partial legacy records can have an inst object with
    // only some of the fixed slots. `?.files` on each slot, not on the
    // outer guard, prevents an "undefined.files" crash when the card mounts.
    const inst = rec.instructorDocs || {};
    const items = [
      { id:'registration', title:'Completed Registration form', kind:'auto-reg' },
      { id:'attendance',   title:'Attendance Record',           kind:'auto-att' },
      { id:'marksheet',    title:'10th Marksheet',              kind:'upload', files: (docs.marksheet && docs.marksheet.files) || [] },
      { id:'aadhaar',      title:'Aadhaar',                     kind:'upload', files: (docs.aadhaar && docs.aadhaar.files) || [] },
      { id:'photo-id',     title:'Photo ID',                    kind:'upload', files: (docs['photo-id'] && docs['photo-id'].files) || [] },
      { id:'medical',      title:'Endorsed Medical Certificate', kind:'upload', files: (rec.medical && rec.medical.files) || [] },
      { id:'simulator',    title:'Simulator Progress Report',    kind:'auto-sim' },
      { id:'rpa',          title:'RPA Progress Report',          kind:'auto-rpa' },
      { id:'skill',        title:'Skill Test Report',            kind:'auto-skill' },
      { id:'theory',       title:'Theory Test (Marked)',         kind:'auto-theory' },
      { id:'idaCert',      title:'IDA Issued Certificate',       kind:'upload', files: inst.idaCert?.files || [] },
      { id:'egcaRpc',      title:'eGCA RPC',                     kind:'upload', files: inst.egcaRpc?.files || [] },
      { id:'logbook',      title:'Completed Logbook',            kind:'upload', files: inst.logbook?.files || [] },
    ];
    // Append the student's passport-size photograph (DGCA item 12) and
    // any free-form Extras the instructor added.
    if (docs.photo && (docs.photo.files || []).length) {
      items.push({ id:'photo', title:'Passport-size photo', kind:'upload', files: docs.photo.files });
    }
    (inst.extras || []).forEach(ex => {
      items.push({ id:'extra-' + ex.id, title: ex.title, kind:'upload', files: ex.files || [] });
    });
    return items.map((it, i) => ({ ...it, include: true, order: i + 1 }));
  }

  /* ---------- packet build orchestration ---------- */

  async function buildPacketBytes(rec, items, instructorSig) {
    const { PDFDocument, StandardFonts } = window.PDFLib;
    const out = await PDFDocument.create();
    const font = await out.embedFont(StandardFonts.Helvetica);
    const boldFont = await out.embedFont(StandardFonts.HelveticaBold);

    let n = 1;
    const progressJobs = [];
    for (const it of items) {
      if (!it.include) continue;
      const footer = `${n} — ${it.title}`;
      if (it.kind === 'auto-reg')   await renderRegistration(out, rec, font, boldFont, instructorSig);
      else if (it.kind === 'auto-att')   await renderAttendance(out, rec, font, boldFont, instructorSig);
      else if (it.kind === 'auto-sim')   progressJobs.push(renderProgressOrSkill(out, rec, font, boldFont, 'simulator', 'Simulator Progress Test', footer, instructorSig));
      else if (it.kind === 'auto-rpa')   progressJobs.push(renderProgressOrSkill(out, rec, font, boldFont, 'rpa',       'RPA Progress Test',       footer, instructorSig));
      else if (it.kind === 'auto-skill') progressJobs.push(renderProgressOrSkill(out, rec, font, boldFont, 'skill',     'DGCA Skill Test',         footer, instructorSig));
      else if (it.kind === 'auto-theory') renderTheory(out, rec, font, boldFont, footer);
      else if (it.kind === 'upload') {
        if (!it.files || it.files.length === 0) {
          const page = out.addPage([A4.w, A4.h]);
          drawHeader(page, font, boldFont, it.title, rec);
          page.drawText('No file attached.', { x: 36, y: A4.h - 110, size: 11, font });
          drawFooter(page, font, footer);
        } else {
          for (const f of it.files) await appendUploadFile(out, f, footer, font);
        }
      }
      n++;
    }
    await applyProgressSignatures(out, progressJobs);
    return out.save();
  }

  async function exportVerifiedZip(rec) {
    if (!window.JSZip) throw new Error('Zip library not loaded yet.');
    const zip = new window.JSZip();
    const folder = zip.folder(`${(rec.profile?.fullName || rec.id).replace(/[^\w.\-]+/g,'_')}-verified`);
    const docs = rec.documents || {};
    const inst = rec.instructorDocs || { logbook:{files:[]}, idaCert:{files:[]}, egcaRpc:{files:[]}, extras:[] };

    const bundle = [];
    Object.entries(docs).forEach(([id, ds]) => {
      if (ds && ds.status === 'verified' && (ds.files || []).length) {
        ds.files.forEach((f, i) => bundle.push({ folder: id, name: f.name || `${id}-${i+1}`, file: f }));
      }
    });
    if (rec.medical && rec.medical.status === 'verified') {
      (rec.medical.files || []).forEach((f, i) => bundle.push({ folder: 'medical', name: f.name || `medical-${i+1}`, file: f }));
    }
    ['logbook','idaCert','egcaRpc'].forEach(k => {
      (inst[k]?.files || []).forEach((f, i) => bundle.push({ folder: k, name: f.name || `${k}-${i+1}`, file: f }));
    });
    (inst.extras || []).forEach(ex => {
      (ex.files || []).forEach((f, i) => bundle.push({ folder: `extra-${ex.id}`, name: f.name || `${ex.title}-${i+1}`, file: f }));
    });

    let missing = 0;
    for (const item of bundle) {
      try {
        const bytes = await fetchBytes(item.file);
        folder.folder(item.folder).file(item.name, bytes);
      } catch (_) { missing++; }
    }
    const blob = await zip.generateAsync({ type: 'uint8array' });
    return { bytes: blob, total: bundle.length, missing };
  }

  /* ---------- React UI ---------- */

  function DGCAPacketCard({ rec, ctx }) {
    const [items, setItems] = useState(() => defaultItems(rec));
    const [busy, setBusy] = useState(false);
    const [zipBusy, setZipBusy] = useState(false);

    // Re-derive when the underlying record changes (new uploads etc).
    useEffect(() => { setItems(defaultItems(rec)); }, [rec.updatedAt]);

    const me = window.Store.getUser ? window.Store.getUser(ctx.session.uid) : null;
    const instructorSig = me && me.signature;

    const move = (ix, dir) => {
      const j = ix + dir;
      if (j < 0 || j >= items.length) return;
      const next = items.slice();
      [next[ix], next[j]] = [next[j], next[ix]];
      setItems(next);
    };
    const toggle = (ix) => setItems(items.map((it, i) => i === ix ? { ...it, include: !it.include } : it));

    const addExternal = () => {
      // Use any of the student's Extras that aren't already in the
      // list (handles re-adding after toggling off). Otherwise prompt.
      const inst = rec.instructorDocs || { extras: [] };
      const used = new Set(items.map(it => it.id));
      const avail = (inst.extras || []).filter(ex => !used.has('extra-' + ex.id));
      if (avail.length) {
        const ex = avail[0];
        setItems([...items, { id: 'extra-' + ex.id, title: ex.title, kind: 'upload', include: true, files: ex.files || [] }]);
      } else {
        ctx.setToast('Add a document in "Instructor uploads → + Add other document" first.');
      }
    };

    const onBuild = async () => {
      if (!window.PDFLib) { ctx.setToast('PDF library still loading — try again in a second'); return; }
      setBusy(true);
      try {
        const bytes = await buildPacketBytes(rec, items, instructorSig);
        const fname = `${(rec.profile?.fullName || rec.id).replace(/[^\w.\-]+/g,'_')}_DGCA_packet.pdf`;
        dlAs(bytes, fname, 'application/pdf');
        window.Store.logAudit('DGCA packet built', rec.profile.fullName,
          `${items.filter(i=>i.include).length} sections`);
        ctx.setToast('Packet downloaded');
      } catch (e) {
        console.error(e);
        ctx.setToast(e.message || 'Could not build packet');
      } finally { setBusy(false); }
    };

    const onZip = async () => {
      if (!window.JSZip) { ctx.setToast('Zip library still loading — try again in a second'); return; }
      setZipBusy(true);
      try {
        const { bytes, total, missing } = await exportVerifiedZip(rec);
        const fname = `${(rec.profile?.fullName || rec.id).replace(/[^\w.\-]+/g,'_')}_verified.zip`;
        dlAs(bytes, fname, 'application/zip');
        window.Store.logAudit('Verified files exported', rec.profile.fullName,
          `${total - missing}/${total} files`);
        ctx.setToast(missing
          ? `Downloaded — ${missing} file${missing===1?'':'s'} could not be fetched`
          : 'Zip downloaded');
      } catch (e) {
        console.error(e);
        ctx.setToast(e.message || 'Could not build zip');
      } finally { setZipBusy(false); }
    };

    return (
      <div className="card">
        <div className="card-hd">
          <div>
            <h3>DGCA submission packet</h3>
            <div className="sub">Pre-arranged in the DGCA order. Reorder with ↑/↓, untick to exclude, then export.</div>
          </div>
          <div className="row" style={{gap:8,flexWrap:'wrap'}}>
            <button className="btn btn-sm" onClick={addExternal}>+ Add other</button>
            <button className="btn btn-sm" disabled={zipBusy} onClick={onZip}>
              {zipBusy ? 'Zipping…' : 'Export verified files (zip)'}
            </button>
            <button className="btn btn-primary btn-sm" disabled={busy} onClick={onBuild}>
              {busy ? 'Building…' : 'Build DGCA packet (PDF)'}
            </button>
          </div>
        </div>
        {!instructorSig && (
          <div className="banner warn" style={{margin:'0 18px 12px'}}>
            <div className="body">
              You haven't saved a signature yet. Add one under <b>Account → Your signature</b> so it auto-stamps the progress and attendance pages.
            </div>
          </div>
        )}
        <div style={{padding:'0 4px 14px'}}>
          <table className="tbl">
            <thead><tr>
              <th style={{width:36}}></th>
              <th style={{width:54}}>#</th>
              <th>Document</th>
              <th style={{width:140}}>Source</th>
              <th style={{width:130}}>Reorder</th>
            </tr></thead>
            <tbody>
              {items.map((it, ix) => (
                <PacketRow key={it.id} it={it} ix={ix} total={items.length}
                  items={items} setItems={setItems} toggle={toggle} move={move}/>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    );
  }

  /* ---------- cohort-wide bulk export ---------- */

  async function exportCohortZip(students) {
    if (!window.JSZip) throw new Error('Zip library not loaded yet.');
    const zip = new window.JSZip();
    let totalFiles = 0, missing = 0;
    for (const rec of students) {
      const sub = (rec.profile?.fullName || rec.id).replace(/[^\w.\-]+/g,'_');
      const root = zip.folder(sub);
      const docs = rec.documents || {};
      const inst = rec.instructorDocs || { logbook:{files:[]}, idaCert:{files:[]}, egcaRpc:{files:[]}, extras:[] };

      const bundle = [];
      Object.entries(docs).forEach(([id, ds]) => {
        if (ds && ds.status === 'verified' && (ds.files || []).length) {
          ds.files.forEach((f, i) => bundle.push({ folder: id, name: f.name || `${id}-${i+1}`, file: f }));
        }
      });
      if (rec.medical && rec.medical.status === 'verified') {
        (rec.medical.files || []).forEach((f, i) => bundle.push({ folder: 'medical', name: f.name || `medical-${i+1}`, file: f }));
      }
      ['logbook','idaCert','egcaRpc'].forEach(k => {
        (inst[k]?.files || []).forEach((f, i) => bundle.push({ folder: k, name: f.name || `${k}-${i+1}`, file: f }));
      });
      (inst.extras || []).forEach(ex => {
        (ex.files || []).forEach((f, i) => bundle.push({ folder: `extra-${ex.id}`, name: f.name || `${ex.title}-${i+1}`, file: f }));
      });

      for (const item of bundle) {
        try {
          const bytes = await fetchBytes(item.file);
          root.folder(item.folder).file(item.name, bytes);
          totalFiles++;
        } catch (_) { missing++; }
      }
    }
    const blob = await zip.generateAsync({ type: 'uint8array' });
    return { bytes: blob, totalFiles, missing, students: students.length };
  }

  function CohortExportButton({ ctx }) {
    const [busy, setBusy] = useState(false);
    const onClick = async () => {
      if (!window.JSZip) { ctx.setToast('Zip library still loading — try again in a second'); return; }
      const students = window.Store.listStudents();
      if (!students.length) { ctx.setToast('No students to export'); return; }
      if (!confirm(`Export verified files for all ${students.length} student${students.length===1?'':'s'}? Large cohorts may take a minute.`)) return;
      setBusy(true);
      try {
        const { bytes, totalFiles, missing } = await exportCohortZip(students);
        const fname = `IDA-cohort-verified-${new Date().toISOString().slice(0,10)}.zip`;
        dlAs(bytes, fname, 'application/zip');
        window.Store.logAudit('Cohort export', '',
          `${students.length} students · ${totalFiles} files${missing ? ` · ${missing} missing` : ''}`);
        ctx.setToast(missing
          ? `Done · ${missing} file${missing===1?'':'s'} could not be fetched`
          : `Done · ${totalFiles} files from ${students.length} students`);
      } catch (e) {
        console.error(e);
        ctx.setToast(e.message || 'Could not build cohort zip');
      } finally { setBusy(false); }
    };
    return (
      <button className="btn btn-sm" disabled={busy} onClick={onClick}>
        {busy ? 'Exporting…' : 'Export cohort verified files (zip)'}
      </button>
    );
  }

  // Packet row — each TR is BOTH a draggable source AND a drop zone
  // so dropping row A onto row B splices A into B's position. Touch
  // and mouse share the same pointer-events code path via touch-dnd.
  function PacketRow({ it, ix, total, items, setItems, toggle, move }) {
    const dragRef = window.useDraggable(() => ix);
    const onDrop = useCallback((from) => {
      const f = parseInt(from, 10);
      if (!Number.isInteger(f) || f === ix) return;
      const next = items.slice();
      const [m] = next.splice(f, 1);
      next.splice(ix, 0, m);
      setItems(next);
    }, [ix, items, setItems]);
    const dropRef = window.useDropZone(onDrop);
    // Combine the two refs onto one TR via a callback ref. React calls
    // this with the DOM node on mount; both hooks' effects then see
    // ref.current set and wire up their event listeners.
    const setRefs = (el) => {
      if (dragRef) dragRef.current = el;
      if (dropRef) dropRef.current = el;
    };
    return (
      <tr ref={setRefs}
          style={{background: it.include ? 'transparent' : 'var(--bg-2)'}}>
        <td><input type="checkbox" checked={it.include} onChange={()=>toggle(ix)}/></td>
        <td className="mono small">{ix + 1}</td>
        <td>
          <div className="small" style={{fontWeight:600}}>{it.title}</div>
          {it.kind === 'upload' && (
            <div className="tiny muted">{(it.files || []).length} file{(it.files||[]).length===1?'':'s'} attached</div>
          )}
        </td>
        <td className="tiny muted">
          {it.kind === 'upload' ? 'Upload' :
           it.kind === 'auto-reg' ? 'Auto · registration' :
           it.kind === 'auto-att' ? 'Auto · attendance' :
           it.kind === 'auto-sim' ? 'Auto · simulator' :
           it.kind === 'auto-rpa' ? 'Auto · RPA' :
           it.kind === 'auto-skill' ? 'Auto · skill' :
           it.kind === 'auto-theory' ? 'Auto · theory' :
           '—'}
        </td>
        <td>
          <div className="row" style={{gap:4}}>
            <button className="btn btn-xs" onClick={()=>move(ix, -1)} disabled={ix === 0}>↑</button>
            <button className="btn btn-xs" onClick={()=>move(ix, 1)} disabled={ix === total - 1}>↓</button>
          </div>
        </td>
      </tr>
    );
  }

  // Expose helpers + components
  window.DGCAPacketCard = DGCAPacketCard;
  window.CohortExportButton = CohortExportButton;
  window.buildPacketBytes = buildPacketBytes;
  window.exportVerifiedZip = exportVerifiedZip;
  window.exportCohortZip = exportCohortZip;

})();
