/* global React, BrainMark, Wordmark, I */
const { useState: useSH, useRef: useSHRef, useEffect: useSHE, useLayoutEffect: useSHL } = React;

/* ---------------- 7-stage application model ---------------- */
const STAGES = [
  // Borrower-first order (exec decision 2026-08-06).
  { id: 'borrower',   label: () => 'Borrower Info' },
  { id: 'loan',       label: lt => lt === 'refinance' ? 'Refinance Info' : 'Purchase Info' },
  { id: 'employment', label: () => 'Employment & Income' },
  { id: 'assets',     label: () => 'Assets' },
  { id: 'reo',        label: () => 'Real Estate Owned' },
  { id: 'credit',     label: () => 'Credit & Liabilities' },
  { id: 'questions',  label: () => 'Background Questions' },
  { id: 'review',     label: () => 'Review & Submit' },
];

/* ---------------- left progress rail ---------------- */

/**
 * Compact "Documents" entry for the rail foot. Purely an observer: it reads
 * the saved document count and watches the upload lifecycle events the
 * existing uploaders already dispatch, so it stays accurate no matter which
 * surface (panel, inline bar, page drop) did the uploading. Clicking it opens
 * the full DocUploadPanel; it owns no upload logic of its own.
 */
function RailDocsChip({ onClick }) {
  const [docCount, setDocCount] = useSH(0);
  const [activeCount, setActiveCount] = useSH(0);

  useSHE(function() {
    function refresh() {
      try {
        var s = JSON.parse(localStorage.getItem('bevri_documents_v1'));
        setDocCount(Array.isArray(s && s.documents) ? s.documents.length : 0);
      } catch {}
    }
    refresh();
    function onStart() { setActiveCount(function(n) { return n + 1; }); }
    function onEnd() { setActiveCount(function(n) { return Math.max(0, n - 1); }); refresh(); }
    window.addEventListener('bevri:doc-uploading', onStart);
    window.addEventListener('bevri:doc-processed', onEnd);
    window.addEventListener('bevri:doc-error', onEnd);
    return function() {
      window.removeEventListener('bevri:doc-uploading', onStart);
      window.removeEventListener('bevri:doc-processed', onEnd);
      window.removeEventListener('bevri:doc-error', onEnd);
    };
  }, []);

  const busy = activeCount > 0;
  const sub = busy
    ? (activeCount === 1 ? 'Processing 1 document…' : 'Processing ' + activeCount + ' documents…')
    : (docCount > 0 ? docCount + ' document' + (docCount === 1 ? '' : 's') + ' on file' : 'Auto-fills your application');

  return (
    <button className="rail-docs" onClick={onClick} title="Upload documents">
      <span className="rail-docs-ic">
        {busy
          ? <span className="rail-docs-spin" />
          : <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="17 8 12 3 7 8" /><line x1="12" y1="3" x2="12" y2="15" /></svg>}
      </span>
      <span className="rail-docs-txt">
        <b>Documents</b>
        <span>{sub}</span>
      </span>
      {docCount > 0 && !busy && <span className="rail-docs-count">{docCount}</span>}
    </button>
  );
}

function ProgressRail({ loanType, currentStage, stageFill, onStageClick, onHome, onOpenChat, onOpenUpload, completedStages = [] }) {
  const curIdx = STAGES.findIndex(s => s.id === currentStage);
  const completedSet = new Set(completedStages);
  // LO white-labeling: the rail leads with the loan officer's identity when
  // the loan is attributed; unattributed sessions keep the Bevri wordmark.
  const [railLo, setRailLo] = useSH(window.__bevriLoBranding || null);
  useSHE(function() {
    let alive = true;
    fetchPosLoBranding().then(function(lo) { if (alive && lo) setRailLo(lo); });
    return function() { alive = false; };
  }, []);

  return (
    <>
      <div className="rail-head">
        {/* Top-left is the LO's space only (Court branding feedback): logo,
            else headshot, else initials. No Bevri mark here; attribution
            lives in the rail-foot "Powered by bevri.ai". */}
        {railLo && railLo.displayName ? (
          <a className="rail-lo step-anim" href={railLo.phone ? 'tel:' + String(railLo.phone).replace(/[^\d+]/g, '') : undefined}
            title={railLo.phone ? 'Call ' + railLo.displayName + ' at ' + railLo.phone : railLo.displayName}>
            <LoAvatar lo={railLo} size={36} preferLogo />
            <span className="rail-lo-txt">
              <b>{railLo.displayName}</b>
              <span>{[railLo.nmls ? 'NMLS ' + railLo.nmls : null, railLo.companyName].filter(Boolean).join(' \u00b7 ')}</span>
            </span>
          </a>
        ) : null}
      </div>
      <div className="rail-scroll">
        <div className="rail-eyebrow mono">
          YOUR APPLICATION
          {curIdx > 0 && (
            <span style={{ marginLeft: 8, color: 'var(--sage)', fontWeight: 700 }}>
              {curIdx}/{STAGES.length}
            </span>
          )}
        </div>
        {STAGES.map((s, i) => {
          const wasCompleted = completedSet.has(s.id);
          const state = wasCompleted && s.id !== currentStage ? 'done'
            : wasCompleted && s.id === currentStage ? 'editing'
            : i === curIdx ? 'active'
            : 'upcoming';
          // Every section is freely navigable, filled or not: the borrower can
          // jump ahead, and the operator chat can fill any section from
          // anywhere. Only the section they are currently on is inert.
          const clickable = state !== 'active' && state !== 'editing';
          return (
            <button key={s.id}
              className={'stage ' + state + (clickable ? ' clickable' : '')}
              onClick={clickable ? () => onStageClick(s.id) : undefined}>
              <span className="stage-marker-wrap">
                <span className="stage-marker">{state === 'done' ? I.check(13) : i + 1}</span>
                <span className="stage-line" />
              </span>
              <span className="stage-body">
                <span className="stage-label">{s.label(loanType)}</span>
                {state === 'done'    && <span className="stage-status">Completed</span>}
                {state === 'editing' && <span className="stage-status">Editing</span>}
                {state === 'active'  && <>
                  <span className="stage-status">In progress{stageFill > 0 ? ` · ${stageFill}%` : ''}</span>
                  <span className="stage-mini"><i style={{ width: Math.max(6, stageFill || 0) + '%' }} /></span>
                </>}
              </span>
            </button>
          );
        })}
      </div>
      <div className="rail-foot">
        <RailDocsChip onClick={onOpenUpload} />
        <button className="rail-help" onClick={onOpenChat}>
          <span className="ava"><BrainMark size={22} /></span>
          <span className="rail-help-txt">
            <b>Ask Bevri Operator</b>
            <span>Questions? I'm here to help.</span>
          </span>
        </button>
        <button className="powered-by" onClick={onHome} title="Powered by Bevri">
          Powered by <b>bevri.ai</b>
        </button>
      </div>
    </>
  );
}

/* ---------------- context-aware assistant fallback replies ---------------- */
function currentLoanId() {
  try { return new URLSearchParams(window.location.search).get('loanId') || ''; } catch { return ''; }
}
function readPosJson(key) {
  try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch { return null; }
}
function writePosJson(key, value) {
  try { localStorage.setItem(key, JSON.stringify(value)); } catch {}
}
function appendUploadedDocuments(docs) {
  const nextDocs = Array.isArray(docs) ? docs.filter(Boolean) : [];
  if (!nextDocs.length) return;
  const saved = readPosJson('bevri_documents_v1');
  const existing = Array.isArray(saved?.documents) ? saved.documents : [];
  const byId = new Map(existing.map(doc => [doc.id || doc.name, doc]));
  nextDocs.forEach(doc => byId.set(doc.id || doc.name, { ...doc, uploadedAt: doc.uploadedAt || new Date().toISOString() }));
  writePosJson('bevri_documents_v1', { documents: Array.from(byId.values()) });
}
function getNestedPath(obj, path) {
  return path.split('.').reduce((o, k) => (o && typeof o === 'object' ? o[k] : undefined), obj);
}
function setNestedPath(obj, path, value) {
  const keys = path.split('.');
  let cur = obj;
  for (let i = 0; i < keys.length - 1; i++) {
    if (!cur[keys[i]] || typeof cur[keys[i]] !== 'object') cur[keys[i]] = {};
    cur = cur[keys[i]];
  }
  cur[keys[keys.length - 1]] = value;
  return obj;
}
// Array-aware setter for operator record writes: a numeric path segment
// creates/indexes an ARRAY, so a chat write into an empty section produces the
// same shape the form flows save ({incomes:{primary:[record]}}), never
// {primary:{0:record}} which the flows cannot render. Only the operator path
// uses this; IDP auto-fill keeps the original setNestedPath behavior.
function setNestedPathForRecords(obj, path, value) {
  const keys = path.split('.');
  let cur = obj;
  for (let i = 0; i < keys.length - 1; i++) {
    const key = keys[i];
    if (!cur[key] || typeof cur[key] !== 'object') cur[key] = /^\d+$/.test(keys[i + 1]) ? [] : {};
    cur = cur[key];
  }
  cur[keys[keys.length - 1]] = value;
  return obj;
}
// Applies the operator's proposed field writes to the borrower's local draft.
// These are borrower-stated facts from the conversation, so they overwrite (the
// borrower just told us), then flows re-read via the same event as auto-fill.
function applyOperatorFieldOps(fieldOps) {
  if (!Array.isArray(fieldOps) || !fieldOps.length) return 0;
  const byKey = {};
  fieldOps.forEach(op => {
    if (!op || !op.posKey || !op.posPath) return;
    if (!byKey[op.posKey]) byKey[op.posKey] = readPosJson(op.posKey) || {};
    setNestedPathForRecords(byKey[op.posKey], op.posPath, op.value);
  });
  const keys = Object.keys(byKey);
  keys.forEach(k => writePosJson(k, byKey[k]));
  if (keys.length) window.dispatchEvent(new CustomEvent('bevri:fields-autofilled', { detail: { posKeys: keys } }));
  return keys.length ? fieldOps.length : 0;
}
window.applyOperatorFieldOps = applyOperatorFieldOps;

