// ─────────────────────────────────────────────────────────────
// Section 6 - Document Upload
// Adviser-selectable required documents; client uploads files.
// ─────────────────────────────────────────────────────────────

// Default hard limit on uploads per document category. Keeps the file list
// readable for advisers and prevents accidental dumps (e.g. dragging an
// entire downloads folder). Applies to BOTH drag-drop and click-browse
// paths because they both funnel through onFilesPicked → onPick. A doc
// entry can override this with its own `maxFiles` (see maxFilesFor).
const MAX_FILES_PER_CATEGORY = 5;
// Resolve the per-category cap: a catalogue entry's `maxFiles` wins,
// otherwise the default. (e.g. CSV transaction summaries allow more, since
// clients often export one file per bank account.)
const maxFilesFor = doc => (doc && Number(doc.maxFiles) > 0) ? Number(doc.maxFiles) : MAX_FILES_PER_CATEGORY;
// Some requirements are PER PERSON (payslips: 3 each). A couple advised
// together works from ONE shared household checklist, so that slot has to
// hold both sets - and the submit gate has to ask for both. Rather than
// duplicate catalogue entries, an entry declares its couple variant via
// `coupleMinFiles` / `coupleMaxFiles` / `coupleLabel` / `coupleHint` and this
// resolver swaps them in. validation.jsx reads the same `coupleMinFiles` off
// the catalogue, so the gate and the UI cannot drift apart.
const docForCouple = (doc, sharedChecklist) => {
  if (!doc || !sharedChecklist) return doc;
  const out = { ...doc };
  if (Number(doc.coupleMinFiles) > 0) out.minFiles = Number(doc.coupleMinFiles);
  if (Number(doc.coupleMaxFiles) > 0) out.maxFiles = Number(doc.coupleMaxFiles);
  if (doc.coupleLabel) out.label = doc.coupleLabel;
  if (doc.coupleHint) out.hint = doc.coupleHint;
  return out;
};

