/* Case workspace — lighter stage screens. Exposes screens on window. */

/* ── Review center ─────────────────────────────────────────── */
function uploadField(upload, label) {
  const row = upload && (upload.fields || []).find(([k]) => k === label);
  return row ? row[1] : null;
}

function uploadOfType(uploads, needle) {
  return (uploads || []).find((u) => ((u.type || "") + " " + (u.file || "")).toLowerCase().includes(needle));
}

function legalTone(status) {
  return status === "pass" ? "ok" : status === "review" ? "warn" : "err";
}

function legalLabel(status) {
  return status === "pass" ? "Pass" : status === "review" ? "Review" : "Blocker";
}

function buildLegalDataChecks() {
  const { ACTIVE_UPLOADS = [], SCENARIO = {} } = window.VVE;
  if (window.VVE_LEGAL && window.VVE_LEGAL.evaluate) {
    const validation = window.VVE_LEGAL.evaluate(window.__activeCaseId || "case", ACTIVE_UPLOADS);
    window.VVE_LEGAL_VALIDATION = validation;
    return validation.checks.map((check) => ({
      ...check,
      tone: legalTone(check.status),
      label: legalLabel(check.status),
      body: check.body + " Bewijs: " + check.evidence,
    }));
  }
  const budget = uploadOfType(ACTIVE_UPLOADS, "projectbegroting");
  const alv = uploadOfType(ACTIVE_UPLOADS, "alv-besluit");
  const mandate = uploadOfType(ACTIVE_UPLOADS, "machtiging");
  const ubo = uploadOfType(ACTIVE_UPLOADS, "ubo");
  const payment = uploadOfType(ACTIVE_UPLOADS, "betaalgedrag");
  const loanTerm = (SCENARIO.terms || []).find((t) => t.k === "Leenbedrag");
  const checks = [
    {
      key: "loan_amount",
      title: "Leenbedrag consistent met projectbegroting",
      body: budget ? "Leenbehoefte uit bron: " + (uploadField(budget, "Leenbehoefte") || "niet gevonden") + ". Scenario toont: " + (loanTerm ? loanTerm.v : "niet berekend") + "." : "Projectbegroting ontbreekt nog.",
      tone: budget && uploadField(budget, "Leenbehoefte") ? "ok" : "err",
      gate: "G6",
    },
    {
      key: "alv_decision",
      title: "ALV-besluit bevat besluit, stemuitslag en leenbedrag",
      body: alv ? "Besluit: " + (uploadField(alv, "Besluit") || "niet gevonden") + " · Stemuitslag: " + (uploadField(alv, "Stemuitslag") || "niet gevonden") + " · Leenbedrag: " + (uploadField(alv, "Leenbedrag") || "niet gevonden") + "." : "ALV-besluit ontbreekt nog.",
      tone: alv && uploadField(alv, "Besluit") && uploadField(alv, "Stemuitslag") ? "ok" : "err",
      gate: "G7",
    },
    {
      key: "mandate_signed",
      title: "Machtiging begeleiding aanvraag is ondertekend",
      body: mandate ? "Gemachtigde: " + (uploadField(mandate, "Gemachtigde") || "niet gevonden") + " · Ondertekening: " + (uploadField(mandate, "Ondertekening") || "niet gevonden") + "." : "Machtiging ontbreekt nog.",
      tone: mandate && /aanwezig|ondertekend/i.test(uploadField(mandate, "Ondertekening") || "") ? "ok" : "err",
      gate: "G8",
    },
    {
      key: "ubo_signed",
      title: "UBO-verklaring is ondertekend",
      body: ubo ? "Ondertekening: " + (uploadField(ubo, "Ondertekening") || "niet gevonden") + "." : "UBO-verklaring ontbreekt nog.",
      tone: ubo && /aanwezig|ondertekend/i.test(uploadField(ubo, "Ondertekening") || "") ? "ok" : "err",
      gate: "G8",
    },
    {
      key: "payment_behavior",
      title: "Betaalgedrag bevat geen materiele achterstanden",
      body: payment ? "Achterstanden leden: " + (uploadField(payment, "Achterstanden leden") || "controle nodig") + "." : "Verklaring betaalgedrag ontbreekt nog.",
      tone: payment && /geen/i.test(uploadField(payment, "Achterstanden leden") || "") ? "ok" : "warn",
      gate: "G5",
    },
  ];
  return checks;
}