// Remember the IDP classification on the draft's document record, so the
// needs engine and scorecard can match documents by their real type instead
// of guessing from filenames. Additive: only fills in what the server sent.
function rememberDocClassification(documentId, category, subtype, review) {
  if (!documentId || (!category && !subtype && !review)) return;
  var saved = readPosJson('bevri_documents_v1');
  var docs = saved && Array.isArray(saved.documents) ? saved.documents : [];
  var changed = false;
  for (var i = 0; i < docs.length; i++) {
    if (docs[i] && docs[i].id === documentId) {
      if (category && docs[i].category !== category) { docs[i].category = category; changed = true; }
      if (subtype && docs[i].subtype !== subtype) { docs[i].subtype = subtype; changed = true; }
      // Store the deterministic review verdict so the panel, the chat, and
      // the operator's digest all read the same opinion of this document.
      if (review && review.verdict) { docs[i].review = review; changed = true; }
      break;
    }
  }
  if (changed) writePosJson('bevri_documents_v1', { documents: docs });
}

// Polls IDP extraction and auto-fills matching localStorage keys (empty fields only).
// Delays: 2 → 4 → 8 → 15 → 25 s. Fires 'bevri:fields-autofilled' on any write.
async function pollAndAutoFill(documentId, loanId) {
  const DELAYS = [2000, 4000, 8000, 15000, 25000];
  for (let di = 0; di < DELAYS.length; di++) {
    await new Promise(function(r) { setTimeout(r, DELAYS[di]); });
    var shouldStop = false;
    var result = null;
    try {
      var res = await fetch('/api/pos/intake/documents/' + encodeURIComponent(documentId) + '/fields?loanId=' + encodeURIComponent(loanId), { credentials: 'same-origin' });
      if (!res.ok) {
        shouldStop = true;
      } else {
        var data = await res.json().catch(function() { return null; });
        if (data) rememberDocClassification(documentId, data.documentType, data.documentSubtype, data.review);
        if (!data || data.status === 'error') {
          shouldStop = true;
        } else if (data.status !== 'pending' && data.status !== 'processing') {
          var filled = [];
          var touched = new Set();
          var fields = (data.fields || []).filter(function(f) { return f.autoApply; });
          for (var fi = 0; fi < fields.length; fi++) {
            var field = fields[fi];
            var stored = readPosJson(field.posKey) || {};
            var current = getNestedPath(stored, field.posPath);
            if (current !== undefined && current !== null && current !== '') { continue; }
            setNestedPath(stored, field.posPath, field.value);
            writePosJson(field.posKey, stored);
            touched.add(field.posKey);
            filled.push(field);
          }
          if (touched.size > 0) {
            window.dispatchEvent(new CustomEvent('bevri:fields-autofilled', { detail: { documentId: documentId, posKeys: Array.from(touched) } }));
            showToast(filled.length + ' field' + (filled.length === 1 ? '' : 's') + ' filled from your document', 'ok');
          }
          result = { status: 'ready', filled: filled, review: data.review || null, documentSubtype: data.documentSubtype || null };
          shouldStop = true;
        }
      }
    } catch (e) { shouldStop = true; }
    if (result) { return result; }
    if (shouldStop) { break; }
  }
  return { status: 'error', filled: [] };
}
window.pollAndAutoFill = pollAndAutoFill;

// Dispatch a toast via the bevri:toast custom event.
function showToast(message, kind = '') {
  window.dispatchEvent(new CustomEvent('bevri:toast', { detail: { message, kind } }));
}
window.showToast = showToast;
// Fetch and cache the loan tracker status ("where is my loan?"). Cached on
// window so the tracker view renders instantly on revisit and the operator's
// application context can include the current stage without an extra fetch.
function fetchPosLoanStatus() {
  var loanId = window.BevriPosStorage?.loanId || currentLoanId();
  if (!loanId) return Promise.resolve(null);
  return fetch('/api/pos/intake/status?loanId=' + encodeURIComponent(loanId), { credentials: 'same-origin' })
    .then(function(r) { return r.ok ? r.json() : null; })
    .then(function(d) {
      if (d && d.success) window.__bevriLoanStatus = d;
      return d;
    })
    .catch(function() { return null; });
}
window.fetchPosLoanStatus = fetchPosLoanStatus;

// Fetch and cache the owning LO's public card from the link's ?lo= slug.
// Renders the "you're applying with" chip and rides the operator context so
// the chat can answer "who is my loan officer?".
function fetchPosLoCard() {
  if (window.__bevriPosLo !== undefined) return Promise.resolve(window.__bevriPosLo);
  var slug = '';
  try { slug = (new URLSearchParams(window.location.search).get('lo') || '').toLowerCase(); } catch {}
  if (!slug) { window.__bevriPosLo = null; return Promise.resolve(null); }
  return fetch('/api/pos/intake/lo?slug=' + encodeURIComponent(slug), { credentials: 'same-origin' })
    .then(function(r) { return r.ok ? r.json() : null; })
    .then(function(d) {
      window.__bevriPosLo = d && d.success && d.lo ? d.lo : null;
      return window.__bevriPosLo;
    })
    .catch(function() { window.__bevriPosLo = null; return null; });
}
window.fetchPosLoCard = fetchPosLoCard;

/**
 * Merged LO branding for white-labeling the island (rail header, welcome
 * hero, chat header): server truth (status.loanTeam) wins over the link's
 * ?lo= card; either alone works. Cached; null means unattributed, and every
 * consumer falls back to Bevri branding.
 */
function fetchPosLoBranding() {
  if (window.__bevriLoBranding !== undefined) return Promise.resolve(window.__bevriLoBranding);
  const statusP = window.__bevriLoanStatus ? Promise.resolve(window.__bevriLoanStatus) : fetchPosLoanStatus();
  return statusP
    .then(function(s) {
      const team = s && s.loanTeam && s.loanTeam.displayName ? s.loanTeam : null;
      return fetchPosLoCard().then(function(card) {
        const merged = team || card ? { ...(card || {}), ...(team || {}) } : null;
        window.__bevriLoBranding = merged && merged.displayName ? merged : null;
        return window.__bevriLoBranding;
      });
    })
    .catch(function() { window.__bevriLoBranding = null; return null; });
}
window.fetchPosLoBranding = fetchPosLoBranding;

/** LO avatar: headshot when the LO uploaded one, branded initials otherwise.
 *  preferLogo (rail use) puts the company logo first; logos keep their aspect
 *  ratio instead of being cropped into the circle. */
