// ===========================================================================
// LEON Sign — send it, track it, file it
// ===========================================================================
// Built against Leon's own DocuSign account rather than against a guess at what
// e-signature software looks like. The study is in docs/leon-sign-study.md; the
// three facts it turned on are:
//
//   1. 17 of their 23 envelopes are SHOP DRAWING APPROVALS at a revision, and
//      every document type they send is already produced by this Hub. So the
//      job is not "add e-signature", it is CLOSING A LOOP that is currently a
//      manual download, rename, upload and re-file.
//   2. It is a revision workflow — Rev00 through Rev05 — which is exactly what
//      scope.submittals already models.
//   3. Routing is the same every time: the external party, then a Leon
//      counter-signer. That is orders 1 and 2, not a thing to design each time.
//
// WHAT THIS IS NOT. A page with no backend cannot produce what makes a DocuSign
// signature hold up — identity verified server-side, a tamper-evident chain and
// a timestamped certificate from a party with no stake in the document. So an
// envelope declares its own weight and the screen never claims more than it
// has. The one piece of real evidence a browser CAN produce is a content hash,
// and that is taken at send: it proves the bytes, and it does not prove the
// hour, and both halves are said out loud.

const SIGN_SECTIONS = [
  { key: 'inbox',   label: 'Waiting on me', icon: '✍️', group: 'Signing',
    hint: 'Envelopes routed to you that are yours to sign now.' },
  { key: 'all',     label: 'All envelopes', icon: '📄', group: 'Signing',
    hint: 'Everything sent, in progress, completed or voided.' },
  { key: 'compose', label: 'New envelope',  icon: '➕', group: 'Signing',
    hint: 'Take a document the Hub already produced and route it for signature.' },
  { key: 'about',   label: 'What this is',  icon: 'ℹ️', group: 'Settings',
    hint: 'What a signature here does and does not mean.' },
];

function signStatusTone(status) {
  if (status === 'Completed') return 'bg-green-100 text-green-800';
  if (status === 'Declined' || status === 'Voided') return 'bg-red-100 text-red-700';
  if (status === 'Draft') return 'bg-[var(--leon-line)]/60 text-[var(--leon-black)]/60';
  return 'bg-[var(--leon-yellow)]/30 text-[var(--leon-black)]/75';
}

function SignStatusBadge({ env }) {
  const s = signEnvelopeStatus(env);
  return <span className={`px-2 py-0.5 rounded text-[11px] font-semibold ${signStatusTone(s)}`}>{s}</span>;
}