// Master catalogue the adviser can pick from (mirrored in team portal)
const DOC_CATALOGUE = [
  { id: 'certified_id',       label: 'Certified Photo ID',                hint: 'Passport, OR a certified copy of BOTH the front and back of your driver\'s licence. Must be certified within the last 3 months.', accept: '.pdf,.jpg,.jpeg,.png', required: true,
    details: {
      title: 'How to get your ID certified',
      body: [
        'A "certified copy" is a photocopy or scan that has been verified as a true and accurate copy of the original by an authorised person. The certifier signs and dates each page and includes their name, qualification and contact details.',
        'In Australia, certifiers commonly include:',
        ['Justice of the Peace (JP) - free service offered at most public libraries, court houses and JP signing centres.',
         'Australia Post - most participating outlets offer document certification (~$10 per document, no booking required).',
         'Police officer (sworn).',
         'Pharmacist, medical practitioner, or registered nurse.',
         'Lawyer, accountant (CA/CPA), or notary public.',
         'School principal or teacher with 5+ years\' service.'],
        'Take your original photo ID plus a clear photocopy or scan to the certifier. They\'ll write or stamp on the copy: "I certify this is a true copy of the original document I have sighted", along with their signature, full name, qualification and the date.',
        'Here\'s an example of what a correctly certified driver licence looks like:',
        { infographic: 'certified_id' },
        'Tip: certification must be dated within the last 3 months.'
      ]
    }
  },
  { id: 'proof_of_address',   label: 'Proof of Address',                  hint: 'Utility bill, council rates or bank statement dated within 3 months.',    accept: '.pdf,.jpg,.jpeg,.png', required: true,
    details: {
      title: 'What counts as proof of address?',
      body: [
        'Any official document showing your full name and current residential address, dated within the last 3 months. Acceptable examples include:',
        ['Electricity, gas, water, internet or phone bill.',
         'Council rates notice.',
         'Bank or credit card statement.',
         'Australian Taxation Office (ATO) correspondence.',
         'Centrelink or Medicare letter.',
         'Lease or rental agreement (if recent and signed).'],
        'A driver\'s licence on its own does not count, because it doesn\'t show recency. PO Box addresses are also not accepted - we need a residential address.'
      ]
    }
  },
  { id: 'csv_transactions',   label: 'CSV Transaction Summary (12 mo)',   hint: 'Export from your banking app - all everyday accounts.',                   accept: '.csv,.xlsx,.pdf',       required: true, maxFiles: 15,
    details: {
      title: 'Why we ask for 12 months of transactions',
      body: [
        'Ideally we need 12 months of transactions from the bank account(s) where you do the majority of your day-to-day spending. This gives us a realistic picture of your true cashflow rather than a snapshot of a "good" or "bad" month.',
        'How to export from common Australian banks:',
        ['Commonwealth Bank (NetBank): Accounts → select account → Export → choose date range (12 months) → CSV.',
         'ANZ Internet Banking: Account list → select account → Export transactions → CSV.',
         'Westpac Online Banking: Statements & documents → Transaction search → set date range → Export to CSV.',
         'NAB Internet Banking: Accounts → Transaction history → Export → CSV.',
         'Most other banks: look for "Export", "Download" or "Statements" inside the account view.'],
        'If your spending is split across multiple accounts (e.g. a joint household account plus a personal one), please export and upload each separately. PDF statements are also fine if CSV isn\'t available.',
        'Privacy: this data is encrypted in transit and at rest, only viewable by your nominated adviser, and never sold or shared with third parties.'
      ]
    }
  },
  { id: 'third_party_auth',   label: 'Third Party Authority',             hint: 'Download the form below, print and sign it by hand, then scan or photograph it and upload the signed copy.',      accept: '.pdf,.jpg,.jpeg,.png', required: true,
    // downloadForm drives the "download → print + wet-sign → scan /
    // photo → upload" steps panel rendered above this row's dropzone.
    // The blank form is bundled as a served asset.
    // url stays null until a Third Party Authority approved by the licensee
    // (Strategic Finance Partners Pty Ltd, AFSL 559714) exists. Save the
    // approved PDF to project/assets/ and set url to enable the download;
    // the rest of the flow already works.
    downloadForm: { url: null, name: 'Tallowwood Wealth Advice - Third Party Authority.pdf' },
    details: {
      title: 'What is a Third Party Authority?',
      body: [
        'A Third Party Authority (TPA) is a signed letter from you giving us permission to contact your super fund, insurer, or other financial institution on your behalf. Without it, those organisations are legally prevented from sharing your account details with us under privacy law.',
        'We use the TPA to gather information you\'d otherwise have to chase yourself - current super balances, insurance cover within super, contribution history, fee structures, and so on. This saves you considerable time and means we can build your strategy with accurate, current data.',
        'How to complete it:',
        ['Download your adviser\'s Third Party Authority from the steps panel above. If no download is offered yet, your adviser will email it to you.',
         'Print it out and sign it by hand - a wet (ink) signature is required, a typed or digital signature can\'t be accepted.',
         'Scan the signed form, or take a clear photo of each page.',
         'Upload the signed copy in the upload box.'],
        'Tip: make sure the whole page is visible and the signature is legible before you upload.'
      ]
    }
  },
  // Per-person requirement: 3 payslips each. Couples advised together share
  // one checklist, so the couple* fields lift this slot to 6 files (3 each)
  // for both the cap and the submit gate - see docForCouple above.
  { id: 'payslips',           label: 'Most Recent 3 Payslips',            hint: 'Upload your 3 most recent payslips - or tick "I have uploaded the only relevant payslip information" if you have fewer.', accept: '.pdf,.jpg,.jpeg,.png', required: true, minFiles: 3, allowAllIHaveToggle: true,
    coupleLabel: 'Most Recent 3 Payslips (each)',
    coupleHint: 'Upload the 3 most recent payslips for each of you - 6 in total - or tick "I have uploaded the only relevant payslip information" if you have fewer.',
    coupleMinFiles: 6, coupleMaxFiles: 6,
    details: {
      title: 'Which payslips to provide',
      body: [
        'Please provide your three most recent payslips from your current employer. If you receive income from multiple employers or sources, please include the latest 3 payslips from each.',
        'If you are being advised as a couple, we need three payslips for each of you - six in total - uploaded to this same slot. Either of you can add them.',
        'Payslips are usually available through your company\'s payroll portal (e.g. MYOB, Xero Me, ADP, Employment Hero). If your employer emails them, search your inbox for "payslip" or "remittance".',
        'A combined PDF is fine, or you can upload them as separate files - whichever is easier.'
      ]
    }
  },
  { id: 'tax_return',         label: 'Last Tax Return / Notice of Assessment', hint: 'ATO myGov download is fine.',                                       accept: '.pdf',                  required: false },
  { id: 'super_statement',    label: 'Latest Super Statement',            hint: 'Most recent annual statement from your super fund.',                      accept: '.pdf,.jpg,.jpeg,.png', required: false,
    details: {
      title: 'How to find your super statement',
      body: [
        'Most super funds issue an annual statement at end of financial year (around July–September). You can usually download a copy by:',
        ['Logging into your super fund\'s member portal - look for "Statements", "Documents" or "Annual report".',
         'Checking your linked email inbox - most funds email a notification when statements are available.',
         'Logging into myGov → ATO online services → Super → this won\'t give you the statement itself but will list all your super accounts.'],
        'If you have multiple super funds, please upload the most recent statement from each.'
      ]
    }
  },
  { id: 'holding_statement',  label: 'Current Holding Statement',         hint: 'Most recent holding statement for your share portfolio (broker or share registry, e.g. CommSec, Computershare, Link).', accept: '.pdf,.jpg,.jpeg,.png,.csv,.xlsx', required: false },
  { id: 'insurance_schedule', label: 'Current Insurance Schedule(s)',     hint: 'Life / TPD / IP / trauma - policy schedules.',                            accept: '.pdf,.jpg,.jpeg,.png', required: false },
  { id: 'mortgage_statement', label: 'Mortgage / Loan Statement',         hint: 'Most recent statement for each loan account.',                            accept: '.pdf,.jpg,.jpeg,.png', required: false },
  { id: 'trust_deed',         label: 'Trust / SMSF Deed',                 hint: 'If you operate a family trust or self-managed super fund.',               accept: '.pdf',                  required: false },
  { id: 'will_poa',           label: 'Will & Power of Attorney',          hint: 'Optional, but helpful for estate planning.',                              accept: '.pdf',                  required: false },
  { id: 'other',              label: 'Other Supporting Document',         hint: 'Anything else your adviser has asked for.',                               accept: '*',                     required: false },
  // Optional catch-all that's ALWAYS appended after the adviser-required
  // list so the client can drop in anything else they think is relevant
  // (e.g. a previous SoA, a fund-switch confirmation, a Centrelink letter
  // they had in their drawer). Not part of the validation gate.
  // maxFiles 10 (not the default 5): clients routinely have more context to
  // share than the required list covers, and running out of room here means
  // they email the rest separately or leave it out altogether.
  { id: 'additional',         label: 'Additional documentation',          hint: 'Anything else you think is relevant - previous statements, super correspondence, letters from your fund, etc. Up to 10 files.', accept: '*',                     required: false, alwaysShown: true, maxFiles: 10,
    details: {
      title: 'Anything else worth sharing?',
      body: [
        'This slot is for anything you think could help your adviser - even if they didn\'t specifically ask for it.',
        'Useful examples include:',
        ['Previous Statements of Advice from another adviser.',
         'Letters or notices from your super fund, insurer or accountant.',
         'Recent investment correspondence (e.g. share buy-back offers, dividend statements).',
         'Centrelink, Veterans Affairs or Medicare correspondence.',
         'Anything else you think might be useful context.'],
        'You can upload up to 10 files in this slot. Drop in whatever feels most relevant - your adviser will review everything before your strategy meeting.'
      ]
    }
  },
];

// Default adviser-selected requirements (the 4 examples + payslips)
const DEFAULT_REQUIRED_DOCS = ['certified_id', 'proof_of_address', 'csv_transactions', 'third_party_auth', 'payslips'];