function LoAvatar({ lo, size = 34, preferLogo = false }) {
  if (preferLogo && lo && lo.logo) {
    return <img className="lo-ava-logo" src={lo.logo} alt={(lo.companyName || lo.displayName || 'Loan officer') + ' logo'}
      style={{ height: size, maxWidth: Math.round(size * 2.6) }} referrerPolicy="no-referrer" />;
  }
  if (lo && lo.photoUrl) {
    return <img className="lo-ava-img" src={lo.photoUrl} alt={lo.displayName || 'Loan officer'}
      style={{ width: size, height: size }} referrerPolicy="no-referrer" />;
  }
  const initials = String((lo && lo.displayName) || '').trim().split(/\s+/).slice(0, 2)
    .map(function(part) { return part.charAt(0).toUpperCase(); }).join('') || 'LO';
  return <span className="lo-ava-init" style={{ width: size, height: size, fontSize: Math.round(size * 0.38) }} aria-hidden="true">{initials}</span>;
}
window.LoAvatar = LoAvatar;

/**
 * "Your loan officer" card for the shell header, upper right.
 * Server truth first (the loan's assigned LO from /status), the link's ?lo=
 * slug as fallback, so the borrower always sees who owns their application,
 * even on resume links that carry no slug.
 */
function LoBadge() {
  const [lo, setLo] = useSH(function() {
    var s = window.__bevriLoanStatus;
    return (s && s.loanTeam) || window.__bevriPosLo || null;
  });
  useSHE(function() {
    var alive = true;
    var statusPromise = window.__bevriLoanStatus
      ? Promise.resolve(window.__bevriLoanStatus)
      : fetchPosLoanStatus();
    statusPromise.then(function(s) {
      if (alive && s && s.loanTeam && s.loanTeam.displayName) { setLo(s.loanTeam); return null; }
      return fetchPosLoCard().then(function(card) {
        if (alive && card) setLo(function(prev) { return prev || card; });
      });
    });
    return function() { alive = false; };
  }, []);
  if (!lo || !lo.displayName) return null;
  var initials = String(lo.displayName).trim().split(/\s+/).slice(0, 2)
    .map(function(part) { return part.charAt(0).toUpperCase(); }).join('') || 'LO';
  return (
    <div className="lo-card" title={lo.phone ? 'Call ' + lo.displayName + ' at ' + lo.phone : lo.displayName}>
      <span className="lo-card-avatar" aria-hidden="true">{initials}</span>
      <span className="lo-card-info">
        <b>{lo.displayName}</b>
        <span className="lo-card-role">Your Loan Officer{lo.nmls ? ' · NMLS ' + lo.nmls : ''}</span>
        {lo.phone ? <a className="lo-card-phone" href={'tel:' + String(lo.phone).replace(/[^\d+]/g, '')}>{lo.phone}</a> : null}
      </span>
    </div>
  );
}

/**
 * Live autosave indicator: "Saving…" while a draft sync is pending, a ticked
 * "Saved" when it lands, then back to the quiet default. Driven by the
 * bevri:draft-save-state events from posStorage.
 */
function SaveStatus() {
  const [state, setState] = useSH('idle');
  useSHE(function() {
    var timer = null;
    function onState(e) {
      var next = e.detail && e.detail.state;
      if (timer) { clearTimeout(timer); timer = null; }
      if (next === 'saving') setState('saving');
      else if (next === 'saved') {
        setState('saved');
        timer = setTimeout(function() { setState('idle'); }, 2400);
      } else setState('idle');
    }
    window.addEventListener('bevri:draft-save-state', onState);
    return function() {
      if (timer) clearTimeout(timer);
      window.removeEventListener('bevri:draft-save-state', onState);
    };
  }, []);
  if (state === 'saving') {
    return <span className="save-status is-saving" aria-live="polite">Saving<span className="save-dots"><i /><i /><i /></span></span>;
  }
  if (state === 'saved') {
    return (
      <span className="save-status is-saved" aria-live="polite">
        <svg className="save-check" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
        Saved
      </span>
    );
  }
  return <span className="save-exit-copy">Your progress saves automatically.</span>;
}

// Compact status block for the operator prompt: enough to answer "where is
// my loan?" truthfully, nothing that could speak to outcomes.
function loanStatusForContext() {
  var s = window.__bevriLoanStatus;
  if (!s || !s.submitted) return undefined;
  return {
    submitted: true,
    submittedAt: s.submittedAt || null,
    currentStage: s.tracker && s.tracker.current ? s.tracker.current.label : null,
    stageDescription: s.tracker && s.tracker.current ? s.tracker.current.description : null,
    needsLoanTeamContact: Boolean(s.tracker && s.tracker.attention),
    readinessGrade: s.readiness && s.readiness.grade ? s.readiness.grade : null,
  };
}

// Payroll verification status for the operator: the webhook stamps the server
// draft; locally we can also infer it from imported argyle records so the
// chat knows verified income is settled even before the next full hydrate.
function payrollForContext() {
  if (window.__bevriPayroll) return window.__bevriPayroll;
  try {
    const recs = (readPosJson('bevri_income_v1')?.incomes?.primary) || [];
    const employers = recs.filter(r => r && r.method === 'argyle').map(r => r.employer);
    return employers.length ? { provider: 'argyle', employers } : undefined;
  } catch { return undefined; }
}

function currentApplicationContext() {
  return {
    loanId: currentLoanId(),
    payroll: payrollForContext(),
    // Credit pull outcome (memory-only; set by the credit step after the
    // authorize/status flow). Count only, never scores or tradeline details.
    creditStatus: window.__bevriCreditStatus || undefined,
    status: loanStatusForContext(),
    loanOfficer: (window.__bevriLoanStatus && window.__bevriLoanStatus.loanTeam) || window.__bevriPosLo || undefined,
    // Required-before-submit gaps (from /api/pos/intake/required) so the
    // operator can steer the borrower to exactly what still blocks submission.
    requiredMissing: (window.__bevriRequiredMissing && window.__bevriRequiredMissing.length ? window.__bevriRequiredMissing : undefined),
    app: readPosJson('bevri_pos_app_v1'),
    purchase: readPosJson('bevri_purchase_v1'),
    refinance: readPosJson('bevri_refinance_v1'),
    borrower: readPosJson('bevri_borrower_v1'),
    income: readPosJson('bevri_income_v1'),
    assets: readPosJson('bevri_assets_v1'),
    reo: readPosJson('bevri_reo_v1'),
    credit: readPosJson('bevri_credit_v1'),
    questions: readPosJson('bevri_questions_v1'),
    documents: readPosJson('bevri_documents_v1'),
  };
}
function dockReply(text, ctx) {
  const t = text.toLowerCase();
  const stage = (ctx.stageLabel || 'this section').toLowerCase();
  if (/document|paperwork|need to provide|upload|bring|paystub|bank statement|w-?2|1099|id|license/.test(t)) {
    return { text: "You can upload borrower documents from the Upload documents card in Review & Submit. Start with photo ID, recent pay stubs, W-2s or 1099s, and recent bank statements. Files stay attached to this application for the loan team to review.", quick: ["Upload a paystub", "Upload bank statements", "What else is needed?"] };
  }
  if (/afford|payment|monthly|rate|qualify|preapproval|pre-approval/.test(t)) {
    return { text: `I can help build a real qualification picture, but I need the full POS facts rather than a generic estimate: property, occupancy, borrower, income, assets, REO, credit/liabilities, declarations, and documents. You are in **${ctx.stageLabel || 'the application'}** now. Answer the current step and I'll keep narrowing the next best question.`, quick: ["What should I do next?", "Explain this step", "What documents can I upload?"] };
  }
  if (/secure|safe|privacy|protect|data/.test(t)) {
    return { text: "Your information is encrypted and only used to evaluate your loan options. We never sell your data, and a soft review at this stage doesn't affect your credit score.", quick: ["Why do you need this?", "What documents will I need?"] };
  }
  if (/how long|take|time|minutes|fast/.test(t)) {
    return { text: "Each section only takes a few minutes. I ask one question at a time so it never feels overwhelming. You can pause and your progress is saved automatically.", quick: ["Can I finish later?", "What's next after this?"] };
  }
  if (/why|need this|reason/.test(t)) {
    return { text: `Great question. The details in **${ctx.stageLabel || 'this step'}** help us match you with the loan programs you actually qualify for and give you an accurate, personalized quote with no surprises later.`, quick: ["What documents will I need?", "Is my information secure?"] };
  }
  if (/later|pause|save|come back|finish/.test(t)) {
    return { text: 'Absolutely. Your progress is saved as you go. You can use **Save & exit** anytime and pick up right where you left off.', quick: ["What's next after this?"] };
  }
  if (/next|after this|what's left|remaining/.test(t)) {
    return { text: "After this you'll move through Employment & Income, Assets, Real Estate Owned, and Credit & Liabilities, then a final review before you submit. You can always see where you are on the left.", quick: ["How long does this take?"] };
  }
  if (/help|stuck|confus|don't understand|what do/.test(t)) {
    return { text: `No problem, I'm right here. Tell me which question you're stuck on in **${ctx.stageLabel || 'this section'}** and I'll explain it in plain language.`, quick: ["What documents will I need?", "Why do you need this?"] };
  }
  return { text: `Happy to help with that. While you're in **${ctx.stageLabel || 'your application'}**, you can ask me to explain any question, what documents you'll need, or what happens next.`, quick: ["Why do you need this?", "What documents will I need?", "Is my information secure?"] };
}