// ---------------------------------------------------------------------------
// The signing surface
// ---------------------------------------------------------------------------
// A mark is drawn or typed. Drawn is kept as a data URI on the RECIPIENT, so
// one person signing a three-page set signs once and every field they own
// carries the same mark — which is also how it works on paper.
function SignPad({ onDone, onCancel, name }) {
  const cv = useRef(null);
  const [drawing, setDrawing] = useState(false);
  const [dirty, setDirty] = useState(false);
  const [typed, setTyped] = useState(name || '');
  const [mode, setMode] = useState('draw');
  const pos = e => {
    const r = cv.current.getBoundingClientRect();
    return [(e.clientX - r.left) * (cv.current.width / r.width),
            (e.clientY - r.top) * (cv.current.height / r.height)];
  };
  const start = e => {
    e.preventDefault();
    const c = cv.current.getContext('2d');
    c.lineWidth = 2.4; c.lineCap = 'round'; c.strokeStyle = '#12100e';
    c.beginPath(); c.moveTo(...pos(e));
    setDrawing(true); setDirty(true);
    cv.current.setPointerCapture(e.pointerId);
  };
  const move = e => { if (!drawing) return; const c = cv.current.getContext('2d'); c.lineTo(...pos(e)); c.stroke(); };
  const end = () => setDrawing(false);
  const clear = () => { const c = cv.current; c.getContext('2d').clearRect(0, 0, c.width, c.height); setDirty(false); };
  return (
    <div className="space-y-3">
      <div className="flex gap-2">
        {[['draw', 'Draw it'], ['type', 'Type it']].map(([k, l]) => (
          <button key={k} type="button" onClick={() => setMode(k)}
            className={`px-2.5 py-1 rounded text-xs font-semibold border ${mode === k
              ? 'bg-[var(--leon-black)] text-white border-[var(--leon-black)]'
              : 'border-[var(--leon-line)]'}`}>{l}</button>
        ))}
      </div>
      {mode === 'draw' ? (
        <>
          <canvas ref={cv} width={620} height={180}
            onPointerDown={start} onPointerMove={move} onPointerUp={end} onPointerLeave={end}
            className="w-full border border-[var(--leon-line)] rounded-lg bg-white touch-none cursor-crosshair" />
          <div className="flex items-center gap-2">
            <Button size="sm" variant="ghost" onClick={clear}>Clear</Button>
            <span className="text-[11px] text-[var(--leon-black)]/45">Draw with a mouse, trackpad, stylus or finger.</span>
          </div>
        </>
      ) : (
        <div>
          <TextInput value={typed} onChange={e => setTyped(e.target.value)} placeholder="Type your name" />
          <div className="mt-2 border border-[var(--leon-line)] rounded-lg bg-white px-4 py-6 text-center">
            <span style={{ fontFamily: "'Century Gothic Leon', cursive", fontSize: 30 }}>{typed || ' '}</span>
          </div>
        </div>
      )}
      <div className="flex justify-end gap-2">
        <Button variant="ghost" onClick={onCancel}>Cancel</Button>
        <Button disabled={mode === 'draw' ? !dirty : !typed.trim()}
          onClick={() => onDone(mode === 'draw'
            ? { image: cv.current.toDataURL('image/png') }
            : { typed: typed.trim() })}>
          Sign
        </Button>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// One envelope
// ---------------------------------------------------------------------------
function SignEnvelopeCard({ ctx, env, onOpen }) {
  const s = signEnvelopeSummary(env);
  const doc = (env.documents || [])[0];
  return (
    <button type="button" onClick={() => onOpen(env.id)}
      className="w-full text-left rounded-lg border border-[var(--leon-line)] bg-white p-3 hover:border-[var(--leon-brown)]">
      <div className="flex items-start gap-2">
        <div className="min-w-0 flex-1">
          <div className="flex items-center gap-2 flex-wrap">
            <span className="font-semibold text-sm truncate">{env.subject || doc?.name || env.number}</span>
            <SignStatusBadge env={env} />
            {env.weight === 'contract' && !s.binding && (
              <span className="px-1.5 py-0.5 rounded text-[10px] bg-[var(--leon-red)]/10 text-[var(--leon-red)] font-semibold">
                not a binding signature
              </span>
            )}
          </div>
          <div className="text-[11px] text-[var(--leon-black)]/55 mt-0.5">
            {env.number}
            {' · '}{s.signed} of {s.signers} signed
            {s.waitingOn.length ? ` · waiting on ${s.waitingOn.join(', ')}` : ''}
          </div>
        </div>
        <span className="text-[10px] text-[var(--leon-black)]/35 shrink-0">
          {(env.sentDate || env.createdDate || '').slice(0, 10)}
        </span>
      </div>
    </button>
  );
}

function SignEnvelopeDetail({ ctx, env, onBack }) {
  const [signing, setSigning] = useState(null);      // recipientId
  const [voiding, setVoiding] = useState(false);
  const [reason, setReason] = useState('');
  const s = signEnvelopeSummary(env);
  const blockers = signSendBlockers(env);
  const me = (env.recipients || []).find(r =>
    (r.userId && r.userId === ctx.currentUserId) ||
    (r.email && ctx.currentUser && r.email.toLowerCase() === String(ctx.currentUser.email || '').toLowerCase()));
  const myTurn = me && signWaitingOn(env).some(r => r.id === me.id);
  const editable = signEnvelopeStatus(env) === 'Draft';

  return (
    <div className="space-y-4">
      <div className="flex items-center gap-2 flex-wrap">
        <button onClick={onBack} className="text-xs font-semibold text-[var(--leon-brown)]">&larr; All envelopes</button>
        <span className="flex-1" />
        <SignStatusBadge env={env} />
        {editable && (
          <Button size="sm" onClick={() => ctx.sendSignEnvelope(env.id)} disabled={!!blockers.length}
            title={blockers.length ? blockers.join('\n') : 'Route it to the first recipient'}>
            Send for signature
          </Button>
        )}
        {signEnvelopeStatus(env) !== 'Voided' && signEnvelopeStatus(env) !== 'Completed' && !editable && (
          <Button size="sm" variant="ghost" onClick={() => setVoiding(true)}>Void</Button>
        )}
      </div>

      {!!blockers.length && editable && (
        <div className="rounded-lg border border-[var(--leon-yellow)]/50 bg-[var(--leon-yellow)]/10 p-3">
          <p className="text-xs font-semibold mb-1">Not ready to send</p>
          <ul className="text-[11px] text-[var(--leon-black)]/70 list-disc ml-4 space-y-0.5">
            {blockers.map((b, i) => <li key={i}>{b}</li>)}
          </ul>
        </div>
      )}

      {/* WHAT THIS SIGNATURE MEANS — stated on the envelope itself, not buried
          in a settings page, because it is the one thing a reader must not have
          to guess about. */}
      <div className={`rounded-lg p-3 text-xs ${env.weight === 'contract'
        ? 'border border-[var(--leon-red)]/30 bg-[var(--leon-red)]/[0.04]'
        : 'border border-[var(--leon-line)] bg-[var(--leon-cream)]/60'}`}>
        <strong>{(SIGN_WEIGHTS.find(w => w.key === env.weight) || {}).label}</strong>{' — '}
        {env.weight === 'contract' ? (
          <>this is prepared and tracked here, but a signature taken inside the Hub is
          <strong> not a binding e-signature</strong>: there is no server-side identity check and no
          independently timestamped certificate. Use it to get the document agreed and filed, and take
          the binding signature through a real e-signature service.</>
        ) : (
          <>a record of who approved this drawing and when, kept for our own file. That is what a shop
          drawing sign-off is. It is not a legal e-signature and does not claim to be.</>
        )}
      </div>

      <div className="grid gap-4 lg:grid-cols-3">
        <div className="lg:col-span-2 space-y-4">
          <Collapsible id={`sign-docs-${env.id}`} title="Documents" count={(env.documents || []).length} defaultOpen>
            {!(env.documents || []).length ? (
              <EmptyState text="No document on this envelope yet." />
            ) : (env.documents || []).map(d => (
              <div key={d.id} className="flex items-start gap-2 border border-[var(--leon-line)] rounded-lg p-2.5 mb-2 bg-white">
                <span className="text-lg">📄</span>
                <div className="min-w-0 flex-1">
                  <div className="text-sm font-semibold truncate">{d.name}</div>
                  {d.source && (
                    <div className="text-[11px] text-[var(--leon-black)]/50">
                      from {d.source.kind}{d.source.revision != null ? ` · Rev ${d.source.revision}` : ''}
                    </div>
                  )}
                  {/* The one real piece of tamper-evidence a browser can make. */}
                  {d.sha256 && (
                    <div className="text-[10px] text-[var(--leon-black)]/40 font-mono truncate mt-0.5"
                      title="SHA-256 of the bytes as sent. Proves the document has not changed since; it does not prove when it was signed.">
                      sha256 {d.sha256.slice(0, 32)}…
                    </div>
                  )}
                </div>
                {d.url && <AttachmentLink url={d.url} name={d.name} />}
              </div>
            ))}
          </Collapsible>

          <Collapsible id={`sign-rcp-${env.id}`} title="Recipients &amp; routing" count={(env.recipients || []).length} defaultOpen>
            <p className="text-[11px] text-[var(--leon-black)]/50 mb-2">
              Everyone on the same step is asked at once. A later step is not asked until the one
              before it is done.
            </p>
            {(env.recipients || []).slice()
              .sort((a, b) => (qnum(a.order) || 1) - (qnum(b.order) || 1))
              .map(r => {
                const waiting = signWaitingOn(env).some(x => x.id === r.id);
                return (
                  <div key={r.id} className={`flex items-center gap-2 border rounded-lg p-2 mb-1.5 ${waiting
                    ? 'border-[var(--leon-brown)]/50 bg-[var(--leon-brown)]/[0.05]' : 'border-[var(--leon-line)] bg-white'}`}>
                    <span className="w-6 h-6 rounded-full bg-[var(--leon-cream)] text-[11px] font-bold flex items-center justify-center shrink-0">
                      {qnum(r.order) || 1}
                    </span>
                    <div className="min-w-0 flex-1">
                      <div className="text-sm font-semibold truncate">{r.name || <span className="text-[var(--leon-red)]">unnamed</span>}</div>
                      <div className="text-[11px] text-[var(--leon-black)]/50 truncate">
                        {r.email} · {(SIGN_RECIPIENT_ROLES.find(x => x.key === r.role) || {}).label}
                        {r.party === 'internal' ? ' · LEON' : ''}
                      </div>
                    </div>
                    <span className="text-[11px] text-[var(--leon-black)]/55 shrink-0">{r.status}</span>
                    {r.signatureImage && <img src={r.signatureImage} alt="" className="h-7 shrink-0" />}
                    {r.signatureTyped && !r.signatureImage && (
                      <span className="shrink-0" style={{ fontFamily: "'Century Gothic Leon', cursive" }}>{r.signatureTyped}</span>
                    )}
                    {waiting && <span className="text-[10px] font-semibold text-[var(--leon-brown)] shrink-0">their turn</span>}
                  </div>
                );
              })}
          </Collapsible>

          {myTurn && (
            <div className="rounded-lg border-2 border-[var(--leon-brown)] bg-white p-3">
              <p className="text-sm font-semibold mb-2">This is with you to sign.</p>
              {signing === me.id ? (
                <SignPad name={me.name} onCancel={() => setSigning(null)}
                  onDone={mark => { ctx.recordSignSignature(env.id, me.id, mark); setSigning(null); }} />
              ) : (
                <div className="flex gap-2">
                  <Button onClick={() => setSigning(me.id)}>Sign it</Button>
                  <Button variant="ghost" onClick={() => {
                    const why = prompt('Why are you declining?');
                    if (why !== null) ctx.declineSignEnvelope(env.id, me.id, why);
                  }}>Decline</Button>
                </div>
              )}
            </div>
          )}
        </div>

        {/* THE AUDIT TRAIL. Every consequential act, newest first, with the
            limit of a browser-made trail stated under it rather than implied. */}
        <div>
          <div className="rounded-lg border border-[var(--leon-line)] bg-white p-3">
            <div className="text-[11px] font-bold uppercase tracking-[0.14em] text-[var(--leon-black)]/45 mb-2">
              History
            </div>
            <div className="space-y-2 max-h-[420px] overflow-y-auto">
              {(env.events || []).map(e => (
                <div key={e.id} className="text-[11px] border-b border-[var(--leon-line)] pb-1.5 last:border-0">
                  <div className="font-semibold">{e.detail}</div>
                  <div className="text-[var(--leon-black)]/45">
                    {String(e.date).slice(0, 16).replace('T', ' ')}{e.by ? ` · ${e.by}` : ''}
                  </div>
                </div>
              ))}
            </div>
            <p className="text-[10px] text-[var(--leon-black)]/40 mt-2 pt-2 border-t border-[var(--leon-line)]">
              Times come from the signer&rsquo;s own computer and nothing here is witnessed by a third
              party. The document hash is real evidence that the bytes have not changed; it is not
              evidence of when.
            </p>
          </div>
        </div>
      </div>

      <Modal open={voiding} onClose={() => setVoiding(false)} title="Void this envelope">
        <p className="text-xs text-[var(--leon-black)]/60 mb-2">
          Voiding stops it. Nothing is deleted &mdash; the envelope and its history stay as the record
          of what happened.
        </p>
        <Field label="Why"><TextInput value={reason} onChange={e => setReason(e.target.value)} /></Field>
        <div className="flex justify-end gap-2 mt-3">
          <Button variant="ghost" onClick={() => setVoiding(false)}>Cancel</Button>
          <Button onClick={() => { ctx.voidSignEnvelope(env.id, reason); setVoiding(false); setReason(''); }}>Void it</Button>
        </div>
      </Modal>
    </div>
  );
}

// ---------------------------------------------------------------------------
// The module
// ---------------------------------------------------------------------------
function SignSoftware({ ctx }) {
  const boot = typeof swBootParam === 'function' ? swBootParam('section') : '';
  const [section, setSection] = useState(SIGN_SECTIONS.some(s => s.key === boot) ? boot : 'inbox');
  const [openId, setOpenId] = useState(typeof swBootParam === 'function' ? swBootParam('envelope') : '');
  const envelopes = ctx.signEnvelopes || [];
  const open = envelopes.find(e => e.id === openId) || null;
  const mine = signMyQueue(envelopes, ctx.currentUserId, ctx.currentUser && ctx.currentUser.email);

  const status = `${envelopes.length} envelope${envelopes.length === 1 ? '' : 's'} · `
    + `${envelopes.filter(e => SIGN_OPEN_STATUSES.indexOf(signEnvelopeStatus(e)) >= 0).length} open · `
    + `${mine.length} waiting on you`;

  return (
    <SoftwareRail swKey="sign" sections={SIGN_SECTIONS} active={section} onSelect={setSection} status={status}>
      {open ? (
        <SignEnvelopeDetail ctx={ctx} env={open} onBack={() => setOpenId('')} />
      ) : section === 'inbox' ? (
        !mine.length ? (
          <EmptyState text="Nothing is waiting on your signature." />
        ) : (
          <div className="space-y-2">
            {mine.map(({ env }) => <SignEnvelopeCard key={env.id} ctx={ctx} env={env} onOpen={setOpenId} />)}
          </div>
        )
      ) : section === 'all' ? (
        !envelopes.length ? (
          <EmptyState text="No envelopes yet. Send one from a shop drawing, a submittal or a quotation — or start one here." />
        ) : (
          <div className="space-y-2">
            {envelopes.map(env => <SignEnvelopeCard key={env.id} ctx={ctx} env={env} onOpen={setOpenId} />)}
          </div>
        )
      ) : section === 'compose' ? (
        <SignComposePanel ctx={ctx} onCreated={setOpenId} />
      ) : (
        <SignAboutPanel />
      )}
    </SoftwareRail>
  );
}

function SignAboutPanel() {
  return (
    <div className="max-w-3xl space-y-3 text-sm">
      <h3 className="font-bold">What a signature here does and does not mean</h3>
      <p className="text-[var(--leon-black)]/70">
        LEON Sign owns the whole workflow around a signature: it takes a document this Hub already
        produced, names it to one standard, routes it to the people who have to see it in the right
        order, tracks who is holding it up, and files the executed copy back on the job it came from.
      </p>
      <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)]/60 p-3">
        <p className="font-semibold mb-1">Approval records — what most of this is</p>
        <p className="text-[var(--leon-black)]/70 text-xs">
          Seventeen of the twenty-three envelopes sent from this company in the last seven months were
          shop drawing approvals. An approval is a record that a named person agreed a drawing at a
          revision, on a date. LEON Sign produces exactly that, and it is genuinely useful.
        </p>
      </div>
      <div className="rounded-lg border border-[var(--leon-red)]/30 bg-[var(--leon-red)]/[0.04] p-3">
        <p className="font-semibold mb-1">Contracts — what this cannot do</p>
        <p className="text-[var(--leon-black)]/70 text-xs">
          A binding e-signature needs three things this app has no way to provide: identity checked by
          a server rather than by asking, a tamper-evident chain nobody can rewrite, and a completion
          certificate timestamped by a party with no stake in the document. The Hub runs entirely in
          your browser, so it can offer none of them. Supply agreements, sales contracts and NDAs
          should still go through a real e-signature service — prepare and track them here, and
          sign them there.
        </p>
      </div>
      <p className="text-[var(--leon-black)]/70 text-xs">
        The one real piece of evidence produced here is a <strong>SHA-256 hash</strong> of each
        document, taken at the moment it is sent. That proves the bytes have not changed since. It
        does not prove <em>when</em> anything happened — every time recorded comes from the
        signer&rsquo;s own computer clock.
      </p>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Composing — the loop this exists to close
// ---------------------------------------------------------------------------
// The point is NOT a file picker. Every document Leon sends for signature is
// already produced by this Hub, so the compose step offers the job's own
// submittal revisions, drawings and quotations, and takes the name, the
// revision and the scope from the record rather than asking anyone to retype
// them. That is what removes the download-rename-upload-refile round trip.
function SignComposePanel({ ctx, onCreated }) {
  const projects = typeof ctx.toolProjects === 'function' ? ctx.toolProjects() : (ctx.projects || []);
  const [projectId, setProjectId] = useState((projects[0] || {}).id || '');
  const project = projects.find(p => p.id === projectId) || null;
  const [weight, setWeight] = useState('approval');
  const [picked, setPicked] = useState(null);      // { kind, name, url, scopeId, revision, refId }
  const [subject, setSubject] = useState('');
  const [message, setMessage] = useState('');
  const [rows, setRows] = useState([
    { id: 'r1', name: '', email: '', role: 'signer', order: 1, party: 'external' },
    { id: 'r2', name: '', email: '', role: 'signer', order: 2, party: 'internal' },
  ]);

  // Everything on this job that is worth signing, read from the records that
  // already hold it.
  const sources = useMemo(() => {
    if (!project) return [];
    const out = [];
    (project.scopes || []).forEach(sc => {
      (sc.submittals || []).forEach(th => {
        (th.revisions || []).forEach(rv => {
          if (!rv.fileUrl) return;
          out.push({ kind: 'Submittal revision', scopeId: sc.id, scopeName: sc.name,
            refId: rv.id, revision: rv.revisionNumber != null ? rv.revisionNumber : rv.number,
            name: rv.fileName || th.title || 'Revision', url: rv.fileUrl, title: th.title });
        });
      });
      (sc.documents || []).forEach(d => {
        if (d.fileUrl) out.push({ kind: 'Scope document', scopeId: sc.id, scopeName: sc.name,
          refId: d.id, revision: null, name: d.fileName || d.name || 'Document', url: d.fileUrl });
      });
    });
    (project.drawingSets || []).forEach(ds => {
      if (ds.fileUrl) out.push({ kind: 'Drawing set', scopeId: null, refId: ds.id,
        revision: ds.revision != null ? ds.revision : null, name: ds.fileName || ds.name || 'Drawing set', url: ds.fileUrl });
    });
    return out;
  }, [project]);

  // ONE generated name. Their own account runs three conventions because a
  // person types it each time; the Hub knows every part of it.
  const generatedName = picked && project
    ? signDocumentName({ project: project.name, scope: picked.scopeName || '',
        kind: picked.title || picked.kind, revision: picked.revision })
    : '';

  const setRow = (id, f) => setRows(rs => rs.map(r => r.id === id ? Object.assign({}, r, f) : r));
  const people = (ctx.teamDirectory || []).filter(p => p.active !== false);

  const create = () => {
    if (!picked) { alert('Pick the document to send first.'); return; }
    const id = ctx.addSignEnvelope({
      subject: subject || generatedName, message, weight,
      projectId: project ? project.id : null, scopeId: picked.scopeId || null,
      source: { kind: picked.kind, refId: picked.refId, revision: picked.revision },
      documents: [makeSignDocument({ name: generatedName || picked.name, url: picked.url,
        source: { kind: picked.kind, projectId: project ? project.id : null,
                  scopeId: picked.scopeId || null, refId: picked.refId, revision: picked.revision } })],
      recipients: rows.filter(r => r.name.trim() || r.email.trim()).map(r => makeSignRecipient(r)),
    });
    if (id && onCreated) onCreated(id);
  };

  return (
    <div className="max-w-4xl space-y-4">
      <div className="grid sm:grid-cols-2 gap-3">
        <Field label="Job">
          <Select value={projectId} onChange={e => { setProjectId(e.target.value); setPicked(null); }}>
            {projects.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </Select>
        </Field>
        <Field label="What kind of signature this is"
          hint={(SIGN_WEIGHTS.find(w => w.key === weight) || {}).hint}>
          <Select value={weight} onChange={e => setWeight(e.target.value)}>
            {SIGN_WEIGHTS.map(w => <option key={w.key} value={w.key}>{w.label}</option>)}
          </Select>
        </Field>
      </div>

      <Collapsible id="sign-pick-doc" title="1 · The document" count={sources.length} defaultOpen>
        {!sources.length ? (
          <p className="text-xs text-[var(--leon-black)]/55">
            Nothing on this job carries a file yet. A submittal revision, a scope document or a
            drawing set with a file attached is what gets sent.
          </p>
        ) : (
          <div className="space-y-1 max-h-64 overflow-y-auto">
            {sources.map((sdoc, i) => (
              <button key={i} type="button" onClick={() => setPicked(sdoc)}
                className={`w-full text-left px-2.5 py-1.5 rounded border text-xs ${picked === sdoc
                  ? 'border-[var(--leon-brown)] bg-[var(--leon-brown)]/[0.06]' : 'border-[var(--leon-line)] bg-white'}`}>
                <span className="font-semibold">{sdoc.name}</span>
                <span className="text-[var(--leon-black)]/45">
                  {' · '}{sdoc.kind}{sdoc.scopeName ? ` · ${sdoc.scopeName}` : ''}
                  {sdoc.revision != null ? ` · Rev ${sdoc.revision}` : ''}
                </span>
              </button>
            ))}
          </div>
        )}
        {picked && (
          <div className="mt-3 rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)]/60 p-2.5">
            <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">It will be sent as</div>
            <div className="text-sm font-semibold">{generatedName}</div>
            <p className="text-[10px] text-[var(--leon-black)]/45 mt-1">
              Generated from the job, the scope, the document and its revision &mdash; so every
              envelope is named the same way instead of three ways.
            </p>
          </div>
        )}
      </Collapsible>

      <Collapsible id="sign-pick-rcp" title="2 · Who signs, and in what order" defaultOpen>
        <p className="text-[11px] text-[var(--leon-black)]/50 mb-2">
          Leon&rsquo;s own envelopes go to the outside party first and a LEON counter-signer second.
          That is what these two steps are; add more if the job needs them.
        </p>
        {rows.map(r => (
          <div key={r.id} className="grid sm:grid-cols-12 gap-2 mb-2 items-end">
            <Field label="Step" className="sm:col-span-1">
              <QNum w="w-14" value={r.order} onChange={v => setRow(r.id, { order: v })} />
            </Field>
            <Field label="Name" className="sm:col-span-3">
              <TextInput value={r.name} onChange={e => setRow(r.id, { name: e.target.value })} />
            </Field>
            <Field label="Email" className="sm:col-span-4">
              <TextInput value={r.email} onChange={e => setRow(r.id, { email: e.target.value })} />
            </Field>
            <Field label="Doing what" className="sm:col-span-2">
              <Select value={r.role} onChange={e => setRow(r.id, { role: e.target.value })}>
                {SIGN_RECIPIENT_ROLES.map(x => <option key={x.key} value={x.key}>{x.label}</option>)}
              </Select>
            </Field>
            <Field label="Who" className="sm:col-span-2">
              <Select value={r.userId || ''} onChange={e => {
                const p = people.find(x => x.id === e.target.value);
                setRow(r.id, p ? { userId: p.id, name: p.name, email: p.email || '', party: 'internal' }
                               : { userId: null, party: 'external' });
              }}>
                <option value="">Outside party</option>
                {people.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
              </Select>
            </Field>
          </div>
        ))}
        <Button size="sm" variant="ghost"
          onClick={() => setRows(rs => rs.concat([{ id: uid('r'), name: '', email: '', role: 'copy',
            order: Math.max(...rs.map(x => qnum(x.order) || 1)) + 1, party: 'external' }]))}>
          + Add a recipient
        </Button>
      </Collapsible>

      <Collapsible id="sign-msg" title="3 · What to say" defaultOpen>
        <Field label="Subject" hint="Left blank, the generated name is used.">
          <TextInput value={subject} onChange={e => setSubject(e.target.value)} placeholder={generatedName} />
        </Field>
        <Field label="Message">
          <TextArea rows={3} value={message} onChange={e => setMessage(e.target.value)} />
        </Field>
      </Collapsible>

      <div className="flex justify-end">
        <Button onClick={create} disabled={!picked}>Create the envelope</Button>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Send for signature — the button, wherever a signable document lives
// ---------------------------------------------------------------------------
// One component for every mount point, so a shop drawing, a submittal revision,
// a quotation and a contract all behave identically and there is one place to
// change what sending means. It takes the SOURCE record rather than a file, so
// the envelope keeps a pointer back to what it came from and the executed copy
// can be filed there without anyone choosing a destination twice.
//
// It renders the envelope's own state when one already exists, because the
// second question anyone asks about a document they sent is "where is it", and
// making them go and look somewhere else for that is how the loop stays open.
function SignSendButton({ ctx, source, label, size }) {
  const [open, setOpen] = useState(false);
  const envs = (ctx.signEnvelopes || []).filter(e =>
    e.source && source && e.source.refId && e.source.refId === source.refId);
  const latest = envs[0] || null;
  if (!source || !source.url) return null;
  if (latest) {
    const s = signEnvelopeSummary(latest);
    return (
      <button type="button" title={s.waitingOn.length ? `Waiting on ${s.waitingOn.join(', ')}` : latest.number}
        onClick={() => ctx.goSoftware && ctx.goSoftware('sign', { envelope: latest.id })}
        className={`px-2 py-0.5 rounded text-[11px] font-semibold ${signStatusTone(s.status)}`}>
        ✍️ {s.status}{s.signers ? ` ${s.signed}/${s.signers}` : ''}
      </button>
    );
  }
  return (
    <>
      <Button size={size || 'sm'} variant="ghost" onClick={() => setOpen(true)}
        title="Route this document for signature in LEON Sign">
        ✍️ {label || 'Send for signature'}
      </Button>
      <SignQuickSendModal ctx={ctx} open={open} onClose={() => setOpen(false)} source={source} />
    </>
  );
}

// The short path: the document is already chosen, so the only questions left
// are who signs it and what kind of signature this is.
function SignQuickSendModal({ ctx, open, onClose, source }) {
  const [weight, setWeight] = useState(source && source.weight ? source.weight : 'approval');
  const [rows, setRows] = useState([
    { id: 'q1', name: '', email: '', role: 'signer', order: 1, party: 'external' },
    { id: 'q2', name: '', email: '', role: 'signer', order: 2, party: 'internal' },
  ]);
  const people = (ctx.teamDirectory || []).filter(p => p.active !== false);
  const name = signDocumentName({
    project: source && source.projectName, scope: source && source.scopeName,
    kind: source && source.kind, revision: source && source.revision,
  });
  const setRow = (id, f) => setRows(rs => rs.map(r => r.id === id ? Object.assign({}, r, f) : r));
  const go = () => {
    const recips = rows.filter(r => r.name.trim() || r.email.trim());
    if (!recips.length) { alert('Add at least one recipient.'); return; }
    const id = ctx.addSignEnvelope({
      subject: name, weight,
      projectId: source.projectId || null, scopeId: source.scopeId || null,
      source: { kind: source.kind, refId: source.refId, revision: source.revision },
      documents: [makeSignDocument({ name, url: source.url,
        source: { kind: source.kind, projectId: source.projectId || null,
                  scopeId: source.scopeId || null, refId: source.refId, revision: source.revision } })],
      recipients: recips.map(makeSignRecipient),
    });
    onClose();
    if (id && ctx.goSoftware) ctx.goSoftware('sign', { envelope: id });
  };
  return (
    <Modal open={open} onClose={onClose} size="lg" title="Send for signature">
      <div className="rounded-lg border border-[var(--leon-line)] bg-[var(--leon-cream)]/60 p-2.5 mb-3">
        <div className="text-[10px] uppercase tracking-wide text-[var(--leon-black)]/45">It will be sent as</div>
        <div className="text-sm font-semibold">{name}</div>
      </div>
      <Field label="What kind of signature this is"
        hint={(SIGN_WEIGHTS.find(w => w.key === weight) || {}).hint}>
        <Select value={weight} onChange={e => setWeight(e.target.value)}>
          {SIGN_WEIGHTS.map(w => <option key={w.key} value={w.key}>{w.label}</option>)}
        </Select>
      </Field>
      {weight === 'contract' && (
        <p className="text-[11px] text-[var(--leon-red)] mt-1 mb-2">
          A signature taken inside the Hub is not legally binding. Prepare and track it here, and take
          the binding signature through a real e-signature service.
        </p>
      )}
      <p className="text-[11px] text-[var(--leon-black)]/50 mt-3 mb-1">
        The outside party first, then a LEON counter-signer &mdash; which is how every envelope this
        company has sent is routed.
      </p>
      {rows.map(r => (
        <div key={r.id} className="grid sm:grid-cols-12 gap-2 mb-2 items-end">
          <Field label="Step" className="sm:col-span-1">
            <QNum w="w-14" value={r.order} onChange={v => setRow(r.id, { order: v })} />
          </Field>
          <Field label="Name" className="sm:col-span-3">
            <TextInput value={r.name} onChange={e => setRow(r.id, { name: e.target.value })} />
          </Field>
          <Field label="Email" className="sm:col-span-4">
            <TextInput value={r.email} onChange={e => setRow(r.id, { email: e.target.value })} />
          </Field>
          <Field label="Doing what" className="sm:col-span-2">
            <Select value={r.role} onChange={e => setRow(r.id, { role: e.target.value })}>
              {SIGN_RECIPIENT_ROLES.map(x => <option key={x.key} value={x.key}>{x.label}</option>)}
            </Select>
          </Field>
          <Field label="Who" className="sm:col-span-2">
            <Select value={r.userId || ''} onChange={e => {
              const p = people.find(x => x.id === e.target.value);
              setRow(r.id, p ? { userId: p.id, name: p.name, email: p.email || '', party: 'internal' }
                             : { userId: null, party: 'external' });
            }}>
              <option value="">Outside party</option>
              {people.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </Select>
          </Field>
        </div>
      ))}
      <div className="flex justify-end gap-2 mt-3">
        <Button variant="ghost" onClick={onClose}>Cancel</Button>
        <Button onClick={go}>Create the envelope</Button>
      </div>
    </Modal>
  );
}