function ReviewCenter() {
  const { REVIEW } = window.VVE;
  const [done, setDone] = React.useState({});
  const [notes, setNotes] = React.useState({});
  const [legalDone, setLegalDone] = React.useState({});
  const caseKey = window.__activeCaseId || "case";
  const legalChecks = buildLegalDataChecks();
  const legalValidation = window.VVE_LEGAL_VALIDATION || null;
  React.useEffect(() => {
    let live = true;
    if (window.VVE_STORAGE) {
      window.VVE_STORAGE.loadUiState("review-center:" + caseKey, null).then((stored) => {
        if (!live || !stored) return;
        setDone(stored.done || {});
        setNotes(stored.notes || {});
        setLegalDone(stored.legalDone || {});
        window.VVE_APPROVAL_STATE = stored;
      }).catch(() => {});
    }
    return () => { live = false; };
  }, [caseKey]);
  React.useEffect(() => {
    if (window.VVE_STORAGE && window.VVE_STORAGE.syncLegalValidation) {
      window.VVE_STORAGE.syncLegalValidation(caseKey, window.VVE.ACTIVE_UPLOADS || [])
        .catch((error) => console.warn("VVE legal validation sync failed", error));
    }
  }, [caseKey]);
  const persistApprovalState = (nextDone, nextNotes, nextLegalDone) => {
    const state = {
      caseId: caseKey,
      done: nextDone,
      notes: nextNotes,
      legalDone: nextLegalDone,
      legalChecks: legalChecks.map((check) => ({ ...check, accepted: !!nextLegalDone[check.key] })),
      legalValidation,
      updatedAt: new Date().toISOString(),
    };
    window.VVE_APPROVAL_STATE = state;
    if (window.VVE_STORAGE) window.VVE_STORAGE.saveUiState("review-center:" + caseKey, state).catch(() => {});
    if (window.VVE_STORAGE && window.VVE_STORAGE.syncCaseState) {
      window.VVE_STORAGE.syncCaseState(caseKey, {
        approvals: state,
        legalValidation,
        legalValidationSummary: legalValidation ? legalValidation.summary : null,
        approvalCount: Object.keys(nextDone || {}).length + Object.keys(nextLegalDone || {}).filter((key) => nextLegalDone[key]).length,
      }).catch((error) => console.warn("VVE approval backend sync failed", error));
    }
  };
  const markReview = (id, label) => {
    const nextDone = { ...done, [id]: 1 };
    const nextNotes = { ...notes, [id]: label };
    setDone(nextDone);
    setNotes(nextNotes);
    persistApprovalState(nextDone, nextNotes, legalDone);
  };
  const toggleLegal = (key) => {
    const nextLegal = { ...legalDone, [key]: !legalDone[key] };
    setLegalDone(nextLegal);
    persistApprovalState(done, notes, nextLegal);
  };
  const blockers = REVIEW.filter((r) => r.sev === "err").length;
  const warnings = REVIEW.filter((r) => r.sev === "warn").length;
  const openLegal = legalChecks.filter((check) => !legalDone[check.key]).length;
  return (
    <div className="stack">
      <div className="banner err">
        <Icon name="flag" size={18} style={{ flex: "0 0 auto", marginTop: 1 }} />
        <div><div className="bt">{blockers} blockers · {warnings} reviewpunten · {openLegal} juridische/data-checks open</div>
          <div className="bd">Jeroen verwerkt de bronchecks, ontbrekende verklaringen en lage-confidence extracties voordat VVE Finance een indieningspakket mag vrijgeven.</div></div>
      </div>
      <div className="card">
        <div className="card-head"><h3><Icon name="shield" size={16} />Legal & data checks</h3><Badge tone={openLegal ? "warn" : "ok"} sm>{legalValidation ? `${legalValidation.summary.pass} pass · ${legalValidation.summary.review} review · ${legalValidation.summary.blocker} blockers` : openLegal ? "human approval" : "akkoord"}</Badge></div>
        {legalValidation && (
          <div className="banner info" style={{ margin: "12px 16px" }}>
            <Icon name="shield" size={17} style={{ flex: "0 0 auto", marginTop: 1 }} />
            <div><div className="bt">Deterministische dossiercontrole · geen juridisch advies</div>
              <div className="bd">VVE Finance toetst Warmtefonds-minimums, splitsingsakte-aanwezigheid en formulierhandtekeningen. Jeroen of juridisch adviseur accordeert de uitkomst.</div></div>
          </div>
        )}
        {legalChecks.map((check) => (
          <div className="lrow" key={check.key}>
            <div className={"sdisc " + check.tone}><Icon name={check.tone === "ok" ? "check" : check.tone === "warn" ? "flag" : "alert"} size={16} /></div>
            <div className="lr-main">
              <div className="lr-title">{check.title}<Badge tone={check.tone} sm>{check.label || (check.tone === "ok" ? "Pass" : check.tone === "warn" ? "Review" : "Blocker")}</Badge></div>
              <div className="lr-sub" style={{ maxWidth: "72ch" }}>{check.body}</div>
              <div className="metaline">
                <span className="mono">Gate {check.gate}</span><span><Icon name="user" size={12} />Jeroen keurt expliciet goed</span>
                {check.sourceUrls && check.sourceUrls[0] && <span><Icon name="link" size={12} /><a href={check.sourceUrls[0]} target="_blank" rel="noreferrer">bronregel</a></span>}
              </div>
            </div>
            <div className="lr-side">
              <button className={"btn sm " + (legalDone[check.key] ? "outline" : "primary")} onClick={() => toggleLegal(check.key)}>
                <Icon name={legalDone[check.key] ? "history" : "check"} size={14} />{legalDone[check.key] ? "Heropen" : "Accepteer check"}
              </button>
            </div>
          </div>
        ))}
      </div>
      <div className="card">
        {REVIEW.map((r) => (
          <div className="lrow" key={r.id} style={{ opacity: done[r.id] ? 0.5 : 1 }}>
            <div className={"sdisc " + r.sev}>
              <Icon name={r.sev === "err" ? "alert" : r.sev === "warn" ? "flag" : "fileText"} size={16} />
            </div>
            <div className="lr-main">
              <div className="lr-title">{r.title}<Badge tone={r.sev} sm>{r.cat}</Badge></div>
              <div className="lr-sub" style={{ maxWidth: "64ch" }}>{r.body}</div>
              <div className="metaline">
                <span><Icon name="user" size={12} />{r.owner}</span>
                <span className="mono">Gate {r.gate}</span>
                {r.deadline && <span><Icon name="clock" size={12} />deadline {new Date(r.deadline + "T00:00:00").toLocaleDateString("nl-NL", { day: "numeric", month: "short" })}</span>}
              </div>
            </div>
            <div className="lr-side">
              {done[r.id]
                ? <Badge tone="ok" sm dot>{notes[r.id] || "Verwerkt"}</Badge>
                : <div className="row gap6">
                    <button className="btn ghost sm" onClick={() => markReview(r.id, "Gecorrigeerd")}>Corrigeer</button>
                    <button className="btn primary sm" onClick={() => markReview(r.id, "Geaccepteerd")}>Accepteer</button>
                  </div>}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ── Financieel scenario ───────────────────────────────────── */
function Scenario() {
  const { SCENARIO } = window.VVE;
  const [approved, setApproved] = React.useState(false);
  const max = Math.max(...SCENARIO.waterfall.map((w) => Math.abs(w.amount)));
  const loan = SCENARIO.waterfall.find((w) => w.type === "loan");
  const colorFor = (t) => t === "cost" ? "var(--tge-ink)" : t === "loan" ? "var(--tge-navy)" : "var(--tge-emerald)";
  return (
    <div className="grid-2">
      <div className="card">
        <div className="card-head"><h3><Icon name="coins" size={16} />Kostenwaterfall</h3>
          <Badge tone="neutral" sm>{SCENARIO.route}</Badge></div>
        <div className="card-body">
          <div className="wf">
            {SCENARIO.waterfall.map((w) => (
              <div className="wf-row" key={w.label}>
                <span className="wl">{w.label}</span>
                <div className="wf-bar">
                  <i style={{ left: 0, width: (Math.abs(w.amount) / max * 100) + "%", background: colorFor(w.type), opacity: w.type === "cost" ? 1 : 0.85 }}></i>
                </div>
                <span className="wf-amt" style={{ color: w.amount < 0 ? "var(--st-ok-fg)" : "var(--fg1)" }}>{window.fmtEUR(w.amount)}</span>
              </div>
            ))}
            <div className="wf-row total">
              <span className="wl">Leenbehoefte</span><div></div>
              <span className="wf-amt">{window.fmtEUR(loan ? loan.amount : 0)}</span>
            </div>
          </div>
        </div>
      </div>
      <div className="stack">
        <div className="card">
          <div className="card-head"><h3><Icon name="fileText" size={16} />Indicatief scenario</h3></div>
          <div className="card-body" style={{ paddingTop: 4, paddingBottom: 4 }}>
            {SCENARIO.terms.map((t) => (
              <div className="kv" key={t.k}><span className="k">{t.k}</span><span className="v">{t.v}</span></div>
            ))}
          </div>
        </div>
        <div className="banner warn">
          <Icon name="alert" size={18} style={{ flex: "0 0 auto", marginTop: 1 }} />
          <div><div className="bt">Indicatief — geen kredietaanbod</div>
            <div className="bd">Rente en bedragen zijn bron-gecheckt, maar blijven indicatief tot Warmtefonds een aanbod doet. Gate G6 · scenario-akkoord door Jeroen.</div></div>
        </div>
        <button className={"btn " + (approved ? "outline" : "primary")} style={{ alignSelf: "flex-start" }} onClick={() => setApproved(true)}>
          <Icon name="check" size={15} />{approved ? "Scenario geaccordeerd" : "Accordeer scenario"}
        </button>
      </div>
    </div>
  );
}

/* ── ALV-pack ──────────────────────────────────────────────── */
function AlvPack() {
  const { ALV } = window.VVE;
  const [preview, setPreview] = React.useState(null);
  return (
    <div className="stack">
      {preview && (
        <div className="banner info">
          <Icon name="eye" size={18} style={{ flex: "0 0 auto", marginTop: 1 }} />
          <div><div className="bt">Review geopend: {preview.name}</div>
            <div className="bd">{preview.note}. In de MVP opent hier de bewerkbare documentpreview met besluittekst, bronverwijzingen en comments.</div></div>
        </div>
      )}
      <div className="banner info">
        <Icon name="gavel" size={18} style={{ flex: "0 0 auto", marginTop: 1 }} />
        <div><div className="bt">Alles blijft concept tot review</div>
          <div className="bd">Conceptstukken voor besluitvorming. Besluitgeldigheid blijft altijd menselijke of juridische review — Gate G7.</div></div>
      </div>
      <div className="card">
        {ALV.map((a) => (
          <div className="doc" key={a.name}>
            <div className={"sdisc " + (a.status === "legal" ? "warn" : "neutral")}>
              <Icon name={a.status === "legal" ? "shield" : "fileText"} size={16} />
            </div>
            <div className="dn"><b>{a.name}</b><div className="df" style={{ fontFamily: "var(--font-body)" }}>{a.note}</div></div>
            <Badge tone={a.status === "legal" ? "warn" : "neutral"} sm>{a.status === "legal" ? "Juridische review" : "Concept"}</Badge>
            <button className="btn ghost sm" onClick={() => setPreview(a)}><Icon name="eye" size={14} />Review</button>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ── Export center ─────────────────────────────────────────── */
function packageSlug(value) {
  return String(value || "vve").toLowerCase()
    .normalize("NFD").replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
}

function csvCell(value) {
  const text = String(value == null ? "" : value);
  return /[",\n]/.test(text) ? "\"" + text.replace(/"/g, "\"\"") + "\"" : text;
}

function buildDocumentIndex(docs, uploads) {
  const rows = [["Groep", "Document", "Vereiste", "Status", "Bestand", "Confidence", "Upload parser"]];
  docs.forEach((group) => {
    group.items.forEach((doc) => {
      const upload = uploads.find((u) => u.file === doc.file || u.type === doc.name);
      rows.push([
        group.group,
        doc.name,
        doc.req,
        doc.status,
        doc.file || "",
        doc.conf == null ? "" : Math.round(doc.conf * 100) + "%",
        upload ? upload.parser : "",
      ]);
    });
  });
  return rows.map((row) => row.map(csvCell).join(",")).join("\n");
}

const WARMTEFONDS_TEMPLATE_VERSION = "warmtefonds-mvp-v1.0";
const GENERATED_PACKAGE_DOCUMENTS = [
  { path: "00_memo/warmtefonds_route_memo.docx", title: "Warmtefonds route-memo", format: "DOCX", purpose: "Adviseurshandoff en aanvraagcontext" },
  { path: "00_memo/warmtefonds_route_memo.pdf", title: "Warmtefonds route-memo", format: "PDF", purpose: "Leesbare export voor handmatige indiening" },
  { path: "04_besluitvorming/alv_besluit_lening.docx", title: "ALV-besluit lening", format: "DOCX", purpose: "Bewerkbaar conceptbesluit voor review" },
  { path: "04_besluitvorming/notulen_template.docx", title: "Notulen-template Warmtefonds", format: "DOCX", purpose: "Bewerkbaar verslag en stemvastlegging" },
  { path: "06_risico_en_approval_log/legal_validation_report.docx", title: "Legal/data validation report", format: "DOCX", purpose: "Menselijke approval en risico-overzicht" },
  { path: "06_risico_en_approval_log/legal_validation_report.pdf", title: "Legal/data validation report", format: "PDF", purpose: "Leesbare export van checks en approvals" },
];

function textLines(value) {
  return String(value == null ? "" : value).split(/\r?\n/).filter(Boolean);
}

function factValue(facts, key, fallback) {
  const row = (facts || []).find((f) => f.k === key);
  return row && row.v ? row.v : fallback;
}

function termValue(scenario, key, fallback) {
  const row = ((scenario && scenario.terms) || []).find((t) => t.k === key);
  return row && row.v ? row.v : fallback;
}

function sourceLine(upload) {
  return upload.file + " -> " + upload.type + " (" + Math.round((upload.conf || 0) * 100) + "%, " + (upload.parser || "parser onbekend") + ")";
}

function xmlEscape(value) {
  return String(value == null ? "" : value)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
}

function docxParagraph(text, style) {
  const pStyle = style ? '<w:pPr><w:pStyle w:val="' + style + '"/></w:pPr>' : "";
  return "<w:p>" + pStyle + "<w:r><w:t xml:space=\"preserve\">" + xmlEscape(text) + "</w:t></w:r></w:p>";
}

function docxBody(title, sections) {
  const paragraphs = [docxParagraph(title, "Title")];
  sections.forEach((section) => {
    if (section.heading) paragraphs.push(docxParagraph(section.heading, "Heading1"));
    (section.lines || []).forEach((line) => paragraphs.push(docxParagraph(line, line === "" ? null : section.style)));
  });
  paragraphs.push("<w:sectPr><w:pgSz w:w=\"11906\" w:h=\"16838\"/><w:pgMar w:top=\"1134\" w:right=\"1134\" w:bottom=\"1134\" w:left=\"1134\"/></w:sectPr>");
  return paragraphs.join("");
}

async function buildDocxBlob(title, sections) {
  const docx = new JSZip();
  docx.file("[Content_Types].xml",
    '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
    '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
    '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
    '<Default Extension="xml" ContentType="application/xml"/>' +
    '<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>' +
    '<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>' +
    '<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>' +
    '<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>' +
    '</Types>');
  docx.file("_rels/.rels",
    '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
    '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
    '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>' +
    '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>' +
    '<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>' +
    '</Relationships>');
  docx.file("word/document.xml",
    '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
    '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">' +
    '<w:body>' + docxBody(title, sections) + '</w:body></w:document>');
  docx.file("word/styles.xml",
    '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
    '<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">' +
    '<w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/><w:rPr><w:b/><w:sz w:val="32"/></w:rPr></w:style>' +
    '<w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/><w:rPr><w:b/><w:sz w:val="24"/></w:rPr></w:style>' +
    '</w:styles>');
  docx.file("docProps/core.xml",
    '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
    '<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/">' +
    '<dc:title>' + xmlEscape(title) + '</dc:title><dc:creator>VVE Finance</dc:creator>' +
    '<dcterms:created xsi:type="dcterms:W3CDTF" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' + new Date().toISOString() + '</dcterms:created>' +
    '</cp:coreProperties>');
  docx.file("docProps/app.xml",
    '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
    '<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Application>VVE Finance</Application></Properties>');
  return docx.generateAsync({
    type: "blob",
    mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  });
}

function pdfSafe(value) {
  return String(value == null ? "" : value)
    .replace(/€/g, "EUR ")
    .replace(/[–—]/g, "-")
    .replace(/[·]/g, "-")
    .replace(/[^\x20-\x7E]/g, " ")
    .replace(/\\/g, "\\\\")
    .replace(/\(/g, "\\(")
    .replace(/\)/g, "\\)");
}

function buildPdfBlob(title, sections) {
  const lines = [];
  sections.forEach((section) => {
    if (section.heading) lines.push(section.heading);
    (section.lines || []).forEach((line) => lines.push(line));
    lines.push("");
  });
  const contentLines = [title, "", ...lines].slice(0, 52);
  const ops = ["BT", "/F1 16 Tf", "50 790 Td", "(" + pdfSafe(contentLines[0]) + ") Tj", "0 -26 Td", "/F1 10 Tf"];
  contentLines.slice(1).forEach((line) => {
    ops.push("(" + pdfSafe(line).slice(0, 110) + ") Tj");
    ops.push("0 -15 Td");
  });
  ops.push("ET");
  const stream = ops.join("\n");
  const objects = [
    "<< /Type /Catalog /Pages 2 0 R >>",
    "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
    "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
    "<< /Length " + stream.length + " >>\nstream\n" + stream + "\nendstream",
    "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
  ];
  let pdf = "%PDF-1.4\n";
  const offsets = [0];
  objects.forEach((object, index) => {
    offsets.push(pdf.length);
    pdf += (index + 1) + " 0 obj\n" + object + "\nendobj\n";
  });
  const xrefStart = pdf.length;
  pdf += "xref\n0 " + (objects.length + 1) + "\n0000000000 65535 f \n";
  offsets.slice(1).forEach((offset) => {
    pdf += String(offset).padStart(10, "0") + " 00000 n \n";
  });
  pdf += "trailer\n<< /Size " + (objects.length + 1) + " /Root 1 0 R >>\nstartxref\n" + xrefStart + "\n%%EOF";
  return new Blob([pdf], { type: "application/pdf" });
}

function routeMemoSections(vve, facts, scenario, uploads) {
  return [
    {
      heading: "Samenvatting",
      lines: [
        "VvE: " + vve,
        "Route: " + (scenario.route || "Warmtefonds VvE Energiebespaarlening"),
        "Leenbedrag: " + termValue(scenario, "Leenbedrag", "Nog niet berekend"),
        "Gegenereerd: " + new Date().toLocaleString("nl-NL"),
        "Status: handmatig in te dienen bij Warmtefonds; geen automatische indiening.",
      ],
    },
    { heading: "Kerngegevens uit dossier", lines: (facts || []).map((f) => f.k + ": " + f.v + " (bron: " + f.src + ")") },
    { heading: "Financieel scenario", lines: ((scenario && scenario.terms) || []).map((t) => t.k + ": " + t.v) },
    { heading: "Gekoppelde brondocumenten", lines: uploads.length ? uploads.map(sourceLine) : ["Geen sessie-uploads gekoppeld."] },
  ];
}

function alvDecisionSections(vve, facts, scenario, uploads) {
  const alv = uploadOfType(uploads, "alv-besluit");
  return [
    {
      heading: "Conceptbesluit lening",
      lines: [
        "De vergadering van eigenaars van " + vve + " besluit om een VvE Energiebespaarlening bij het Nationaal Warmtefonds aan te vragen.",
        "Indicatief leenbedrag: " + termValue(scenario, "Leenbedrag", factValue(facts, "Leenbehoefte", "nog niet berekend")) + ".",
        "Looptijd: " + termValue(scenario, "Looptijd", "20 jaar concept") + ".",
        "Maatregelen en projectbegroting worden vastgesteld op basis van de gekoppelde projectdocumenten.",
        "Dit document blijft concept tot juridische review van splitsingsakte, stemrechten en ALV-procedure.",
      ],
    },
    {
      heading: "Stem- en quorumvastlegging",
      lines: [
        "Aanwezigheid/opkomst: " + (uploadField(alv, "Opkomst") || uploadField(alv, "Aanwezigheid") || "door Jeroen te controleren"),
        "Stemuitslag: " + (uploadField(alv, "Stemuitslag") || "door Jeroen te controleren"),
        "Besluitstatus: " + (uploadField(alv, "Besluit") || "door Jeroen te controleren"),
      ],
    },
    { heading: "Bronnen", lines: uploads.length ? uploads.map(sourceLine) : ["Geen brondocumenten gekoppeld."] },
  ];
}

function legalReportSections(gates, audit, legalValidation, sources) {
  const checks = legalValidation && legalValidation.checks ? legalValidation.checks : [];
  return [
    {
      heading: "Validatiemodus",
      lines: [
        "Modus: deterministic_rules_human_review",
        "Juridisch advies: nee",
        "Templateversie: " + WARMTEFONDS_TEMPLATE_VERSION,
        "Jeroen of juridisch adviseur blijft eigenaar van finale goedkeuring.",
      ],
    },
    {
      heading: "Legal/data checks",
      lines: checks.length ? checks.map((check) => legalLabel(check.status) + " - " + check.title + " - " + check.evidence) : ["Geen legal validation state beschikbaar in deze browsersessie."],
    },
    {
      heading: "Vrijgave-gates",
      lines: (gates || []).map((gate) => (gate.done ? "Voldaan - " : "Open - ") + gate.label),
    },
    {
      heading: "Bronnenregister",
      lines: (sources || []).map((source) => source.title + " - " + (source.url || "geen URL") + " - status " + (source.status || "onbekend")),
    },
    {
      heading: "Audittrail",
      lines: (audit || []).map((item) => item.time + " - " + item.txt),
    },
  ];
}

function routeMemoText(vve, facts, scenario, uploads) {
  const lines = [
    "Warmtefonds route-memo",
    "=======================",
    "",
    "VvE: " + vve,
    "Route: " + scenario.route,
    "Gegenereerd: " + new Date().toLocaleString("nl-NL"),
    "",
    "Kerngegevens",
    "-----------",
    ...facts.map((f) => "- " + f.k + ": " + f.v + " (bron: " + f.src + ")"),
    "",
    "Scenario",
    "--------",
    ...scenario.terms.map((t) => "- " + t.k + ": " + t.v),
    "",
    "Gekoppelde brondocumenten",
    "-------------------------",
    ...(uploads.length ? uploads.map((u) => "- " + u.file + " -> " + u.type + " (" + Math.round((u.conf || 0) * 100) + "%)") : ["- Geen sessie-uploads gekoppeld"]),
    "",
    "Let op: dit pakket is voorbereid voor handmatige indiening. VVE Finance dient niet automatisch in bij Warmtefonds.",
  ];
  return lines.join("\n");
}

async function generateWarmtefondsPackage({ gates }) {
  if (!window.JSZip) throw new Error("JSZip is niet geladen");
  const {
    FACTS = [], SCENARIO = {}, ALV = [], EXPORT = {}, AUDIT = [], DOCS = [], ACTIVE_UPLOADS = [],
    WARMTEFONDS_RULES = [], WARMTEFONDS_SOURCES = [],
  } = window.VVE;
  const vve = (FACTS.find((f) => f.k === "VvE-naam") || {}).v || "VvE";
  const caseId = (window.__activeCaseId || "case").toLowerCase();
  const fileName = packageSlug(caseId + "-" + vve + "-warmtefonds-pakket") + ".zip";
  const zip = new JSZip();
  const generatedAt = new Date().toISOString();
  const legalValidation = window.VVE_LEGAL_VALIDATION || null;
  const routeSections = routeMemoSections(vve, FACTS, SCENARIO, ACTIVE_UPLOADS);
  const alvSections = alvDecisionSections(vve, FACTS, SCENARIO, ACTIVE_UPLOADS);
  const legalSections = legalReportSections(gates, AUDIT, legalValidation, WARMTEFONDS_SOURCES);
  const generatedDocuments = GENERATED_PACKAGE_DOCUMENTS.map((doc) => ({
    ...doc,
    templateVersion: WARMTEFONDS_TEMPLATE_VERSION,
    generatedAt,
  }));

  zip.file("00_route_memo.txt", routeMemoText(vve, FACTS, SCENARIO, ACTIVE_UPLOADS));
  zip.file("00_memo/warmtefonds_route_memo.docx", await buildDocxBlob("Warmtefonds route-memo", routeSections));
  zip.file("00_memo/warmtefonds_route_memo.pdf", buildPdfBlob("Warmtefonds route-memo", routeSections));
  zip.file("01_document_index.csv", buildDocumentIndex(DOCS, ACTIVE_UPLOADS));
  zip.file("02_extracted_fields.json", JSON.stringify(ACTIVE_UPLOADS.map((u) => ({
    file: u.file,
    type: u.type,
    target: u.target,
    parser: u.parser,
    confidence: u.conf,
    rawChars: u.rawChars || 0,
    fields: u.fields || [],
  })), null, 2));
  zip.file("03_review_and_approval_log.json", JSON.stringify({
    gates,
    audit: AUDIT,
    persistedApprovalState: window.VVE_APPROVAL_STATE || null,
    legalValidation,
    generatedAt,
    generatedBy: "Jeroen Koster",
  }, null, 2));
  zip.file("04_warmtefonds_checklist.json", JSON.stringify({
    rules: WARMTEFONDS_RULES,
    sources: WARMTEFONDS_SOURCES,
    contents: EXPORT.contents,
  }, null, 2));
  zip.file("04_besluitvorming/alv_besluit_lening.docx", await buildDocxBlob("ALV-besluit Warmtefonds lening", alvSections));
  zip.file("04_besluitvorming/notulen_template.docx", await buildDocxBlob("Notulen-template Warmtefonds", [
    ...alvSections,
    { heading: "Notulen vastlegging", lines: [
      "Datum vergadering: in te vullen",
      "Voorzitter: in te vullen",
      "Aantal stemrechten totaal: " + factValue(FACTS, "Appartementsrechten", "in te vullen"),
      "Aantal stemrechten aanwezig/vertegenwoordigd: in te vullen",
      "Besluit, stemuitslag en eventuele bezwaren worden hier vastgelegd.",
    ] },
  ]));
  zip.file("06_risico_en_approval_log/legal_validation_report.docx", await buildDocxBlob("Legal/data validation report", legalSections));
  zip.file("06_risico_en_approval_log/legal_validation_report.pdf", buildPdfBlob("Legal/data validation report", legalSections));
  zip.file("template_manifest.json", JSON.stringify({
    caseId,
    vve,
    templateVersion: WARMTEFONDS_TEMPLATE_VERSION,
    generatedAt,
    generatedBy: "VVE Finance",
    generatedDocuments,
    sourceDocumentCount: ACTIVE_UPLOADS.length,
    legalValidationSummary: legalValidation ? legalValidation.summary : null,
    legalAdvice: false,
    submissionMode: "manual_warmtefonds_handoff",
  }, null, 2));

  const alvFolder = zip.folder("05_alv_concepts");
  ALV.forEach((item, index) => {
    alvFolder.file(String(index + 1).padStart(2, "0") + "_" + packageSlug(item.name) + ".txt",
      item.name + "\n" + "=".repeat(item.name.length) + "\n\n" + item.note + "\n\nStatus: " + item.status + "\n");
  });

  const sourceFolder = zip.folder("06_source_documents");
  for (const upload of ACTIVE_UPLOADS) {
    if (upload.originalFile) {
      sourceFolder.file(upload.file, upload.originalFile);
    } else {
      sourceFolder.file(packageSlug(upload.file) + ".txt", upload.textPreview || JSON.stringify(upload.fields || [], null, 2));
    }
  }

  const blob = await zip.generateAsync({ type: "blob" });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = fileName;
  document.body.appendChild(link);
  link.click();
  link.remove();
  setTimeout(() => URL.revokeObjectURL(url), 2000);
  return {
    fileName,
    docCount: ACTIVE_UPLOADS.length,
    byteSize: blob.size,
    templateVersion: WARMTEFONDS_TEMPLATE_VERSION,
    generatedDocumentCount: generatedDocuments.length,
    generatedDocuments,
  };
}

function ExportCenter() {
  const { EXPORT, ACTIVE_UPLOADS = [] } = window.VVE;
  const [gates, setGates] = React.useState(EXPORT.gates);
  const [released, setReleased] = React.useState(false);
  const [generating, setGenerating] = React.useState(false);
  const [packageInfo, setPackageInfo] = React.useState(null);
  const caseKey = window.__activeCaseId || "case";
  React.useEffect(() => {
    let live = true;
    if (window.VVE_STORAGE) {
      window.VVE_STORAGE.loadUiState("package:" + caseKey, null).then((stored) => {
        if (!live || !stored) return;
        setPackageInfo(stored.packageInfo || null);
        setReleased(!!stored.released);
      }).catch(() => {});
    }
    return () => { live = false; };
  }, [caseKey]);
  const open = gates.filter((g) => !g.done).length;
  const approveGate = (label) => setGates((prev) => prev.map((g) => g.label === label ? { ...g, done: true } : g));
  const approveAll = () => setGates((prev) => prev.map((g) => ({ ...g, done: true })));
  const ready = open === 0;
  const generatePackage = async () => {
    if (!ready || generating) return;
    setGenerating(true);
    try {
      const info = await generateWarmtefondsPackage({ gates });
      setPackageInfo(info);
      setReleased(true);
      if (window.VVE_STORAGE) {
        window.VVE_STORAGE.saveUiState("package:" + caseKey, {
          packageInfo: info,
          released: true,
          sourceDocumentCount: ACTIVE_UPLOADS.length,
          generatedAt: new Date().toISOString(),
        }).catch(() => {});
        if (window.VVE_STORAGE.syncPackage) {
          window.VVE_STORAGE.syncPackage(caseKey, {
            packageInfo: info,
            gates,
            outputs: EXPORT.contents || [],
            generatedDocuments: info.generatedDocuments || [],
            templateVersion: info.templateVersion || WARMTEFONDS_TEMPLATE_VERSION,
            sourceDocumentCount: ACTIVE_UPLOADS.length,
            generatedAt: new Date().toISOString(),
          }).catch((error) => console.warn("VVE package backend sync failed", error));
        }
      }
    } catch (error) {
      window.openNotice && window.openNotice({
        eyebrow: "Export",
        title: "Pakket genereren mislukt",
        body: error.message || "Controleer of de ZIP-generator geladen is.",
      });
    } finally {
      setGenerating(false);
    }
  };
  return (
    <div className="grid-2">
      <div className="card">
        <div className="card-head"><h3><Icon name="checkCircle" size={16} />Vrijgave-voorwaarden</h3>
          <span className="meta">{gates.length - open}/{gates.length} voldaan</span></div>
        {gates.map((g) => (
          <div className="doc" key={g.label}>
            <div className={"sdisc " + (g.done ? "ok" : "neutral")}>
              <Icon name={g.done ? "check" : "clock"} size={16} />
            </div>
            <div className="dn"><b style={{ color: g.done ? "var(--fg1)" : "var(--fg2)" }}>{g.label}</b></div>
            <Badge tone={g.done ? "ok" : "neutral"} sm dot={g.done}>{g.done ? "Voldaan" : "Open"}</Badge>
            {!g.done && <button className="btn outline sm" onClick={() => approveGate(g.label)}>Accordeer</button>}
          </div>
        ))}
        <div className="card-body" style={{ borderTop: "1px solid var(--border)" }}>
          <button className="btn outline sm" onClick={approveAll}><Icon name="checkCircle" size={14} />Accordeer alle gates</button>
        </div>
      </div>
      <div className="stack">
        <div className="card">
          <div className="card-head"><h3><Icon name="package" size={16} />Pakketinhoud</h3><Badge tone="neutral" sm>Warmtefonds</Badge></div>
          <div className="card-body">
            {EXPORT.contents.map((c) => (
              <div className="package-row" key={c}>
                <Icon name="fileText" size={14} style={{ color: "var(--fg2)" }} /><span>{c}</span>
              </div>
            ))}
            <div style={{ marginTop: 14, paddingTop: 12, borderTop: "1px solid var(--border)" }}>
              <div className="tiny muted" style={{ marginBottom: 8, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em" }}>Gegenereerde Warmtefonds-documenten</div>
              {GENERATED_PACKAGE_DOCUMENTS.map((d) => (
                <div className="package-row" key={d.path}>
                  <Icon name={d.format === "PDF" ? "fileText" : "fileCheck"} size={14} style={{ color: d.format === "PDF" ? "var(--fg2)" : "var(--tge-emerald)" }} />
                  <span>{d.title} · {d.format}</span>
                </div>
              ))}
            </div>
            {ACTIVE_UPLOADS.length > 0 && (
              <div style={{ marginTop: 14, paddingTop: 12, borderTop: "1px solid var(--border)" }}>
                <div className="tiny muted" style={{ marginBottom: 8, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em" }}>Gekoppelde brondocumenten</div>
                {ACTIVE_UPLOADS.map((d) => (
                  <div className="package-row" key={d.file}>
                    <Icon name="fileCheck" size={14} style={{ color: "var(--tge-emerald)" }} /><span>{d.file} · {d.type}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>
        <div className={"banner " + (released ? "ink" : ready ? "info" : "err")}>
          <Icon name={released ? "checkCircle" : ready ? "package" : "lock"} size={18} style={{ flex: "0 0 auto", marginTop: 1, color: released ? "var(--tge-emerald)" : undefined }} />
          <div>
            <div className="bt">{released ? "Warmtefonds pakket klaar voor handmatige indiening" : ready ? "Pakket mag worden gegenereerd" : "Export geblokkeerd — " + open + " voorwaarden open"}</div>
            <div className="bd">{released ? "Download bevat route-memo DOCX/PDF, bronlijst, checks, documentindex, ALV-besluit, notulen-template, legal/approval report en beschikbare brondocumenten. Jeroen dient daarna handmatig in bij Warmtefonds." : ready ? "Alle menselijke checks zijn akkoord. Het systeem mag nu het ZIP-pakket, de DOCX/PDF outputs en indieningsmemo aanmaken." : "Het MVP dient niet automatisch in. Na vrijgave download je het pakket en dien je handmatig in. Gate G8."}</div>
            {packageInfo && <div className="tiny muted" style={{ marginTop: 8 }}>Laatste download: {packageInfo.fileName} · {packageInfo.docCount} brondocument(en) · {packageInfo.generatedDocumentCount || GENERATED_PACKAGE_DOCUMENTS.length} gegenereerde documenten · {packageInfo.templateVersion || WARMTEFONDS_TEMPLATE_VERSION}</div>}
          </div>
        </div>
        <button className="btn primary" style={{ alignSelf: "flex-start" }} disabled={!ready || generating} onClick={generatePackage}>
          {generating ? <span className="spin"></span> : <Icon name="download" size={15} />}{released ? "Download pakket opnieuw" : "Genereer Warmtefonds pakket"}
        </button>
      </div>
    </div>
  );
}

/* ── Afgerond ──────────────────────────────────────────────── */
function Afgerond() {
  return (
    <div className="card pad empty">
      <Icon name="checkCircle" size={40} className="ico" />
      <h3 style={{ font: "700 16px var(--font-body)", marginBottom: 6 }}>Dossier afgerond</h3>
      <p className="muted" style={{ margin: "0 auto", maxWidth: "48ch" }}>Het exportpakket is vrijgegeven en handmatig ingediend. Follow-up van vragen, aanvullende documenten en subsidievaststelling verloopt via de opvolg-checklist.</p>
    </div>
  );
}

Object.assign(window, { ReviewCenter, Scenario, AlvPack, ExportCenter, Afgerond });