/* ---- Document processing banner (shows in the form area, not just the chat) ---- */
function DocProcessingBanner() {
  const [state, setDPBState] = useSH(null); // null | 'uploading' | 'processing' | 'success' | 'done' | 'error'
  const [filledCount, setFilledCount] = useSH(0);
  const [fileName, setFileName] = useSH('');
  const timerRef = useSHRef(null);

  useSHE(() => {
    function clear() { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } }
    function onUploading() { clear(); setDPBState('uploading'); }
    function onProcessing(e) { clear(); setFileName(e.detail?.fileName || ''); setDPBState('processing'); }
    function onProcessed(e) {
      clear();
      const n = e.detail?.filledCount || 0;
      setFilledCount(n);
      setDPBState(n > 0 ? 'success' : 'done');
      timerRef.current = setTimeout(() => setDPBState(null), n > 0 ? 4000 : 2500);
    }
    function onError() {
      clear();
      setDPBState('error');
      timerRef.current = setTimeout(() => setDPBState(null), 5000);
    }
    window.addEventListener('bevri:doc-uploading', onUploading);
    window.addEventListener('bevri:doc-processing', onProcessing);
    window.addEventListener('bevri:doc-processed', onProcessed);
    window.addEventListener('bevri:doc-error', onError);
    return () => {
      clear();
      window.removeEventListener('bevri:doc-uploading', onUploading);
      window.removeEventListener('bevri:doc-processing', onProcessing);
      window.removeEventListener('bevri:doc-processed', onProcessed);
      window.removeEventListener('bevri:doc-error', onError);
    };
  }, []);

  if (!state) return null;

  const cfg = {
    uploading: { icon: I.doc(15), text: 'Uploading your document…', cls: '', dots: true },
    processing: { icon: I.doc(15), text: fileName ? `Reading ${fileName} and auto-filling matching fields…` : 'Reading your document and auto-filling matching fields…', cls: '', dots: true },
    success: { icon: I.check(14), text: `${filledCount} field${filledCount === 1 ? '' : 's'} auto-filled. You'll see them already filled in as you go`, cls: 'dpb-success', dots: false },
    done: { icon: I.check(14), text: 'Document read. No additional fields to fill in automatically', cls: 'dpb-success', dots: false },
    error: { icon: null, text: 'Document could not be read. Please fill in your details manually', cls: 'dpb-error', dots: false },
  }[state];

  return (
    <div className={'doc-proc-banner' + (cfg.cls ? ' ' + cfg.cls : '')}>
      <span className="dpb-icon">{cfg.icon}</span>
      <span className="dpb-text">{cfg.text}</span>
      {cfg.dots && (
        <span className="dpb-dots">
          <span className="dpb-dot" /><span className="dpb-dot" /><span className="dpb-dot" />
        </span>
      )}
    </div>
  );
}

/* ---- MobileProgressBar: thin overall-progress strip shown below the header on mobile ---- */
function MobileProgressBar({ currentStage, stageFill }) {
  const curIdx = STAGES.findIndex(function(s) { return s.id === currentStage; });
  if (curIdx < 0) return null;
  const totalPct = Math.min(100, Math.round(((curIdx + (stageFill || 0) / 100) / STAGES.length) * 100));
  return (
    <div className="mob-prog">
      <div className="mob-prog-fill" style={{ width: totalPct + '%' }} />
    </div>
  );
}