// tpaForm: the owning adviser's Third Party Authority ({url, name}) resolved by
// the server from clients.invited_by, or null when that adviser has no approved
// form yet. Overrides the static DOCUMENT_TYPES entry for the TPA row only.
function SectionDocuments({ data, set, saveCode, ensureSaveCode, tpaForm }) {
  const d = data.documents || {};
  // Requirements set by the adviser (persisted so the toggle sticks within the mock)
  const required = d.required || DEFAULT_REQUIRED_DOCS;
  const files = d.files || {};            // { [docId]: [ {id, name, size, uploadedAt, status} ] }

  // Couple detection for the "documents are shared" banner. When advice is
  // given as a couple the document set is a single shared household list -
  // both partners see and contribute to the same checklist - so we say so
  // explicitly rather than leaving the client unsure whether their partner
  // sees the same files.
  const primaryP = data?.personal?.primary || {};
  const partnerP = data?.personal?.partner || {};
  const isCouple = ['Married', 'De facto'].includes(primaryP.relStatus);
  const partnerActive = isCouple && primaryP.coupleAdvice === true;
  const partnerName = [partnerP.firstName, partnerP.lastName].filter(Boolean).join(' ').trim() || 'your partner';
  // Does this checklist cover BOTH people? When advice is given as a couple
  // the household uploads to one shared list, so per-person requirements
  // double (payslips: 3 each = 6). If the partner is completing their OWN
  // fact-find instead ('remind'), each person uploads their own set in their
  // own form and the requirement stays per-person.
  const sharedChecklist = partnerActive && data?.personal?.partnerCompletion !== 'remind';
  const docForHousehold = doc => docForCouple(doc, sharedChecklist);
  // Per-document error state. Keyed by docId so a duplicate in
  // "Certified ID" doesn't show up as a confusing banner under
  // "Proof of address". Each DocRow renders its own message inline.
  const [errorsByDoc, setErrorsByDoc] = useState({});
  const setDocError = (docId, msg) => setErrorsByDoc(prev => ({ ...prev, [docId]: msg }));
  const clearDocError = (docId) => setErrorsByDoc(prev => {
    if (!prev[docId]) return prev;
    const { [docId]: _drop, ...rest } = prev;
    return rest;
  });

  // Functional setState so multiple uploads in quick succession can't race.
  const update = (patch) => set(prev => {
    const prevDocs = prev.documents || {};
    return { ...prev, documents: { ...prevDocs, ...patch } };
  });

  // On mount/code-change, hydrate the local file list from the server so a
  // returning user sees their previously-uploaded files.
  useEffect(() => {
    if (!saveCode) return;
    let cancelled = false;
    window.TwApi.Documents.list(saveCode).then(r => {
      if (cancelled || !r?.files) return;
      // UNION merge via functional setState. The previous version used a
      // stale `data` closure snapshot AND overwrote each category wholesale
      // with the server list - so a file uploaded between mount and this
      // fetch resolving got clobbered (then reappeared on the next reload).
      // Now we merge into the LATEST state and union per category by file id,
      // keeping in-flight (pending) local files and any reflected partner
      // files the server table doesn't know about. Nothing is ever dropped.
      set(prev => {
        const pd = prev.documents || {};
        const pf = pd.files || {};
        const merged = { ...pf };
        Object.keys(r.files).forEach(k => {
          const serverArr = Array.isArray(r.files[k]) ? r.files[k] : [];
          const localArr = Array.isArray(pf[k]) ? pf[k] : [];
          const seen = new Map();
          const keyOf = f => (f && f.id != null) ? ('id:' + f.id)
            : (f && (f.tempId || f.name)) ? ('k:' + (f.tempId || f.name) + ':' + (f.size || '')) : null;
          // Server rows first (authoritative for persisted files)...
          serverArr.forEach(f => { const key = keyOf(f); if (key) seen.set(key, f); });
          // ...then any local rows the server doesn't have (in-flight uploads
          // or reflected partner files with no row in this client's table).
          localArr.forEach(f => { const key = keyOf(f); if (key && !seen.has(key)) seen.set(key, f); });
          merged[k] = [...seen.values()];
        });
        return { ...prev, documents: { ...pd, files: merged } };
      });
    }).catch(() => {});
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [saveCode]);

  async function onFilesPicked(docId, fileList) {
    clearDocError(docId);
    const existing = files[docId] || [];
    // Resolve through docForHousehold so a couple's payslip slot enforces the
    // 6-file cap rather than the per-person 3/default 5.
    const docDef = docForHousehold(DOC_CATALOGUE.find(x => x.id === docId));
    const docLabel = docDef?.label || 'this category';
    const cap = maxFilesFor(docDef);

    // Cap enforcement runs BEFORE we touch the network / save-code so we
    // never create an empty fact-find row for a rejected upload.
    const incoming = Array.from(fileList);
    const remaining = cap - existing.length;

    if (remaining <= 0) {
      setDocError(docId, `You've already uploaded ${cap} files for "${docLabel}". Remove one before adding another.`);
      return;
    }

    // Truncate over-cap selections rather than rejecting outright - clients
    // dragging a big folder get the first N in with a clear message about
    // the rest.
    let arr = incoming;
    if (incoming.length > remaining) {
      arr = incoming.slice(0, remaining);
      setDocError(docId, `Only the first ${remaining} of ${incoming.length} file(s) were added - "${docLabel}" is limited to ${cap} files.`);
    }

    let code;
    try { code = await ensureSaveCode(); }
    catch (err) {
      setDocError(docId, 'Could not start your fact find. Try again.');
      return;
    }
    // Optimistic placeholders so the UI updates immediately
    const placeholders = arr.map((f, i) => ({
      pending: true, tempId: `tmp-${Date.now()}-${i}`,
      name: f.name, size: f.size, uploadedAt: new Date().toISOString(), status: 'uploading',
    }));
    // Functional set (against the LATEST state, not the stale `data`/`files`
    // closure) so a concurrent upload or the mount-fetch merge can't clobber
    // these optimistic placeholders.
    set(prev => {
      const pd = prev.documents || {};
      const pf = pd.files || {};
      const cur = pf[docId] || [];
      return { ...prev, documents: { ...pd, files: { ...pf, [docId]: [...cur, ...placeholders] } } };
    });

    // Upload sequentially (clearer per-file errors than Promise.all)
    const finalised = [...existing];
    for (let i = 0; i < arr.length; i++) {
      const file = arr[i];
      const tempId = placeholders[i].tempId;
      try {
        const out = await window.TwApi.Documents.upload(code, docId, file);
        finalised.push({
          id: out.file.id, name: out.file.name, size: out.file.size,
          mimeType: out.file.mimeType, uploadedAt: out.file.uploadedAt, status: 'uploaded',
        });
      } catch (err) {
        // Server-side dedup returns 409 { error: 'duplicate', message }.
        // Surface a friendly inline warning IN the row where the user
        // tried to upload, so the message sits next to the dropzone
        // that just rejected the file. Other failures fall through to
        // a generic per-row message.
        if (err?.status === 409 && err?.body?.error === 'duplicate') {
          setDocError(docId, err.body.message
            || `"${file.name}" is already uploaded to this category. Remove the existing copy before re-uploading.`);
        } else {
          setDocError(docId, `Couldn't upload ${file.name}: ${err.message}`);
        }
      }
      // Strip the resolved placeholder regardless of success
      set(prev => {
        const pd = prev?.documents || {};
        const pf = pd.files || {};
        const cur = pf[docId] || [];
        const kept = cur.filter(x => x.tempId !== tempId);
        return { ...prev, documents: { ...pd, files: { ...pf, [docId]: kept } } };
      });
    }
    // Commit successful uploads to local state
    set(prev => {
      const pd = prev?.documents || {};
      const pf = pd.files || {};
      const cur = (pf[docId] || []).filter(x => !x.pending);
      return { ...prev, documents: { ...pd, files: { ...pf, [docId]: [...cur, ...finalised.filter(x => !existing.includes(x))] } } };
    });
  }

  async function removeFile(docId, idx) {
    const arr = [...(files[docId] || [])];
    const target = arr[idx];
    if (!target) return;
    // Local removal first for snappy UI
    arr.splice(idx, 1);
    update({ files: { ...files, [docId]: arr } });
    if (target.id && saveCode) {
      try { await window.TwApi.Documents.remove(saveCode, target.id); }
      catch (err) { /* the row is already gone from the user's view; just log */ console.warn('remove failed', err); }
    }
  }

  // Adviser-required docs first, then any catalogue entries flagged
  // `alwaysShown: true` (the optional "Additional documentation" slot).
  // Custom adviser-requested uploads arrive as 'custom:<title>' ids. They
  // aren't in the catalogue, so synthesize a required upload slot for each
  // (one file required, any file type, no opt-out).
  const customDocs = (required || [])
    .filter(id => typeof id === 'string' && id.startsWith('custom:'))
    .map(id => ({
      id,
      label: id.slice('custom:'.length).trim() || 'Custom document',
      hint: 'Requested by your adviser.',
      accept: '*', required: true, minFiles: 1,
    }));
  // Every row (and the submit summary below) works from the household-resolved
  // entry, so labels, hints, caps and min-file counts all reflect whether this
  // checklist covers one person or two.
  const visibleDocs = [
    ...DOC_CATALOGUE.filter(doc => required.includes(doc.id) && !doc.alwaysShown),
    ...customDocs,
    ...DOC_CATALOGUE.filter(doc => doc.alwaysShown),
  ].map(docForHousehold);
  const completed = visibleDocs.filter(doc => (files[doc.id] || []).length > 0).length;
  const pct = visibleDocs.length ? Math.round(completed / visibleDocs.length * 100) : 0;

  return (
    <div className="section-body">
      {/* Summary header - adviser-mode toggle removed: clients shouldn't be
          able to change their own required-document list. */}
      <div className="doc-summary">
        <div className="doc-summary-left">
          <div className="doc-summary-count">{completed}<span>/{visibleDocs.length}</span></div>
          <div>
            <div className="doc-summary-label">Documents received</div>
            <div className="doc-summary-bar">
              <div className="doc-summary-fill" style={{ width: `${pct}%` }} />
            </div>
          </div>
        </div>
      </div>

      {/* Client-facing upload list */}
      <div className="card">
        <div className="card-title">Your document checklist</div>
        {/* Shared-household notice for couples: makes it clear both partners
            work from the SAME list, so a client isn't left wondering whether
            their partner sees the same files or a separate set. */}
        {partnerActive && (
          <Callout kind="info" icon="👥">
            These documents are shared with {partnerName}. You're being advised
            as a couple, so you both see and add to the same household checklist -
            either of you can upload any document here, and there's no need to
            upload the same file twice.
          </Callout>
        )}
        {visibleDocs.length === 0 ? (
          <div className="empty-hint">
            No documents required yet - your adviser will add them here shortly.
          </div>
        ) : (
          <div className="doc-list">
            {visibleDocs.map(doc => (
              <React.Fragment key={doc.id}>
                <DocRow
                  doc={doc}
                  tpaForm={tpaForm}
                  files={files[doc.id] || []}
                  error={errorsByDoc[doc.id]}
                  allIHave={(d.allIHave || {})[doc.id] === true}
                  onToggleAllIHave={() => update({
                    allIHave: {
                      ...(d.allIHave || {}),
                      [doc.id]: !((d.allIHave || {})[doc.id]),
                    },
                  })}
                  onDismissError={() => clearDocError(doc.id)}
                  onPick={(fl) => onFilesPicked(doc.id, fl)}
                  onRemove={(idx) => removeFile(doc.id, idx)} />
                {/* Cashflow-accuracy gate, anchored directly under the
                    CSV Transaction Summary slot. The client must
                    confirm the data is the best available before the
                    documents section can be submitted; answering No
                    requires them to say what else they'll provide. */}
                {doc.id === 'csv_transactions' && (
                  <CsvAccuracyGate
                    value={d.csvBestData}
                    detail={d.csvBestDataDetail || ''}
                    onAnswer={(v) => update({
                      csvBestData: v,
                      // Picking Yes clears any stale "what's missing" note.
                      ...(v === true ? { csvBestDataDetail: '' } : {}),
                    })}
                    onDetail={(t) => update({ csvBestDataDetail: t })} />
                )}
              </React.Fragment>
            ))}
          </div>
        )}

        <Callout kind="info" icon="🔒">
          Files are encrypted end-to-end. Only your nominated adviser can view them.
          Max 25 MB per file. Accepted formats are shown on each upload slot.
        </Callout>
      </div>

      {/* Submit-time confirmation summary. This section is the last step
          before the "Submit fact-find" button, so we show the client exactly
          what will be sent - a plain checklist of every category with the
          file count (or a clear "not attached" flag for required items still
          missing). Answers the client feedback: "I wasn't sure everything I
          uploaded actually stayed attached when I submitted." */}
      {visibleDocs.length > 0 && (
        <DocSubmitSummary
          docs={visibleDocs}
          files={files}
          allIHave={d.allIHave || {}}
          partnerActive={partnerActive}
          partnerName={partnerName} />
      )}
    </div>
  );
}

// Plain "here's what you're about to send" list shown at the foot of the
// Documents section (the last step before Submit). Each category shows its
// file count; required categories with nothing attached are flagged so the
// client can fix them before submitting. Read-only - purely a confidence
// check, not another gate (the real gate is the validator).
function DocSubmitSummary({ docs, files, allIHave, partnerActive, partnerName }) {
  const rows = docs.map(doc => {
    const arr = files[doc.id] || [];
    const count = arr.length;
    // Mirror the validator: the opt-out toggle only counts with at least one
    // file behind it, and a slot with a min-file rule isn't satisfied until
    // that many are attached. Without this the summary ticked a couple's
    // payslips at 4 files while the submit gate (6) still blocked them.
    const needed = Math.max(1, Number(doc.minFiles) || 1);
    const optOut = doc.allowAllIHaveToggle && allIHave[doc.id] === true && count >= 1;
    const required = !doc.alwaysShown;
    const satisfied = count >= needed || optOut;
    return { id: doc.id, label: doc.label, count, needed, optOut, required, satisfied };
  });
  const totalFiles = rows.reduce((n, r) => n + r.count, 0);
  const missingRequired = rows.filter(r => r.required && !r.satisfied);

  return (
    <div className="card doc-submit-summary">
      <div className="card-title">Before you submit: documents attached</div>
      <div className="doc-submit-intro">
        {totalFiles > 0
          ? <>You've attached <strong>{totalFiles} file{totalFiles === 1 ? '' : 's'}</strong> across {rows.filter(r => r.count > 0).length} categor{rows.filter(r => r.count > 0).length === 1 ? 'y' : 'ies'}. Everything listed with a tick will be sent to your adviser when you submit.</>
          : <>Nothing is attached yet. Add your required documents above before submitting.</>}
        {partnerActive && <> These are shared with {partnerName}.</>}
      </div>
      <ul className="doc-submit-list">
        {rows.map(r => (
          <li key={r.id} className={cx('doc-submit-item', r.satisfied ? 'ok' : (r.required ? 'missing' : 'skip'))}>
            <span className="doc-submit-mark" aria-hidden="true">{r.satisfied ? '✓' : (r.required ? '!' : '–')}</span>
            <span className="doc-submit-label">{r.label}</span>
            <span className="doc-submit-note">
              {r.optOut
                ? 'Confirmed: only relevant file(s) uploaded'
                : r.count > 0
                  ? (r.count < r.needed
                      ? `${r.count} of ${r.needed} attached - ${r.needed - r.count} still needed`
                      : `${r.count} file${r.count === 1 ? '' : 's'} attached`)
                  : r.required
                    ? 'Not attached yet'
                    : 'Optional - not attached'}
            </span>
          </li>
        ))}
      </ul>
      {missingRequired.length > 0 && (
        <Callout kind="warn" icon="⚠️">
          {missingRequired.length} required document{missingRequired.length === 1 ? '' : 's'} still {missingRequired.length === 1 ? 'needs' : 'need'} attaching before you can submit: {missingRequired.map(r => r.label).join(', ')}.
        </Callout>
      )}
    </div>
  );
}

// Numbered "download → print + wet-sign → scan/photo → upload" steps,
// shown above the dropzone for any document that ships a downloadable
// form to be hand-signed (the Third Party Authority). The download is
// a native <a download> to the bundled blank form; the upload itself
// reuses the standard dropzone below.
function DocSignSteps({ form, hasFiles }) {
  const steps = [
    // When form.url is set the client downloads the blank form here. Until the
    // licensee-approved Tallowwood TPA exists, url is null and the step tells
    // the client their adviser will send it.
    form.url ? {
      title: 'Download the form',
      body: 'Save the Third Party Authority to your device.',
      action: (
        <a className="btn btn-primary btn-sm"
          href={form.url}
          download={form.name || ''}
          target="_blank" rel="noopener"
          style={{ marginTop: 8, display: 'inline-flex', alignItems: 'center', gap: 8, textDecoration: 'none' }}>
          <span aria-hidden="true">↓</span> Download Third Party Authority
        </a>
      ),
    } : {
      title: 'Get the form from your adviser',
      body: 'Your adviser will email you the Third Party Authority to sign. If you have not received it, contact them and they will send it straight through.',
    },
    {
      title: 'Print and sign it',
      body: 'Print the form and sign it by hand. A wet (ink) signature is required - a typed or digital signature can\'t be accepted.',
    },
    {
      title: 'Scan or photograph it',
      body: 'Scan the signed form, or take a clear, well-lit photo of each page so the whole page and your signature are legible.',
    },
    {
      title: hasFiles ? 'Uploaded below' : 'Upload the signed copy',
      body: hasFiles
        ? 'Your signed form is attached below. You can replace it if you need to.'
        : 'Add the signed form using the upload box below.',
    },
  ];
  return (
    <div className="doc-sign-steps" style={{
      margin: '4px 0 14px',
      padding: '16px 18px',
      background: '#F7F5EC',
      border: '1px solid #DDD8C6',
      borderRadius: 10,
    }}>
      <div style={{ fontSize: 12, fontWeight: 800, letterSpacing: '0.05em', textTransform: 'uppercase', color: '#1B3058', marginBottom: 12 }}>
        How to complete this form
      </div>
      <ol style={{ listStyle: 'none', margin: 0, padding: 0, display: 'grid', gap: 12 }}>
        {steps.map((s, i) => (
          <li key={i} style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
            <span aria-hidden="true" style={{
              flex: '0 0 auto', width: 24, height: 24, borderRadius: '50%',
              background: '#1B3058', color: '#fff', fontSize: 13, fontWeight: 800,
              display: 'flex', alignItems: 'center', justifyContent: 'center', marginTop: 1,
            }}>{i + 1}</span>
            <div style={{ flex: 1 }}>
              <div style={{ fontWeight: 700, fontSize: 14, color: 'var(--navy)' }}>{s.title}</div>
              <div style={{ fontSize: 13, color: '#4A4A46', lineHeight: 1.5, marginTop: 2 }}>{s.body}</div>
              {s.action || null}
            </div>
          </li>
        ))}
      </ol>
    </div>
  );
}

function DocRow({ doc, tpaForm, files, error, allIHave, onToggleAllIHave, onDismissError, onPick, onRemove }) {
  // The TPA is per adviser: the server-resolved form for this client's owning
  // adviser wins over the static (url: null) definition.
  const signForm = doc.id === 'third_party_auth' && tpaForm && tpaForm.url
    ? { ...doc.downloadForm, ...tpaForm }
    : doc.downloadForm;
  const inputRef = useRef(null);
  // Stable unique id per row, so the dropzone can be a real <label
  // htmlFor=…> connected to the file input. Using a native label is more
  // reliable than calling inputRef.current.click() in JS - some browsers
  // (and certain webview environments) silently no-op the JS click on
  // display:none inputs, but a label always opens the picker.
  // (Previously generated a unique id here for label htmlFor matching;
  // removed because the label now connects to its nested input by
  // implicit association - see the htmlFor comment below.)
  const [drag, setDrag] = useState(false);
  const [showDetails, setShowDetails] = useState(false);
  const hasFiles = files.length > 0;
  const uploadingCount = files.filter(f => f.pending).length;
  // Per-category cap (a doc can override the default via `maxFiles`). We
  // count placeholders + uploaded files alike so a burst of in-flight
  // uploads can't sneak past the limit.
  const cap = maxFilesFor(doc);
  const atLimit = files.length >= cap;

  // The hidden <input type="file"> is reused for both the empty-state
  // dropzone and the "add another file" button below the file list, so we
  // declare it once at the bottom of the row.
  return (
    <div className={cx('doc-row', hasFiles && 'complete', drag && 'drag')}
      data-ff-name={`doc.${doc.id}`}>
      <div className="doc-row-head">
        <div className="doc-row-status">
          {hasFiles ? <span className="doc-check">✓</span> : <span className="doc-pending">•</span>}
        </div>
        <div className="doc-row-main">
          <div className="doc-row-label">
            {doc.label}
            {/* Adviser-required docs get the red asterisk; the optional
                "Additional documentation" catch-all (alwaysShown=true)
                does NOT, because it's not part of the validation gate. */}
            {!doc.alwaysShown && <span className="req" aria-label="required" style={{ marginLeft: 6 }}>*</span>}
            {doc.details && (
              <button type="button" className="doc-info-btn"
                onClick={() => setShowDetails(true)}
                aria-label={`More information about ${doc.label}`}
                title="More info">?</button>
            )}
            {uploadingCount > 0 ? (
              <span className="doc-row-pill uploading">Uploading…</span>
            ) : hasFiles ? (
              <span className="doc-row-pill ok">Uploaded</span>
            ) : doc.alwaysShown ? (
              <span className="doc-row-pill">Optional</span>
            ) : (
              <span className="doc-row-pill">Required</span>
            )}
          </div>
          <div className="doc-row-hint">{doc.hint}</div>
          <div className="doc-row-accept">Accepts {doc.accept.replace(/,/g, ' · ')}</div>
        </div>
      </div>

      {/* Download → print + wet-sign → scan/photo → upload steps. Only
          rendered for docs that ship a downloadable form to sign (the
          Third Party Authority). Sits above the dropzone so the client
          completes the form before being asked to upload it. */}
      {signForm && (
        <DocSignSteps form={signForm} hasFiles={hasFiles} />
      )}

      {/* File list (if any) */}
      {hasFiles && (
        <div className="doc-files">
          <div className="doc-files-count">
            {files.length} file{files.length === 1 ? '' : 's'} attached
          </div>
          {files.map((f, i) => (
            <div key={f.id || f.tempId || i} className={cx('doc-file', f.pending && 'doc-file-pending')}>
              <div className="doc-file-icon">{f.pending ? <span className="doc-spinner" aria-hidden="true" /> : fileIcon(f.name)}</div>
              <div className="doc-file-meta">
                <div className="doc-file-name">{f.name}</div>
                <div className="doc-file-sub">
                  {f.pending
                    ? <em style={{ color: 'var(--accent)' }}>Uploading…</em>
                    : <>{formatBytes(f.size)} · uploaded {timeAgo(f.uploadedAt)}</>}
                </div>
              </div>
              {!f.pending && (
                <button type="button" className="doc-file-remove" onClick={() => onRemove(i)}>Remove</button>
              )}
            </div>
          ))}
        </div>
      )}

      {/* "I have uploaded the only relevant payslip information" style
          toggle for documents where the client may not have the full
          requested set (e.g. fewer than 3 payslips because they're
          casual or recently started). Validator treats this toggle as
          satisfying the minFiles requirement. */}
      {doc.allowAllIHaveToggle && (
        <label className="invite-toggle" style={{ margin: '8px 0' }}>
          <input type="checkbox" checked={allIHave === true}
            onChange={onToggleAllIHave} />
          <span>I have uploaded the only relevant payslip information.</span>
          <span className="invite-toggle-hint">Tick this only if you cannot provide {doc.minFiles || 3} files - e.g. you are casual, recently started, or paid irregularly.</span>
        </label>
      )}

      {/* Per-row upload error - sits directly above the dropzone so the
          message is right next to the action that failed (e.g.
          "Already uploaded", "File too large"). Dismissible so it
          doesn't linger after the user takes corrective action. */}
      {error && (
        <div role="alert"
          style={{
            display: 'flex', alignItems: 'flex-start', gap: 10,
            background: '#FDECEF', color: '#A6412F',
            padding: '10px 14px', borderRadius: 8,
            margin: '8px 0', fontSize: 13,
            border: '1px solid #F5C2CE',
          }}>
          <span aria-hidden="true" style={{ flexShrink: 0 }}>⚠️</span>
          <span style={{ flex: 1 }}>{error}</span>
          {onDismissError && (
            <button type="button"
              onClick={onDismissError}
              aria-label="Dismiss"
              style={{
                background: 'transparent', border: 0, padding: 0,
                color: '#A6412F', cursor: 'pointer', fontSize: 16, lineHeight: 1,
              }}>×</button>
          )}
        </div>
      )}

      {/* Dropzone - ALWAYS visible (compact when files already exist) so
          the user can drop additional files at any time.

          Implementation: the <input type="file"> sits as an OVERLAY on
          top of the dropzone area with `opacity:0` and `inset:0`. Every
          user click in the dropzone hits the input directly - no label
          association, no JS .click() forwarding, no nested-input
          activation. This is the only pattern that has consistently
          worked across Chrome / Safari / iOS Safari / Firefox in our
          testing. Earlier attempts (display:none + JS click, htmlFor +
          nested input, clip:rect off-screen input) each silently failed
          on at least one browser. Drag-and-drop ALSO lands on the input
          natively because <input type="file"> accepts drops by default. */}
      <div
        className={cx('doc-dropzone', drag && 'drag', hasFiles && 'doc-dropzone-compact', atLimit && 'doc-dropzone-disabled')}
        style={{
          position: 'relative',
          cursor: atLimit ? 'not-allowed' : 'pointer',
          opacity: atLimit ? 0.55 : 1,
        }}
        // Drops are handled natively by the overlay <input type="file">
        // below (it accepts file drops by default). We DON'T attach our
        // own onDrop here, otherwise drops would fire twice. The cap is
        // enforced defensively in onFilesPicked, so even if the input
        // somehow receives a drop while atLimit, the upload is blocked.
        onDragOver={(e) => { if (atLimit) return; e.preventDefault(); setDrag(true); }}
        onDragLeave={() => setDrag(false)}
      >
        <div className="doc-dropzone-icon">{atLimit ? '✓' : hasFiles ? '+' : '↑'}</div>
        <div className="doc-dropzone-main">
          {atLimit ? (
            <><strong>Limit reached</strong> · {files.length} of {cap} files uploaded</>
          ) : hasFiles ? (
            <><strong>Drop another file</strong> or <span className="link">browse</span> · {files.length} of {cap} uploaded</>
          ) : (
            <><strong>Drop files here</strong> or <span className="link">browse</span> · up to {cap} files</>
          )}
        </div>
        {!atLimit && (
          <input
            ref={inputRef}
            type="file"
            accept={doc.accept}
            multiple
            aria-label={`Upload ${doc.label}`}
            style={{
              position: 'absolute', inset: 0,
              width: '100%', height: '100%',
              opacity: 0, cursor: 'pointer',
              // Don't catch the dropzone's mouseup/keyboard accidentally.
              // (the file input itself handles all the activation we need)
              border: 0, padding: 0,
            }}
            onChange={(e) => {
              if (e.target.files?.length) onPick(e.target.files);
              e.target.value = '';
              setDrag(false);
            }}
          />
        )}
      </div>

      {showDetails && doc.details && (
        <DocDetailsModal doc={doc} onClose={() => setShowDetails(false)} />
      )}
    </div>
  );
}

// Confirmation gate for the CSV Transaction Summary. The client must
// declare whether the uploaded transaction data is the best available
// picture of their cashflow. "Yes" unblocks submission; "No" opens a
// mandatory popup capturing what additional information they still
// need to provide (required by validation before submit).
function CsvAccuracyGate({ value, detail, onAnswer, onDetail }) {
  const [showModal, setShowModal] = useState(false);
  // Local draft so typing in the modal doesn't autosave half-sentences;
  // committed on Save.
  const [draft, setDraft] = useState(detail || '');

  function pick(v) {
    onAnswer(v);
    if (v === false) {
      setDraft(detail || '');
      setShowModal(true);
    }
  }

  function saveDetail() {
    const t = draft.trim();
    if (!t) return;
    onDetail(t);
    setShowModal(false);
  }

  function cancelModal() {
    // The popup is mandatory for a "No" answer - backing out without a
    // note reverts the question to unanswered so the validation gate
    // stays consistent (they either explain or change their answer).
    if (!(detail || '').trim()) onAnswer(null);
    setShowModal(false);
  }

  return (
    <div className="doc-row" data-ff-name="doc.csvBestData"
      style={{ background: '#F7F5EC', border: '1px solid #DDD8C6', borderRadius: 8, padding: '14px 16px' }}>
      <Field
        label="Does the provided document reflect the best available data to form an accurate cashflow position?"
        required name="doc.csvBestData"
        hint="For example, if some of your day-to-day spending happens on an account you haven't exported, answer No.">
        <YesNo value={value} onChange={pick} />
      </Field>

      {value === false && (
        <div data-ff-name="doc.csvBestDataDetail"
          style={{ marginTop: 10, padding: '10px 12px', background: 'var(--white, #fff)', border: '1px solid #DDD8C6', borderRadius: 6 }}>
          <div style={{ fontSize: 10, fontWeight: 800, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--mute)', marginBottom: 4 }}>
            Additional information you'll provide
          </div>
          {(detail || '').trim() ? (
            <div style={{ fontSize: 13, color: 'var(--navy)', display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'baseline' }}>
              <span>{detail}</span>
              <button type="button" className="add-btn" style={{ fontSize: 12, flex: '0 0 auto' }}
                onClick={() => { setDraft(detail); setShowModal(true); }}>Edit</button>
            </div>
          ) : (
            <button type="button" className="btn btn-primary btn-sm"
              onClick={() => { setDraft(''); setShowModal(true); }}>
              Tell us what's missing
            </button>
          )}
        </div>
      )}

      {showModal && (
        <div className="ff-modal-backdrop" onClick={cancelModal}>
          <div className="ff-modal" style={{ maxWidth: 520 }} onClick={(e) => e.stopPropagation()}>
            <div className="modal-title">What additional information do you need to provide?</div>
            <p style={{ margin: '0 0 14px', color: '#4A4A46', fontSize: 14, lineHeight: 1.55 }}>
              You've told us the uploaded transaction data doesn't fully reflect your cashflow position. Let your adviser know what's missing and how you plan to provide it - e.g. another account's export, a credit card statement, or cash spending you'll estimate separately.
            </p>
            <Textarea rows={4} value={draft}
              onChange={(e) => setDraft(e.target.value)}
              placeholder="e.g. My joint account with ING covers most groceries - I'll export and upload it this week." />
            <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 16 }}>
              <button type="button" className="btn btn-ghost" onClick={cancelModal}>Cancel</button>
              <button type="button" className="btn btn-primary" onClick={saveDetail}
                disabled={!draft.trim()}>Save</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// Inline SVG infographic showing a correctly certified driver licence:
// the licence card on the left, the certifier's statement + signature +
// date on the right. Drawn as SVG (rather than a bundled image) so it
// stays crisp at any size, prints cleanly, and needs no binary asset in
// the deploy pipeline.
function CertifiedIdExample() {
  const ink = '#202037';
  const mute = '#6E6A5F';
  const purple = '#6E4EA3';
  return (
    <div style={{ margin: '16px 0' }}>
      <svg viewBox="0 0 700 340" xmlns="http://www.w3.org/2000/svg"
        role="img" aria-label="Example of a driver licence certified and signed by a medical practitioner"
        style={{ width: '100%', height: 'auto', display: 'block' }}>
        {/* Backdrop */}
        <rect x="0" y="0" width="700" height="320" rx="16" fill="#E9E6F4" />

        {/* ── Licence card ── */}
        <rect x="22" y="42" width="396" height="240" rx="14" fill="#FFFFFF" stroke="#D8D3E8" />
        {/* Purple header (rounded top corners only) */}
        <path d="M22 56 a14 14 0 0 1 14 -14 h368 a14 14 0 0 1 14 14 v32 h-396 z" fill={purple} />
        <text x="220" y="73" textAnchor="middle" fill="#FFFFFF" fontSize="24" fontWeight="800" letterSpacing="2">DRIVER LICENCE</text>

        {/* Faint watermark rings */}
        <g stroke="#EFECF7" fill="none" strokeWidth="2">
          <circle cx="220" cy="190" r="70" />
          <circle cx="220" cy="190" r="95" />
          <circle cx="220" cy="190" r="120" />
        </g>

        {/* Holder details */}
        <text x="42" y="122" fill={ink} fontSize="14" fontWeight="800">Frankie Example</text>
        <text x="42" y="152" fill={ink} fontSize="11">123 Sample Street,</text>
        <text x="42" y="166" fill={ink} fontSize="11">MELBOURNE, VIC, 3000</text>
        <text x="42" y="196" fill={mute} fontSize="10">Licence number:</text>
        <text x="42" y="212" fill={ink} fontSize="13" fontWeight="700">12345678</text>

        {/* Photo placeholder */}
        <rect x="290" y="104" width="104" height="120" rx="6" fill="#DDD8C6" />
        <circle cx="342" cy="146" r="20" fill="#C9C5BA" />
        <path d="M306 224 q36 -44 72 0 z" fill="#C9C5BA" />

        {/* Holder signature squiggle */}
        <path d="M44 244 q8 -18 14 0 q4 12 10 -4 q5 -14 11 2 q4 10 12 -2"
          stroke={purple} strokeWidth="2" fill="none" strokeLinecap="round" />

        {/* Barcode */}
        <g fill={ink}>
          {[150,154,160,166,170,176,182,186,192,198,202,208,214,218,224,230,234,240,246,250,256,262,266,272,278,282,288,294,298,304,310,314,320,326,330,336,342,346,352,358,362,368,374,378].map((x, i) => (
            <rect key={i} x={x} y="236" width={i % 3 === 0 ? 3 : 1.5} height="18" />
          ))}
        </g>

        {/* Card footer: DOB + expiry */}
        <line x1="140" y1="258" x2="400" y2="258" stroke="#D8D3E8" />
        <text x="200" y="270" textAnchor="middle" fill={mute} fontSize="8" fontWeight="700">Date of Birth</text>
        <text x="200" y="278" textAnchor="middle" fill={ink} fontSize="10" fontWeight="800">01/01/1987</text>
        <text x="330" y="270" textAnchor="middle" fill={mute} fontSize="8" fontWeight="700">Expiry Date</text>
        <text x="330" y="278" textAnchor="middle" fill={ink} fontSize="10" fontWeight="800">23 Feb 2024</text>

        {/* ── Certification block (right) ── */}
        <text x="446" y="78" fill={ink} fontSize="16" fontStyle="italic">
          <tspan x="446" dy="0">I, <tspan fontWeight="700">Sam Sample</tspan>, as a <tspan fontStyle="italic">Medical</tspan></tspan>
          <tspan x="446" dy="22"><tspan fontStyle="italic">Practitioner</tspan>, certify that this</tspan>
          <tspan x="446" dy="22"><tspan fontStyle="italic">Driver Licence</tspan>, is a true copy</tspan>
          <tspan x="446" dy="22">of the original.</tspan>
        </text>

        {/* Certifier signature */}
        <path d="M452 196 q14 -26 26 -6 q6 10 -8 14 q-12 4 4 6 q22 2 36 -10 q8 -8 4 4 q-2 8 10 2"
          stroke={ink} strokeWidth="2.4" fill="none" strokeLinecap="round" />
        <line x1="448" y1="214" x2="540" y2="214" stroke={ink} strokeWidth="1" opacity="0.25" />

        {/* Certification date */}
        <text x="446" y="248" fill={ink} fontSize="16" fontWeight="800">1 February 2023</text>

        {/* Caption */}
        <text x="22" y="336" fill={mute} fontSize="12" fontWeight="600">Driver Licence certified and signed by Medical Practitioner.</text>
      </svg>
    </div>
  );
}

function DocDetailsModal({ doc, onClose }) {
  // Lock body scroll while open
  useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => {
      document.body.style.overflow = prev;
      document.removeEventListener('keydown', onKey);
    };
  }, [onClose]);

  function renderBlock(block, key) {
    // Object blocks carry a special marker - currently only the
    // certified-ID example infographic.
    if (block && typeof block === 'object' && !Array.isArray(block)) {
      if (block.infographic === 'certified_id') return <CertifiedIdExample key={key} />;
      return null;
    }
    if (Array.isArray(block)) {
      return (
        <ul key={key} className="doc-details-list">
          {block.map((item, i) => <li key={i}>{item}</li>)}
        </ul>
      );
    }
    return <p key={key}>{block}</p>;
  }

  return (
    <div className="doc-details-backdrop" onClick={onClose}>
      <div className="doc-details-modal" onClick={(e) => e.stopPropagation()}
        role="dialog" aria-modal="true" aria-labelledby="docDetailsTitle">
        <div className="doc-details-head">
          <div>
            <div className="eyebrow">Help</div>
            <h3 id="docDetailsTitle">{doc.details.title}</h3>
          </div>
          <button type="button" className="doc-details-close"
            onClick={onClose} aria-label="Close">×</button>
        </div>
        <div className="doc-details-body">
          {doc.details.body.map((b, i) => renderBlock(b, i))}
        </div>
        <div className="doc-details-foot">
          <button type="button" className="btn btn-primary" onClick={onClose}>Got it</button>
        </div>
      </div>
    </div>
  );
}

function fileIcon(name) {
  const ext = (name.split('.').pop() || '').toLowerCase();
  if (['pdf'].includes(ext)) return 'PDF';
  if (['csv','xlsx','xls'].includes(ext)) return 'CSV';
  if (['jpg','jpeg','png','gif','heic'].includes(ext)) return 'IMG';
  if (['doc','docx'].includes(ext)) return 'DOC';
  return 'FILE';
}
function formatBytes(b) {
  if (!b) return '-';
  if (b < 1024) return b + ' B';
  if (b < 1024*1024) return (b/1024).toFixed(1) + ' KB';
  return (b/(1024*1024)).toFixed(1) + ' MB';
}
function timeAgo(iso) {
  const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
  if (mins < 1) return 'just now';
  if (mins < 60) return `${mins}m ago`;
  const hrs = Math.floor(mins/60);
  if (hrs < 24) return `${hrs}h ago`;
  return `${Math.floor(hrs/24)}d ago`;
}

Object.assign(window, { SectionDocuments, DOC_CATALOGUE, DEFAULT_REQUIRED_DOCS });