/* ---- DocUploadPanel: full drag-and-drop upload drawer with per-file extraction UX ---- */
function DocUploadPanel({ open, onClose, loanType }) {
  const [fileStates, setFileStates] = useSH([]);   // [{id, name, status, fields, error}]
  const [isDragging, setIsDragging] = useSH(false);
  const [isAuthed, setIsAuthed] = useSH(false);
  const inputRef = useSHRef(null);
  const hasSuccessful = fileStates.some(f => f.status === 'done' && f.fields.length > 0);

  function refreshSession() {
    const loanId = window.BevriPosStorage?.loanId || currentLoanId();
    if (!loanId) return;
    fetch('/api/pos/intake/session?loanId=' + encodeURIComponent(loanId), { credentials: 'same-origin' })
      .then(function(r) { return r.ok ? r.json() : null; })
      .then(function(d) {
        if (!d || !d.success) return;
        setIsAuthed(Boolean(d.isAuthenticated));
      })
      .catch(function() {});
  }

  useSHE(function() { if (open) refreshSession(); }, [open]);

  // Files dropped anywhere on the page (PageDropOverlay) are stashed on
  // window and consumed here once the panel is open, so page-wide drops and
  // in-panel drops go through the exact same pipeline.
  useSHE(function() {
    if (!open) return;
    function consumeDropped() {
      var files = window.__bevriDroppedFiles;
      window.__bevriDroppedFiles = null;
      if (files && files.length) addFiles(files);
    }
    consumeDropped();
    window.addEventListener('bevri:upload-files', consumeDropped);
    return function() { window.removeEventListener('bevri:upload-files', consumeDropped); };
  }, [open]);

  // Borrower needs checklist ("N of M items"): served by the scorecard
  // endpoint, which derives it from the canonical LOS ruleset and the draft's
  // classified uploads. Refreshes after each processed document. When the
  // endpoint is unavailable the panel falls back to the static hint chips.
  const [needs, setNeeds] = useSH(null);
  useSHE(function() {
    if (!open) return;
    var alive = true;
    function loadNeeds() {
      var applicationContext = {
        purchase: readPosJson('bevri_purchase_v1'),
        refinance: readPosJson('bevri_refinance_v1'),
        borrower: readPosJson('bevri_borrower_v1'),
        income: readPosJson('bevri_income_v1'),
        assets: readPosJson('bevri_assets_v1'),
        credit: readPosJson('bevri_credit_v1'),
        questions: readPosJson('bevri_questions_v1'),
        documents: readPosJson('bevri_documents_v1'),
      };
      fetch('/api/pos/intake/scorecard', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ applicationContext, loanId: window.BevriPosStorage?.loanId || '' }),
      })
        .then(function(r) { return r.ok ? r.json() : null; })
        .then(function(d) {
          if (alive) setNeeds(d && d.scorecard && d.scorecard.needs && Array.isArray(d.scorecard.needs.items) ? d.scorecard.needs : null);
        })
        .catch(function() { if (alive) setNeeds(null); });
    }
    loadNeeds();
    window.addEventListener('bevri:doc-processed', loadNeeds);
    return function() { alive = false; window.removeEventListener('bevri:doc-processed', loadNeeds); };
  }, [open]);

  function updateFile(id, patch) {
    setFileStates(function(prev) { return prev.map(function(f) { return f.id === id ? Object.assign({}, f, patch) : f; }); });
  }

  async function processOneFile(id, file) {
    const loanId = window.BevriPosStorage?.loanId || currentLoanId();
    if (!loanId) { updateFile(id, { status: 'error', error: 'Missing application link. Refresh and try again.' }); return; }
    updateFile(id, { status: 'uploading' });
    window.dispatchEvent(new CustomEvent('bevri:doc-uploading'));
    try {
      const body = new FormData();
      body.append('loanId', loanId);
      body.append('documentType', 'borrower_upload');
      body.append('loanPurpose', loanType === 'refinance' ? 'Refinance' : 'Purchase');
      body.append('file', file, file.name);
      const res = await fetch('/api/pos/intake/documents/upload', { method: 'POST', body });
      const data = await res.json().catch(function() { return {}; });
      if (!res.ok || !data.success) {
        updateFile(id, { status: 'error', error: data.error || 'Upload failed. Please try again.' });
        window.dispatchEvent(new CustomEvent('bevri:doc-error'));
        return;
      }
      const doc = data.document || { name: file.name, status: 'uploaded' };
      appendUploadedDocuments([doc]);
      if (!doc.id) {
        updateFile(id, { status: 'done', fields: [] });
        window.dispatchEvent(new CustomEvent('bevri:doc-processed', { detail: { filledCount: 0 } }));
        return;
      }
      updateFile(id, { status: 'analyzing' });
      window.dispatchEvent(new CustomEvent('bevri:doc-processing', { detail: { fileName: file.name } }));
      var result = await pollAndAutoFill(doc.id, loanId);
      var filled = result.filled || [];
      var seen = new Set();
      var uniqueFields = filled.filter(function(f) {
        if (seen.has(f.label)) return false;
        seen.add(f.label);
        return true;
      }).map(function(f) {
        var isAmt = typeof f.value === 'number' && /income|Amount|balance|value|price|amount/i.test(f.urlaField || '');
        var dv = isAmt ? '$' + Number(f.value).toLocaleString('en-US', { maximumFractionDigits: 0 }) : (f.value != null ? String(f.value) : '');
        return { label: f.label, displayValue: dv };
      });
      updateFile(id, { status: 'done', fields: uniqueFields, review: result.review || null });
      window.dispatchEvent(new CustomEvent('bevri:doc-processed', {
        detail: {
          filledCount: uniqueFields.length,
          fileName: file.name,
          documentSubtype: result.documentSubtype || null,
          review: result.review || null,
        },
      }));
      refreshSession();
    } catch (err) {
      updateFile(id, { status: 'error', error: (err && err.message) || 'Upload failed. Please try again.' });
      window.dispatchEvent(new CustomEvent('bevri:doc-error'));
    }
  }

  function addFiles(picked) {
    var arr = Array.from(picked || []).filter(function(f) { return f.type.startsWith('image/') || f.type === 'application/pdf'; });
    if (!arr.length) return;
    var entries = arr.map(function(file) { return { id: Math.random().toString(36).slice(2), name: file.name, status: 'pending', fields: [], error: '' }; });
    setFileStates(function(prev) { return prev.concat(entries); });
    entries.forEach(function(entry, i) { processOneFile(entry.id, arr[i]); });
  }

  function onDragOver(e) { e.preventDefault(); setIsDragging(true); }
  function onDragLeave(e) { if (!e.currentTarget.contains(e.relatedTarget)) setIsDragging(false); }
  function onDrop(e) {
    e.preventDefault(); setIsDragging(false);
    addFiles(e.dataTransfer.files);
  }

  if (!open) return null;

  const SVG_UPLOAD = <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="17 8 12 3 7 8" /><line x1="12" y1="3" x2="12" y2="15" /></svg>;
  const SVG_DOC = <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="M7 3h7l5 5v13H7z" /><path d="M14 3v5h5" /></svg>;
  const SVG_CHECK = <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M5 12.5 10 17.5 19 7" /></svg>;
  const SVG_X = <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M6 6l12 12M18 6 6 18" /></svg>;
  const SVG_ALERT = <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="9" /><path d="M12 8v4M12 16h.01" /></svg>;

  const docTypes = [
    { icon: '📋', label: 'Pay stub' },
    { icon: '📄', label: 'W-2 / 1099' },
    { icon: '🏦', label: 'Bank statement' },
    { icon: '🪪', label: "Driver's license / ID" },
  ];

  return (
    <React.Fragment>
      <div className="upload-panel-scrim" onClick={onClose} />
      <div className="upload-panel" role="dialog" aria-modal="true" aria-label="Upload documents">
        {/* ── Header ── */}
        <div className="up-head">
          <span className="up-head-icon">{SVG_DOC}</span>
          <div style={{ flex: 1 }}>
            <div className="up-head-title">Upload Documents</div>
            <div className="up-head-sub">Uploaded docs auto-fill matching form fields</div>
          </div>
          <button className="up-close" onClick={onClose} aria-label="Close">
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="m6 6 12 12M18 6 6 18" /></svg>
          </button>
        </div>

        <div className="up-body">
          {/* ── What to upload: live needs checklist when available, static
                 hint chips otherwise ── */}
          {needs ? (
            <div>
              <div className="up-section-label">
                Your document checklist
                <span className="up-needs-count">{needs.satisfiedCount} of {needs.totalCount} received</span>
              </div>
              <div className="up-needs">
                {needs.items.map(function(item) {
                  return (
                    <div key={item.id} className={'up-need ' + item.status}>
                      <span className="up-need-ic">{item.status === 'satisfied' ? SVG_CHECK : SVG_DOC}</span>
                      <span className="up-need-txt">
                        <b>{item.title}</b>
                        <span>
                          {item.status === 'satisfied'
                            ? (item.needsAttention
                              ? 'Received, but needs attention. See the file notes below'
                              : (item.matchedDocuments && item.matchedDocuments[0] ? 'Received: ' + item.matchedDocuments[0] : 'Received'))
                            : item.whyItMatters}
                        </span>
                      </span>
                    </div>
                  );
                })}
              </div>
            </div>
          ) : (
            <div>
              <div className="up-section-label">Accepted documents</div>
              <div className="up-hints">
                {docTypes.map(function(h) { return <span key={h.label} className="up-hint">{h.icon} {h.label}</span>; })}
              </div>
            </div>
          )}

          {/* ── Drop zone ── */}
          <div className={'up-drop' + (isDragging ? ' drag-over' : '')}
            onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
            onClick={function() { inputRef.current && inputRef.current.click(); }}>
            <input ref={inputRef} type="file" multiple accept=".pdf,image/*"
              style={{ display: 'none' }}
              onChange={function(e) { addFiles(e.target.files); if (inputRef.current) inputRef.current.value = ''; }} />
            <span className="up-drop-icon">{SVG_UPLOAD}</span>
            <div className="up-drop-label">{isDragging ? 'Drop to upload' : 'Drag & drop files here'}</div>
            <div className="up-drop-sub">PDF, JPG, or PNG, up to 10 MB each · multiple files OK</div>
            <button className="up-drop-btn" type="button"
              onClick={function(e) { e.stopPropagation(); inputRef.current && inputRef.current.click(); }}>
              {SVG_UPLOAD} Browse files
            </button>
          </div>

          {/* ── Uploaded files list ── */}
          {fileStates.length > 0 && (
            <div>
              <div className="up-section-label">Your uploads</div>
              <div className="up-file-list">
                {fileStates.map(function(f) {
                  var progWidth = f.status === 'uploading' ? '55%' : f.status === 'analyzing' ? '85%' : '100%';
                  return (
                    <div key={f.id} className={'up-file ' + f.status}>
                      <div className="up-file-row">
                        <span className="up-file-icon">
                          {f.status === 'done' ? SVG_CHECK : f.status === 'error' ? SVG_ALERT : SVG_DOC}
                        </span>
                        <div className="up-file-info">
                          <div className="up-file-name" title={f.name}>{f.name}</div>
                          <div className={'up-file-status' + (f.status === 'done' ? (f.review && f.review.verdict === 'needs_attention' ? ' warn' : ' ok') : f.status === 'error' ? ' err' : '')}>
                            {f.status === 'uploading' && [<span key="s" className="up-spinner" />, ' Uploading…']}
                            {f.status === 'analyzing' && [<span key="s" className="up-spinner" />, ' Extracting fields…']}
                            {f.status === 'done' && (!f.review || f.review.verdict !== 'needs_attention') && f.fields.length > 0 && ('Looks good: ' + f.fields.length + ' field' + (f.fields.length === 1 ? '' : 's') + ' filled in')}
                            {f.status === 'done' && (!f.review || f.review.verdict !== 'needs_attention') && f.fields.length === 0 && 'Uploaded. No matching fields to fill'}
                            {f.status === 'done' && f.review && f.review.verdict === 'needs_attention' && 'Needs attention'}
                            {f.status === 'error' && (f.error || 'Upload failed')}
                            {f.status === 'pending' && 'Queued…'}
                          </div>
                          {/* Plain-language reasons + application-vs-document mismatches */}
                          {f.status === 'done' && f.review && f.review.verdict === 'needs_attention' && (
                            <div className="up-file-review">
                              {(f.review.reasons || []).map(function(reason, ri) {
                                return <div key={'r' + ri} className="up-file-review-line">{reason}</div>;
                              })}
                              {(f.review.contradictions || []).map(function(c, ci) {
                                return (
                                  <div key={'c' + ci} className="up-file-review-line">
                                    {c.label}: the document shows <b>{c.documentValue}</b>, your application says <b>{c.applicationValue}</b>. You can fix this in chat.
                                  </div>
                                );
                              })}
                            </div>
                          )}
                        </div>
                      </div>
                      {/* progress bar while uploading/analyzing */}
                      {(f.status === 'uploading' || f.status === 'analyzing' || f.status === 'done') && (
                        <div className="up-file-prog">
                          <div className="up-file-prog-bar" style={{ width: progWidth }} />
                        </div>
                      )}
                      {/* extracted fields */}
                      {f.status === 'done' && f.fields.length > 0 && (
                        <div className="up-fields">
                          <div className="up-fields-hd">Filled in for you</div>
                          {f.fields.map(function(field) {
                            return (
                              <div key={field.label} className="up-field-row">
                                <span className="up-field-key">{field.label}</span>
                                <span className="up-field-val">{field.displayValue}</span>
                                <span className="up-field-chk">
                                  <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round"><path d="M5 12.5 10 17.5 19 7" /></svg>
                                </span>
                              </div>
                            );
                          })}
                        </div>
                      )}
                      {f.status === 'done' && f.fields.length === 0 && (
                        <div className="up-fields"><div className="up-no-fields">No fields matched for automatic fill. Your loan team will review this document.</div></div>
                      )}
                    </div>
                  );
                })}
              </div>
            </div>
          )}

          {/* ── Conversion strip — after a successful extraction ── */}
          {hasSuccessful && !isAuthed && (
            <div className="up-save-strip">
              <div className="up-save-title">Your application is taking shape</div>
              <div className="up-save-sub">Create a free account to save everything securely and continue on any device.</div>
              <button className="up-drop-btn" onClick={function() { window.dispatchEvent(new CustomEvent('bevri:signup-open')); }}>
                Save my progress
              </button>
            </div>
          )}
        </div>
      </div>
    </React.Fragment>
  );
}

/* ---- per-loan chat persistence (rides the draft sync via bevri_chat_v1) ----
   Stored rows are shaped like a future PosChatMessage table
   ({id, role: user|assistant, text, source?, createdAt}) so a later move to
   server-side transcripts is a backfill, not a rewrite. Volatile UI fields
   (quick replies, typing) are not persisted except quick on the last row. */
const CHAT_STORE_KEY = 'bevri_chat_v1';
const CHAT_MAX_MESSAGES = 40;
const CHAT_MAX_TEXT = 1500;

function loadChatHistory() {
  var saved = readPosJson(CHAT_STORE_KEY);
  var rows = saved && Array.isArray(saved.messages) ? saved.messages : [];
  return rows
    .filter(function(m) { return m && typeof m.text === 'string' && m.text && (m.role === 'user' || m.role === 'assistant'); })
    .map(function(m, i) {
      return {
        id: m.id || 'h-' + i,
        role: m.role === 'assistant' ? 'ai' : 'user',
        text: m.text,
        quick: Array.isArray(m.quick) ? m.quick : undefined,
      };
    });
}

function persistChatHistory(messages) {
  try {
    var rows = (messages || [])
      .filter(function(m) { return m && typeof m.text === 'string' && m.text; })
      .slice(-CHAT_MAX_MESSAGES)
      .map(function(m, i, arr) {
        var row = {
          id: m.id || 'm-' + i,
          role: m.role === 'user' ? 'user' : 'assistant',
          text: String(m.text).slice(0, CHAT_MAX_TEXT),
          createdAt: m.createdAt || new Date().toISOString(),
        };
        if (m.source) row.source = m.source;
        // Keep quick replies only on the final row: they are the live prompt
        // options, meaningless on older turns.
        if (i === arr.length - 1 && Array.isArray(m.quick) && m.quick.length) row.quick = m.quick;
        return row;
      });
    writePosJson(CHAT_STORE_KEY, { messages: rows, updatedAt: new Date().toISOString() });
  } catch {}
}

function ChatDock({ open, setOpen, context }) {
  const [messages, setMessages] = useSH(loadChatHistory);
  const [draft, setDraft] = useSH('');
  const [typing, setTyping] = useSH(false);
  const [isAuthed, setIsAuthed] = useSH(false);
  const threadRef = useSHRef(null);
  const taRef = useSHRef(null);
  // A restored conversation counts as seeded: returning borrowers are not
  // re-greeted on top of their own history.
  const seeded = useSHRef(loadChatHistory().length > 0);

  // Write-through persistence: one small local write per message, riding the
  // same debounced server sync as every form edit (bevri_chat_v1 is a draft
  // key). Empty state is not persisted so a fresh dock never clears history.
  useSHE(() => {
    if (messages.length > 0) persistChatHistory(messages);
  }, [messages]);

  // Read the current document count / limit from the same-origin session route.
  // Read-only: does not touch the upload or IDP pipeline.
  function refreshDocState() {
    const loanId = window.BevriPosStorage?.loanId || currentLoanId();
    if (!loanId) return;
    fetch(`/api/pos/intake/session?loanId=${encodeURIComponent(loanId)}`, { credentials: 'same-origin' })
      .then(r => (r.ok ? r.json() : null))
      .then(d => {
        if (!d || !d.success) return;
        setIsAuthed(Boolean(d.isAuthenticated));
      })
      .catch(() => {});
  }

  function seedGreeting() {
    if (seeded.current) return;
    seeded.current = true;
    setMessages([{ role: 'ai', id: 'g', text: `Hi 👋 You're on **${context.stageLabel}**. Ask me anything. I can explain a question, tell you what documents you'll need, or what happens next.`, quick: ["Why do you need this?", "What documents will I need?", "Is my information secure?"] }]);
  }

  // seed greeting the first time the thread opens
  useSHE(() => { if (open) seedGreeting(); /* eslint-disable-next-line */ }, [open]);
  useSHL(() => { const el = taRef.current; if (!el) return; el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 120) + 'px'; }, [draft]);
  useSHE(() => { const el = threadRef.current; if (el) el.scrollTop = el.scrollHeight; }, [messages, typing, open]);
  useSHE(() => { refreshDocState(); /* eslint-disable-next-line */ }, []);
  useSHE(() => {
    window.addEventListener('bevri:doc-processed', refreshDocState);
    return () => window.removeEventListener('bevri:doc-processed', refreshDocState);
  }, []);

  // Narrate processed documents in the thread (playbook: review the response
  // immediately and tell the borrower whether it satisfied the request).
  // Local, deterministic message from the stored review verdict -- no model
  // round-trip. The operator can then fix any mismatch conversationally.
  useSHE(() => {
    function onDocProcessed(e) {
      var d = (e && e.detail) || {};
      if (!d.fileName) return; // older dispatch sites carry no narration payload
      seedGreeting();
      var docLabel = d.documentSubtype ? d.documentSubtype + ' (' + d.fileName + ')' : d.fileName;
      var lines = ['Got your ' + docLabel + '.'];
      if (d.filledCount > 0) lines.push('I filled ' + d.filledCount + ' field' + (d.filledCount === 1 ? '' : 's') + ' from it.');
      var review = d.review || null;
      var quick = ["What's still needed?"];
      if (review && review.verdict === 'needs_attention') {
        (review.reasons || []).slice(0, 2).forEach(function(reason) { lines.push(reason); });
        (review.contradictions || []).slice(0, 2).forEach(function(c) {
          lines.push(c.label + ': the document shows **' + c.documentValue + '** but your application says **' + c.applicationValue + '**.');
        });
        if ((review.contradictions || []).length) {
          lines.push('Want me to update your application to match the document?');
          quick = ["Yes, use the document's numbers", 'Keep what I entered', "What's still needed?"];
        }
      } else if (d.filledCount > 0) {
        lines.push('Everything checked out.');
      }
      setMessages(function(prev) {
        return prev.concat([{ role: 'ai', id: 'doc-' + Date.now(), text: lines.join(' '), quick: quick }]);
      });
    }
    window.addEventListener('bevri:doc-processed', onDocProcessed);
    return () => window.removeEventListener('bevri:doc-processed', onDocProcessed);
  }, []);

  function send(text) {
    const clean = (text || '').trim(); if (!clean) return;
    // Intercept signup-intent quick actions without sending to the AI.
    if (/^(create account|save my progress|sign up|save & continue)$/i.test(clean)) {
      window.dispatchEvent(new CustomEvent('bevri:signup-open'));
      return;
    }
    if (/^(continue without account|continue for now)$/i.test(clean)) {
      return;
    }
    setDraft('');
    if (!seeded.current) seedGreeting();
    setOpen(true);
    const userMessage = { role: 'user', id: Math.random().toString(36).slice(2), text: clean };
    const visibleMessages = [...messages, userMessage];
    setMessages(visibleMessages);
    setTyping(true);

    const fallback = () => dockReply(clean, context);
    const finish = payload => {
      setTyping(false);
      setMessages(m => [...m, { role: 'ai', id: Math.random().toString(36).slice(2), ...(payload || fallback()) }]);
    };

    const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
    // The agentic operator may make a tool round-trip, so allow more time than
    // the deterministic reply; on timeout we fall back to the local reply.
    const timeout = controller ? setTimeout(() => controller.abort(), 16000) : null;
    const applicationContext = currentApplicationContext();

    fetch('/api/pos/intake/operator', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        message: clean,
        stageLabel: context.stageLabel,
        // Fine-grained step within the section (e.g. "price", "address") so the
        // operator can answer about the exact question the borrower is on, not
        // just the section. Partial form data already rides in applicationContext.
        stepTitle: context.stepTitle || '',
        loanId: applicationContext.loanId,
        applicationContext,
        messages: messages.map(m => ({ role: m.role === 'ai' ? 'assistant' : m.role, text: m.text, id: m.id })),
      }),
      signal: controller?.signal,
    })
      .then(r => r.ok ? r.json() : Promise.reject(new Error('operator unavailable')))
      .then(data => {
        // The operator can fill the application by talking. Apply its proposed
        // writes to the borrower's local draft, then let the flows re-read.
        const savedCount = applyOperatorFieldOps(data.fieldOps);
        if (savedCount) showToast('Saved ' + savedCount + ' detail' + (savedCount === 1 ? '' : 's') + ' from our chat', 'ok');
        if (data.navigateTo) window.dispatchEvent(new CustomEvent('bevri:pos-navigate', { detail: { step: data.navigateTo } }));
        finish({ text: data.text, quick: data.quick });
      })
      .catch(() => finish(fallback()))
      .finally(() => { if (timeout) clearTimeout(timeout); });
  }

  const fmt = txt => txt.split(/(\*\*[^*]+\*\*)/g).map((p, i) => p.startsWith('**') && p.endsWith('**') ? <strong key={i} style={{ fontWeight: 600 }}>{p.slice(2, -2)}</strong> : <React.Fragment key={i}>{p}</React.Fragment>);
  const lastAi = (() => { for (let i = messages.length - 1; i >= 0; i--) if (messages[i].role === 'ai') return messages[i].id; return null; })();
  const onKey = e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(draft); } };

  return (
    <div className={'chat-dock' + (open ? ' open' : '')}>
      {/* conversation thread (slides up above the composer) */}
      {open && (
        <div className="chat-sheet">
          <div className="chat-sheet-head">
            <span className="ava"><BrainMark size={22} /></span>
            <span className="csh-meta">
              <span className="h-name">{window.__bevriLoBranding && window.__bevriLoBranding.displayName
                ? String(window.__bevriLoBranding.displayName).split(' ')[0] + "'s application assistant"
                : 'Bevri Operator'}</span>
              <span className="h-sub"><span className="dot-live" /> Online · {context.stageLabel}</span>
            </span>
            <button className="chat-x" onClick={() => setOpen(false)} title="Minimize">
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="m6 9 6 6 6-6" /></svg>
            </button>
          </div>
          <div className="chat-thread" ref={threadRef}>
            {messages.map(m => (
              m.role === 'user'
                ? <div className="cd-row user" key={m.id}><div className="cd-bubble user">{m.text}</div></div>
                : <div className="cd-row" key={m.id}>
                    <span className="cd-ava"><BrainMark size={20} /></span>
                    <div>
                      <div className="cd-bubble ai">{fmt(m.text)}</div>
                      {m.id === lastAi && m.quick && !typing && (
                        <div className="cd-quick">{m.quick.map(q => <button key={q} className="cd-chip" onClick={() => send(q)}>{q}</button>)}</div>
                      )}
                    </div>
                  </div>
            ))}
            {typing && <div className="cd-row"><span className="cd-ava"><BrainMark size={20} /></span><div className="cd-bubble ai typing"><span className="dot" /><span className="dot" /><span className="dot" /></div></div>}
          </div>
        </div>
      )}

      {/* persistent bottom composer — on every page, GPT-style */}
      <div className="chat-bar">
        <div className="chat-bar-inner">
          <div className="cbar-composer">
            <span className="cbar-ava"><BrainMark size={22} /></span>
            <textarea ref={taRef} rows={1} className="cbar-input"
              placeholder="Message Bevri…  ask anything about this step"
              value={draft} onChange={e => setDraft(e.target.value)} onKeyDown={onKey}
              onFocus={() => setOpen(true)} />
            <button className="cbar-collapse" onClick={() => window.dispatchEvent(new CustomEvent('bevri:open-upload'))} title="Upload document" aria-label="Upload document">
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21.44 11.05 12.25 20.24a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 1 1-2.83-2.83l8.49-8.48" /></svg>
            </button>
            {open && messages.length > 0 && (
              <button className="cbar-collapse" onClick={() => setOpen(false)} title="Hide conversation">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="m6 9 6 6 6-6" /></svg>
              </button>
            )}
            <button className={'cbar-send' + (draft.trim() ? ' on' : '')} onClick={() => send(draft)} disabled={!draft.trim()} title="Send">
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 19V5" /><path d="m6 11 6-6 6 6" /></svg>
            </button>
          </div>
          <p className="cbar-hint">Operator can make mistakes. Not a commitment to lend.</p>
        </div>
      </div>
    </div>
  );
}

/* ---------------- shell wrapper ---------------- */
// Sign-up nudge: converts an anonymous POS session into a borrower account via
// the same-origin claim route. On success the borrower is sent to sign-in
// (existing account) or sign-up (new), with the draft flushed first so nothing
// is lost. Read-only toward the IDP/upload pipeline.
function SignUpNudge({ open, onClose }) {
  const [email, setEmail] = useSH('');
  const [busy, setBusy] = useSH(false);
  const [err, setErr] = useSH('');
  if (!open) return null;
  const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((email || '').trim());
  async function submit() {
    if (!valid || busy) return;
    setBusy(true); setErr('');
    try {
      const loanId = window.BevriPosStorage?.loanId || '';
      const res = await fetch('/api/pos/intake/claim', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ loanId, email: email.trim() }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok || !data.ok) throw new Error(data.error || 'Could not continue. Please try again.');
      try { await window.BevriPosStorage?.syncNow?.(); } catch {}
      const dest = data.existingAccount ? data.signInUrl : data.signupUrl;
      if (dest) { window.location.assign(dest); return; }
      onClose();
    } catch (e) {
      setErr(e?.message || 'Could not continue. Please try again.');
    } finally {
      setBusy(false);
    }
  }
  const checkIcon = (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--sage, #4f8b6e)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12.5 10 17.5 19 7" /></svg>
  );
  return (
    <div className="modal-scrim" onClick={busy ? undefined : onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        {/* Icon burst */}
        <div style={{ width: 56, height: 56, borderRadius: '50%', background: 'var(--mint-tint)', border: '1px solid var(--mint)', display: 'grid', placeItems: 'center', marginBottom: 16 }}>
          <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="var(--sage)" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" /></svg>
        </div>
        <div className="modal-title">You're halfway there. Save your spot</div>
        <div className="modal-body">
          <p style={{ marginBottom: 14, color: 'var(--muted)', fontSize: 14, lineHeight: 1.55 }}>Enter your email to lock in your progress. Free, no card needed, and it takes 10 seconds.</p>
          <ul style={{ margin: '0 0 18px', padding: 0, listStyle: 'none', display: 'flex', flexDirection: 'column', gap: 9, fontSize: 13.5 }}>
            {[
              'Everything saved securely to the cloud',
              'Continue on any device, any time',
              'Upload unlimited supporting documents',
              'Get matched with the right loan programs',
            ].map(function(item) {
              return (
                <li key={item} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                  {checkIcon}
                  <span style={{ color: 'var(--text)' }}>{item}</span>
                </li>
              );
            })}
          </ul>
          <input className="input" type="email" inputMode="email" autoFocus placeholder="your@email.com"
            value={email} onChange={e => setEmail(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') submit(); }} />
          {err && <div className="field-err" style={{ marginTop: 8 }}>{err}</div>}
        </div>
        <div className="modal-actions">
          <button className="btn btn-secondary" onClick={onClose} disabled={busy}>Continue without saving</button>
          <button className="btn btn-primary" onClick={submit} disabled={!valid || busy}>{busy ? 'Saving…' : 'Save my progress'}</button>
        </div>
      </div>
    </div>
  );
}

// Global toast — listens for bevri:toast events and renders self-removing pills.
function ToastRoot() {
  const [toasts, setToasts] = useSH([]);
  useSHE(() => {
    function onToast(e) {
      const id = Math.random().toString(36).slice(2);
      setToasts(t => [...t, { id, message: e.detail?.message || '', kind: e.detail?.kind || '' }]);
      setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3900);
    }
    window.addEventListener('bevri:toast', onToast);
    return () => window.removeEventListener('bevri:toast', onToast);
  }, []);
  if (!toasts.length) return null;
  return (
    <div className="toast-root" aria-live="polite">
      {toasts.map(t => (
        <div key={t.id} className={'toast' + (t.kind ? ' toast-' + t.kind : '')}>
          {t.kind === 'ok' && <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12.5 10 17.5 19 7" /></svg>}
          {t.message}
        </div>
      ))}
    </div>
  );
}

/**
 * Page-wide drag-and-drop: drag a file anywhere over the island and a
 * "Drop to upload" veil appears; dropping hands the files to the shared
 * DocUploadPanel. Drops landing on an existing dropzone (inline bar, panel)
 * are left to that zone's own handler, so nothing about them changes.
 */
function PageDropOverlay({ onFiles }) {
  const [visible, setVisible] = useSH(false);
  const depthRef = useSHRef(0);
  const onFilesRef = useSHRef(onFiles);
  onFilesRef.current = onFiles;

  useSHE(function() {
    function hasFiles(e) {
      var types = e.dataTransfer && e.dataTransfer.types;
      return !!types && Array.prototype.indexOf.call(types, 'Files') !== -1;
    }
    function inOwnZone(target) {
      return !!(target && target.closest && target.closest('.upload-panel'));
    }
    function reset() { depthRef.current = 0; setVisible(false); }
    function onDragEnter(e) { if (!hasFiles(e)) return; depthRef.current += 1; setVisible(true); }
    function onDragOver(e) { if (hasFiles(e)) e.preventDefault(); }
    function onDragLeave(e) {
      if (!hasFiles(e)) return;
      depthRef.current = Math.max(0, depthRef.current - 1);
      if (depthRef.current === 0) setVisible(false);
    }
    function onDrop(e) {
      var wasOwnZone = inOwnZone(e.target);
      reset();
      if (!hasFiles(e) || wasOwnZone) return; // existing dropzones keep their behavior
      e.preventDefault();
      var files = Array.from(e.dataTransfer.files || []);
      if (files.length) onFilesRef.current(files);
    }
    window.addEventListener('dragenter', onDragEnter);
    window.addEventListener('dragover', onDragOver);
    window.addEventListener('dragleave', onDragLeave);
    window.addEventListener('drop', onDrop);
    window.addEventListener('dragend', reset);
    return function() {
      window.removeEventListener('dragenter', onDragEnter);
      window.removeEventListener('dragover', onDragOver);
      window.removeEventListener('dragleave', onDragLeave);
      window.removeEventListener('drop', onDrop);
      window.removeEventListener('dragend', reset);
    };
  }, []);

  if (!visible) return null;
  return (
    <div className="page-drop-veil" aria-hidden="true">
      <div className="pdv-card">
        <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="17 8 12 3 7 8" /><line x1="12" y1="3" x2="12" y2="15" /></svg>
        <b>Drop to upload</b>
        <span>Pay stubs, W-2s, bank statements, ID (PDF or photo)</span>
      </div>
    </div>
  );
}

function AppShell({ loanType, currentStage, stageFill, onStageClick, onHome, onExit, chatContext, completedStages, children }) {
  const [railOpen, setRailOpen] = useSH(false);
  const [chatOpen, setChatOpen] = useSH(false);
  const [signupOpen, setSignupOpen] = useSH(false);
  const [uploadPanelOpen, setUploadPanelOpen] = useSH(false);
  const curLabel = (STAGES.find(s => s.id === currentStage) || STAGES[0]).label(loanType);
  const ctx = { stageLabel: chatContext?.stageLabel || curLabel, stepTitle: chatContext?.stepTitle || '' };
  const milestonesShownRef = useSHRef(new Set());

  const pickStage = id => { setRailOpen(false); onStageClick(id); };
  const saveAndExit = () => {
    try { window.BevriPosStorage?.syncNow?.(); } catch {}
    showToast('Progress saved. You can return anytime', 'ok');
    setTimeout(onExit, 700);
  };

  // Any island surface can open the sign-up nudge by dispatching this event.
  useSHE(() => {
    const openSignup = () => setSignupOpen(true);
    window.addEventListener('bevri:signup-open', openSignup);
    return () => window.removeEventListener('bevri:signup-open', openSignup);
  }, []);

  // Any island surface can open the upload panel by dispatching bevri:open-upload.
  useSHE(() => {
    const openUpload = () => setUploadPanelOpen(true);
    window.addEventListener('bevri:open-upload', openUpload);
    return () => window.removeEventListener('bevri:open-upload', openUpload);
  }, []);

  // Same pattern for the chat dock (used by the loan tracker's actions).
  useSHE(() => {
    const openChat = () => setChatOpen(true);
    window.addEventListener('bevri:open-chat', openChat);
    return () => window.removeEventListener('bevri:open-chat', openChat);
  }, []);

  // Conversion moment after completing employment. Never a modal (Jason/
  // no-interruptions feedback, 2026-08-05: a quiet
  // dismissible strip appears under the header instead. The account-claim
  // modal itself only ever opens from an explicit tap.
  const [stripVisible, setStripVisible] = useSH(false);
  useSHE(() => {
    if (currentStage !== 'assets') return;
    if (milestonesShownRef.current.has('post-employment')) return;
    milestonesShownRef.current.add('post-employment');
    try { if (sessionStorage.getItem('bevri_pos_strip_dismissed')) return; } catch {}
    const loanId = currentLoanId();
    if (!loanId) return;
    fetch('/api/pos/intake/session?loanId=' + encodeURIComponent(loanId), { credentials: 'same-origin' })
      .then(function(r) { return r.ok ? r.json() : null; })
      .then(function(d) { if (d && d.success && !d.isAuthenticated) setStripVisible(true); })
      .catch(function() {});
  }, [currentStage]);
  const dismissStrip = () => {
    setStripVisible(false);
    try { sessionStorage.setItem('bevri_pos_strip_dismissed', '1'); } catch {}
  };

  return (
    <div className="shell">
      {railOpen && <div className="rail-scrim" onClick={() => setRailOpen(false)} />}
      <aside className={'rail' + (railOpen ? ' open' : '')}>
        <ProgressRail loanType={loanType} currentStage={currentStage} stageFill={stageFill}
          onStageClick={pickStage} onHome={onHome} onOpenChat={() => { setRailOpen(false); setChatOpen(true); }}
          onOpenUpload={() => { setRailOpen(false); setUploadPanelOpen(true); }}
          completedStages={completedStages} />
      </aside>
      <div className="rail-main">
        <header className="shell-top">
          <div className="shell-top-left">
            <button className="rail-toggle" onClick={() => setRailOpen(true)} aria-label="Show progress">
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M4 6h16M4 12h16M4 18h16" /></svg>
            </button>
            <span className="shell-stage-chip">{curLabel}</span>
          </div>
          <div className="topbar-right save-exit-group">
            <LoBadge />
            <SaveStatus />
            <ThemeToggle />
            <button className="ghost-btn" onClick={saveAndExit} title="Save your progress and return to the start page">Save &amp; exit</button>
            <div className="avatar">••</div>
          </div>
        </header>
        <MobileProgressBar currentStage={currentStage} stageFill={stageFill} />
        {stripVisible && (
          <div className="save-strip" role="status">
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" /></svg>
            <span className="save-strip-txt">Your progress saves automatically on this device. Want a secure link to continue from anywhere?</span>
            <button className="save-strip-btn" onClick={() => { dismissStrip(); setSignupOpen(true); }}>Email me a link</button>
            <button className="save-strip-x" onClick={dismissStrip} aria-label="Dismiss">×</button>
          </div>
        )}
        <DocProcessingBanner />
            {children}
            <ChatDock open={chatOpen} setOpen={setChatOpen} context={ctx} />
      </div>
      <DocUploadPanel open={uploadPanelOpen} onClose={() => setUploadPanelOpen(false)} loanType={loanType} />
      <PageDropOverlay onFiles={(files) => {
        window.__bevriDroppedFiles = files;
        setUploadPanelOpen(true);
        window.dispatchEvent(new CustomEvent('bevri:upload-files'));
      }} />
      <SignUpNudge open={signupOpen} onClose={() => setSignupOpen(false)} />
      <ToastRoot />
    </div>
  );
}

window.AppShell = AppShell;
window.STAGES = STAGES;
