// Chart Growth Hub — digital agreement gate.
//
// Renders one document from portal/agreements.js, requires a tick on every
// clause individually plus the member's typed full name, then files an
// immutable acceptance row. The row is the evidence; n8n turns it into a PDF
// on Drive and emails hq@.
//
// Per-clause ticks rather than one master tick is deliberate. A single "I agree"
// box is trivially disputed; a record showing the member ticked "I understand a
// stop-loss is not guaranteed" is not.
//
// Exposes:
//   window.AgreementGate({client, session, member, formKey, onDone, onCancel, embedded})
//   window.cgAgreementStatus(client, studentId)  -> {community:'1.0'|null, ...}
//   window.cgAgreementNeeded(status, formKey)    -> bool
(function(){
const { useState, useEffect, useRef, useMemo } = React;

/* ── which versions has this member accepted? ────────────────────────────── */
// Returns the highest accepted version per form. Rows are immutable and
// append-only, so "latest accepted" is simply the most recent row per key.
// Returns, per form: the version currently in force for this member, plus any
// hq@ waiver. Three things can change the answer beyond a plain signature:
//   · a VOID cancels one specific acceptance record
//   · a REQUIRE invalidates everything signed before it (forced re-sign)
//   · a WAIVE removes the requirement entirely until a later REQUIRE
// Overrides are separate append-only facts, so none of this edits the archive.
async function agreementStatus(client, studentId){
  const out = { community:null, vip_signals:null, bot:null, waived:{}, records:{} };

  const [accRes, ovrRes] = await Promise.all([
    client.from('agreement_acceptances')
      .select('*').eq('student_id', studentId).order('accepted_at', {ascending:false}),
    client.from('agreement_overrides')
      .select('*').eq('student_id', studentId).order('created_at', {ascending:false}),
  ]);
  const acc = (accRes && accRes.data) || [];
  const ovr = (ovrRes && ovrRes.data) || [];   // absent or blocked -> behave as none

  const voided = {};
  ovr.forEach(o => { if(o.action === 'void' && o.target_id) voided[o.target_id] = o; });

  // Latest waive/require per form decides the requirement; rows arrive newest
  // first, so the first one seen for a form is the one that stands.
  const stance = {};
  ovr.forEach(o => {
    if(o.action === 'void') return;
    if(!stance[o.form_key]) stance[o.form_key] = o;
  });

  acc.forEach(r => {
    if(voided[r.id]) return;
    const st = stance[r.form_key];
    // A forced re-sign only invalidates signatures made before it was issued.
    if(st && st.action === 'require' && String(r.accepted_at) < String(st.created_at)) return;
    if(out.records[r.form_key]) return;        // newest valid wins
    out.records[r.form_key] = r;
    out[r.form_key] = r.version;
  });

  Object.keys(stance).forEach(k => {
    if(stance[k].action === 'waive') out.waived[k] = stance[k];
  });

  out.voided = voided;
  out.overrides = ovr;
  return out;
}

// A member needs the form unless hq@ has waived it, or they already hold the
// current version.
function agreementNeeded(status, formKey){
  const A = window.CG_AGREEMENTS;
  if(!A || !A.docs[formKey]) return false;
  if(status && status.waived && status.waived[formKey]) return false;
  const have = status ? status[formKey] : null;
  return have !== A.docs[formKey].version;
}

/* ── styles ──────────────────────────────────────────────────────────────── */
(function injectCSS(){
  if(document.getElementById('cg-agree-css')) return;
  const s = document.createElement('style');
  s.id = 'cg-agree-css';
  s.textContent = `
  .ag-wrap{border:1px solid var(--border-gold);border-radius:var(--radius);
    background:linear-gradient(165deg,rgba(212,175,55,.05),transparent 55%),var(--navy-950);overflow:hidden}
  .ag-head{padding:20px 24px;border-bottom:1px solid var(--border);display:flex;gap:14px;align-items:flex-start}
  .ag-head img{width:40px;height:40px;object-fit:contain;flex-shrink:0}
  .ag-head h2{margin:0;font-size:1.16rem;line-height:1.3}
  .ag-meta{font-family:var(--font-mono);font-size:.68rem;color:var(--ink-faint);margin-top:4px;
    letter-spacing:.03em}
  .ag-body{max-height:340px;overflow-y:auto;padding:20px 24px;border-bottom:1px solid var(--border);
    scroll-behavior:smooth}
  .ag-body h3{font-size:.87rem;margin:18px 0 6px;color:var(--gold-2)}
  .ag-body h3:first-child{margin-top:0}
  .ag-body p{font-size:.85rem;line-height:1.62;color:var(--ink-dim);margin:0 0 9px}
  .ag-intro{font-size:.87rem;line-height:1.6;color:var(--ink);margin:0 0 4px}
  .ag-scrollnote{padding:9px 24px;font-size:.73rem;color:var(--ink-faint);
    border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
  .ag-clauses{padding:18px 24px;display:grid;gap:2px}
  .ag-clause{display:flex;gap:11px;align-items:flex-start;padding:10px 12px;border-radius:9px;
    cursor:pointer;border:1px solid transparent;transition:background .15s,border-color .15s}
  .ag-clause:hover{background:var(--surface)}
  .ag-clause.on{border-color:rgba(74,222,128,.28);background:rgba(74,222,128,.05)}
  .ag-clause.miss{border-color:rgba(248,113,113,.45);background:rgba(248,113,113,.06)}
  .ag-box{width:19px;height:19px;flex-shrink:0;margin-top:1px;border-radius:5px;
    border:1.5px solid var(--border);display:grid;place-items:center;transition:.15s}
  .ag-clause.on .ag-box{border-color:var(--green);background:var(--green)}
  .ag-box svg{width:12px;height:12px;fill:none;stroke:var(--navy-950);stroke-width:2.6;
    stroke-linecap:round;stroke-linejoin:round;opacity:0}
  .ag-clause.on .ag-box svg{opacity:1}
  .ag-clause span{font-size:.83rem;line-height:1.5;color:var(--ink-dim)}
  .ag-clause.on span{color:var(--ink)}
  .ag-sign{padding:4px 24px 22px}
  .ag-sign label{display:block;font-size:.73rem;text-transform:uppercase;letter-spacing:.06em;
    color:var(--ink-faint);margin-bottom:6px}
  .ag-sign input{width:100%;padding:13px 15px;border-radius:var(--radius-sm);background:var(--navy-950);
    border:1px solid var(--border);color:var(--ink);font-family:var(--font-body);font-size:1.02rem;
    letter-spacing:.02em}
  .ag-sign input:focus{outline:none;border-color:var(--gold);box-shadow:0 0 0 3px var(--gold-soft)}
  .ag-hint{font-size:.73rem;color:var(--ink-faint);margin-top:7px}
  .ag-siglabel{display:block;font-size:.73rem;text-transform:uppercase;letter-spacing:.06em;
    color:var(--ink-faint);margin-bottom:6px}
  .ag-sigbox{position:relative;border:1px solid var(--border);border-radius:var(--radius-sm);
    background:var(--navy-950);height:150px;overflow:hidden}
  .ag-sigbox:focus-within{border-color:var(--gold)}
  /* touch-action:none stops the browser scrolling the page instead of drawing */
  .ag-sigcanvas{position:absolute;inset:0;width:100%;height:100%;touch-action:none;cursor:crosshair;
    display:block}
  .ag-sigline{position:absolute;left:22px;right:22px;bottom:34px;border-bottom:1px dashed var(--border);
    pointer-events:none}
  .ag-sigclear{position:absolute;right:9px;bottom:8px;background:var(--surface);border:1px solid var(--border);
    color:var(--ink-faint);border-radius:7px;padding:4px 10px;font-size:.7rem;cursor:pointer;
    font-family:inherit}
  .ag-sigclear:hover{border-color:var(--border-gold);color:var(--gold-2)}
  .ag-foot{padding:0 24px 22px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
  .ag-err{color:var(--red);font-size:.8rem;font-family:var(--font-mono);padding:0 24px 14px}
  .ag-modal{position:fixed;inset:0;z-index:400;background:rgba(4,7,14,.88);backdrop-filter:blur(6px);
    display:grid;place-items:center;padding:20px;overflow-y:auto}
  .ag-modal>div{width:100%;max-width:720px;margin:auto}
  .ag-progress{max-width:720px;margin:0 auto 12px;padding:14px 18px;border-radius:12px;
    border:1px solid var(--border);background:var(--navy-900)}
  .ag-progress-top{display:flex;justify-content:space-between;align-items:center;
    font-size:.86rem;margin-bottom:9px}
  .ag-progress-top span{font-family:var(--font-mono);font-size:.74rem;color:var(--ink-faint)}
  .ag-progress-bar{height:5px;border-radius:999px;background:var(--border);overflow:hidden}
  .ag-progress-bar>div{height:100%;border-radius:999px;transition:width .35s;
    background:linear-gradient(90deg,var(--gold),var(--gold-2))}
  .ag-progress-list{display:flex;gap:7px;flex-wrap:wrap;margin-top:10px}
  .ag-step{font-size:.7rem;padding:3px 9px;border-radius:999px;border:1px solid var(--border);
    color:var(--ink-faint)}
  .ag-step.done{color:var(--green);border-color:rgba(74,222,128,.35)}
  .ag-step.now{color:var(--gold-2);border-color:var(--border-gold)}
  @media(max-width:600px){
    .ag-head,.ag-body,.ag-clauses,.ag-sign,.ag-foot{padding-left:16px;padding-right:16px}
    .ag-body{max-height:44vh}
  }`;
  document.head.appendChild(s);
})();

const TICK = <svg viewBox="0 0 24 24"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>;

/* ── name matching ───────────────────────────────────────────────────────── */
// The typed name must match the registered name, but not so strictly that a
// middle initial or a double space blocks a genuine member. Compare on letters
// only, case- and accent-insensitive.
function normName(s){
  return String(s||'')
    .normalize('NFD').replace(/[̀-ͯ]/g,'')
    .toLowerCase().replace(/[^a-z]/g,'');
}
function nameMatches(typed, registered){
  const t = normName(typed), r = normName(registered);
  if(!t) return false;
  if(!r) return t.length >= 4;          // no name on file — just require something real
  if(t === r) return true;
  // Allow a middle name on one side but not on the other.
  return r.indexOf(t) === 0 || t.indexOf(r) === 0;
}

/* ── signature pad ───────────────────────────────────────────────────────
   A drawn signature or initials. Pointer events cover mouse, pen and touch in
   one code path; the canvas is scaled by devicePixelRatio so the stroke is not
   a blurry mess on a phone. */
function SignaturePad({onChange, label}){
  const ref = useRef(null);
  const drawing = useRef(false);
  const dirty = useRef(false);

  useEffect(()=>{
    const c = ref.current; if(!c) return;
    const fit = () => {
      const r = c.getBoundingClientRect();
      const dpr = window.devicePixelRatio || 1;
      // Resizing a canvas clears it, so only do it when the size really changed.
      if(c.width === Math.round(r.width*dpr) && c.height === Math.round(r.height*dpr)) return;
      c.width = Math.round(r.width*dpr); c.height = Math.round(r.height*dpr);
      const ctx = c.getContext('2d');
      ctx.scale(dpr, dpr);
      ctx.lineWidth = 2.2; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
      ctx.strokeStyle = '#e8dcc0';
    };
    fit();
    window.addEventListener('resize', fit);
    return ()=>window.removeEventListener('resize', fit);
  },[]);

  function pos(e){
    const r = ref.current.getBoundingClientRect();
    return { x: e.clientX - r.left, y: e.clientY - r.top };
  }
  function start(e){
    e.preventDefault();
    const c = ref.current, ctx = c.getContext('2d'), p = pos(e);
    drawing.current = true;
    ctx.beginPath(); ctx.moveTo(p.x, p.y);
    try { c.setPointerCapture(e.pointerId); } catch(err){}
  }
  function move(e){
    if(!drawing.current) return;
    e.preventDefault();
    const ctx = ref.current.getContext('2d'), p = pos(e);
    ctx.lineTo(p.x, p.y); ctx.stroke();
    dirty.current = true;
  }
  function end(){
    if(!drawing.current) return;
    drawing.current = false;
    if(dirty.current && onChange){
      try { onChange(ref.current.toDataURL('image/png')); } catch(e){ onChange(null); }
    }
  }
  function clear(){
    const c = ref.current, ctx = c.getContext('2d');
    ctx.clearRect(0, 0, c.width, c.height);
    dirty.current = false;
    if(onChange) onChange(null);
  }

  return <div>
    <label className="ag-siglabel">{label || 'Sign or initial below'}</label>
    <div className="ag-sigbox">
      <canvas ref={ref} className="ag-sigcanvas"
        onPointerDown={start} onPointerMove={move}
        onPointerUp={end} onPointerLeave={end} onPointerCancel={end}/>
      <div className="ag-sigline"/>
      <button type="button" className="ag-sigclear" onClick={clear}>Clear</button>
    </div>
  </div>;
}

/* ── the gate ────────────────────────────────────────────────────────────── */
function AgreementGate({client, session, member, formKey, onDone, onCancel, embedded}){
  const A = window.CG_AGREEMENTS;
  const doc = A && A.docs[formKey];

  const clauses = useMemo(()=> doc ? A.allClauses(doc) : [], [doc]);
  const [ticks,setTicks] = useState({});
  const [typed,setTyped] = useState('');
  const [busy,setBusy]   = useState(false);
  const [err,setErr]     = useState('');
  const [showMissing,setShowMissing] = useState(false);
  const [readToEnd,setReadToEnd]     = useState(false);
  const [sig,setSig]     = useState(null);   // drawn signature, data URL
  const [dob,setDob]     = useState(null);   // date of birth, from member_private
  const bodyRef = useRef(null);

  // The birthdate is copied onto the acceptance so the 18+ declaration is
  // backed by a date rather than by a tick alone. It lives in member_private,
  // which staff cannot read, so it comes through a security-definer function
  // scoped to the signed-in member.
  useEffect(()=>{ let stop=false;
    (async ()=>{
      try{
        const { data } = await client.rpc('my_birthdate');
        if(!stop && data) setDob(String(data).slice(0,10));
      }catch(e){ /* not set yet — the form still works without it */ }
    })();
    return ()=>{ stop=true; };
    /* eslint-disable-next-line */
  },[]);

  // A member cannot tick clauses they have not scrolled past. Not legally
  // decisive on its own, but it removes the "I never saw it" argument and it
  // costs the honest member two seconds.
  useEffect(()=>{
    const el = bodyRef.current;
    if(!el) return;
    const check = () => {
      // If the document is shorter than the box there is nothing to scroll.
      if(el.scrollHeight - el.clientHeight <= 8){ setReadToEnd(true); return; }
      if(el.scrollTop + el.clientHeight >= el.scrollHeight - 24) setReadToEnd(true);
    };
    check();
    el.addEventListener('scroll', check);
    return ()=>el.removeEventListener('scroll', check);
  },[doc]);

  if(!doc) return <div style={{color:'var(--red)',fontSize:'.85rem'}}>
    Agreement “{formKey}” is not available. Please refresh the page.</div>;

  const allTicked = clauses.every(c => ticks[c.id]);
  const nameOk = nameMatches(typed, member && member.full_name);
  const ready = allTicked && nameOk && readToEnd && !!sig;

  function toggle(id){
    setTicks(t => { const n = {...t}; if(n[id]) delete n[id]; else n[id] = true; return n; });
  }

  async function submit(){
    if(!ready){
      setShowMissing(true);
      if(!readToEnd)      setErr('Please scroll to the end of the agreement before accepting.');
      else if(!allTicked) setErr('Please confirm every point — each one must be ticked individually.');
      else if(!nameOk)    setErr('Type your full registered name exactly as it appears above.');
      else                setErr('Please sign or initial in the signature box.');
      return;
    }
    setErr(''); setBusy(true);

    // Best-effort client IP. If the lookup fails or is blocked we still file the
    // acceptance — an acceptance without an IP is far better than no acceptance.
    let ip = null;
    try {
      const ctl = new AbortController();
      const t = setTimeout(()=>ctl.abort(), 2500);
      const r = await fetch('https://api.ipify.org?format=json', {signal:ctl.signal});
      clearTimeout(t);
      if(r.ok){ const j = await r.json(); ip = j && j.ip ? String(j.ip).slice(0,64) : null; }
    } catch(e){ /* offline, blocked, or timed out — proceed without it */ }

    const now = new Date().toISOString();
    let tz = null;
    try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch(e){}

    const ctx = {
      full_name: (member && member.full_name) || '',
      typed_name: typed.trim(),
      member_id: (member && member.member_id) || '',
      email: session.user.email || '',
      accepted_at: now, ip, user_agent: navigator.userAgent,
      birthdate: dob, signed: !!sig,
      clauses: ticks,
    };

    const payload = {
      student_id: session.user.id,
      member_id: (member && member.member_id) || null,
      email: session.user.email,
      full_name: (member && member.full_name) || typed.trim(),
      typed_name: typed.trim(),
      form_key: doc.key,
      form_title: doc.title,
      version: doc.version,
      effective_date: doc.effective,
      clauses: clauses.map(c => ({id:c.id, text:c.text, accepted:!!ticks[c.id]})),
      confirmed_18: !!ticks.u_18,
      document_text: A.text(doc, ctx),
      ip, user_agent: (navigator.userAgent||'').slice(0,500), timezone: tz,
      signature_png: sig, birthdate: dob || null,
      accepted_at: now,
    };

    const { error } = await client.from('agreement_acceptances').insert(payload);
    setBusy(false);
    if(error){ setErr(error.message); return; }
    if(onDone) onDone(doc.key, doc.version);
  }

  const logo = 'assets/brand/cgh-256.png';

  const card = <div className="ag-wrap">
    <div className="ag-head">
      <img src={logo} alt="" onError={e=>{ e.target.style.display='none'; }}/>
      <div style={{minWidth:0}}>
        <h2>{doc.title}</h2>
        <div className="ag-meta">VERSION {doc.version} · EFFECTIVE {doc.effective} · {A.company.name}</div>
      </div>
    </div>

    <div className="ag-body" ref={bodyRef}>
      <p className="ag-intro">{doc.intro}</p>
      {doc.sections.map((s,i)=>(
        <React.Fragment key={i}>
          <h3>{s.h}</h3>
          {s.p.map((p,j)=><p key={j}>{p}</p>)}
        </React.Fragment>))}
    </div>

    {!readToEnd && <div className="ag-scrollnote">
      <span style={{color:'var(--gold-2)'}}>↓</span>
      Scroll to the end of the agreement to continue.
    </div>}

    <div className="ag-clauses">
      {clauses.map(c=>{
        const on = !!ticks[c.id];
        return <div key={c.id} className={'ag-clause'+(on?' on':'')+((showMissing&&!on)?' miss':'')}
            onClick={()=>toggle(c.id)} role="checkbox" aria-checked={on} tabIndex={0}
            onKeyDown={e=>{ if(e.key===' '||e.key==='Enter'){ e.preventDefault(); toggle(c.id); } }}>
          <span className="ag-box">{TICK}</span>
          <span>{c.text}</span>
        </div>;
      })}
    </div>

    <div className="ag-sign">
      <label>Your full name — this is your electronic signature</label>
      <input value={typed} onChange={e=>setTyped(e.target.value)}
        placeholder={(member && member.full_name) || 'Your full name'}
        autoComplete="off" spellCheck="false"/>
      <div className="ag-hint">
        {member && member.full_name
          ? <>Must match your registered name: <strong style={{color:'var(--ink-dim)'}}>{member.full_name}</strong></>
          : 'Enter your full legal name.'}
        {typed && !nameOk && <span style={{color:'var(--red)'}}> — does not match</span>}
        {typed && nameOk && <span style={{color:'var(--green)'}}> — matches</span>}
      </div>

      <div style={{marginTop:16}}>
        <SignaturePad label="Sign or initial below" onChange={setSig}/>
        <div className="ag-hint">
          {sig ? <span style={{color:'var(--green)'}}>Signature captured.</span>
               : 'Use your finger on a phone, or your mouse on a computer.'}
          {dob && <> · Date of birth on file: <strong style={{color:'var(--ink-dim)'}}>{dob}</strong></>}
          {!dob && <> · <span style={{color:'var(--gold-2)'}}>No date of birth on file</span> — add it under Account settings so it appears on your signed copy.</>}
        </div>
      </div>
    </div>

    {err && <div className="ag-err">{err}</div>}

    <div className="ag-foot">
      <button className="btn btn-primary" style={{width:'auto'}} disabled={busy} onClick={submit}>
        {busy ? <><span className="cg-spin"></span> Filing…</> : 'I accept — sign and continue'}</button>
      {onCancel && <button className="btn btn-ghost" style={{width:'auto'}} disabled={busy}
        onClick={onCancel}>Not now</button>}
      <span style={{fontSize:'.71rem',color:'var(--ink-faint)',marginLeft:'auto'}}>
        A signed copy is filed with {A.company.email}
      </span>
    </div>
  </div>;

  return embedded ? card : <div className="ag-modal"><div>{card}</div></div>;
}

/* ── signed-copy PDF ─────────────────────────────────────────────────────
   The member's own copy of what they signed. Rendered from the stored snapshot,
   never re-generated from the current template, so a later revision of the
   terms cannot change what an old signature appears to say. */
function esc(v){
  return String(v===null||v===undefined?'':v)
    .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function signedCopyHTML(row, opts){
  opts = opts || {};
  const CO = (window.CG_AGREEMENTS && window.CG_AGREEMENTS.company) || {};
  // Absolute, so the logo resolves inside the print iframe wherever the portal
  // page happens to live.
  let logo = '';
  try { logo = new URL('assets/brand/cgh-256.png', document.baseURI).href; } catch(e){}
  // Declared up front, emitted last: in print it is a running footer pinned to
  // the page margin, and on screen it closes the document.
  const foot = '<div class="rf"><span>'+esc(CO.reg || '')+'</span>'
    + '<span>Signed copy &middot; record '+esc(String(row.id||'').slice(0,8))+'</span>'
    + '<span>'+esc(CO.email || '')+'</span></div>';
  const clauses = Array.isArray(row.clauses) ? row.clauses : [];
  const rows = clauses.map((c,i)=>
    '<tr><td class="t">'+(c.accepted?'&#10003;':'&#10007;')+'</td><td class="n">'+(i+1)
    +'</td><td>'+esc(c.text)+'</td></tr>').join('');
  return '<!doctype html><html><head><meta charset="utf-8"><title>'+esc(row.form_title)+'</title><style>'
    + '@page{size:A4;margin:16mm;'
    +   '@bottom-center{content:"Page " counter(page) " of " counter(pages);'
    +     'font:8.5px Helvetica,Arial,sans-serif;color:#94a3b8}}'
    + '.rh{display:flex;align-items:center;gap:9px;border-bottom:1.5px solid #d4af37;padding-bottom:6px;margin-bottom:14px}'
    + '.rh img{height:26px;width:auto}'
    + '.rh .t{font-size:11px;font-weight:700;color:#0f172a;line-height:1.25}'
    + '.rh .s{font-size:8.5px;color:#64748b}'
    + '.rh .r{margin-left:auto;text-align:right;font-size:8px;color:#94a3b8;font-family:monospace}'
    + '.rf{border-top:1px solid #e2e8f0;padding-top:5px;margin-top:16px;'
    +   'display:flex;justify-content:space-between;gap:10px;font-size:8px;color:#94a3b8}'
    + 'body{font:11.5px/1.55 Helvetica,Arial,sans-serif;color:#0f172a;margin:0}'
    + '.hd{border-bottom:2px solid #d4af37;padding-bottom:10px;margin-bottom:14px}'
    + '.hd h1{margin:0;font-size:16px}.hd .sub{color:#64748b;font-size:10px;margin-top:3px}'
    + '.meta{width:100%;border-collapse:collapse;margin-bottom:14px;background:#f8fafc}'
    + '.meta td{padding:5px 8px;border:1px solid #e2e8f0;font-size:10px;vertical-align:top}'
    + '.meta td.k{color:#64748b;width:118px;white-space:nowrap}'
    + 'h2{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#64748b;'
    +   'border-bottom:1px solid #cbd5e1;padding-bottom:4px;margin:16px 0 8px}'
    + 'table.cl{width:100%;border-collapse:collapse}'
    + 'table.cl td{padding:5px 7px;border-bottom:1px solid #f1f5f9;font-size:10.5px;vertical-align:top}'
    + 'td.t{width:16px;color:#15803d;font-weight:700}td.n{width:20px;color:#94a3b8}'
    + '.sig{display:flex;gap:26px;margin:18px 0 4px;break-inside:avoid}'
    + '.sigcell{flex:1}'
    + '.sigimgwrap{height:62px;display:flex;align-items:flex-end}'
    + '.sigimg{max-height:60px;max-width:100%}'
    + '.signone{font-size:9px;color:#94a3b8;padding-bottom:6px}'
    + '.signame{height:62px;display:flex;align-items:flex-end;font-size:17px;font-weight:600;'
    +   'font-family:Georgia,\'Times New Roman\',serif;color:#0f172a}'
    + '.sigrule{border-bottom:1px solid #0f172a;margin-top:3px}'
    + '.sigcap{font-size:8.5px;color:#64748b;text-transform:uppercase;letter-spacing:.06em;margin-top:4px}'
    + 'pre{white-space:pre-wrap;word-wrap:break-word;font:9.5px/1.5 Courier,monospace;color:#334155;margin:0}'
    + 'tr{break-inside:avoid}</style></head><body>'
    + '<div class="rh">'
    +   (logo ? '<img src="'+esc(logo)+'" alt="" onerror="this.style.display=&quot;none&quot;">' : '')
    +   '<div><div class="t">'+esc(CO.name || 'Chart Growth Learning Hub')+'</div>'
    +   '<div class="s">'+esc(row.form_title)+' &middot; v'+esc(row.version)+'</div></div>'
    +   '<div class="r">'+esc(row.member_id || '')+'</div>'
    + '</div>'
    + '<div class="body">'
    + '<div class="hd"><h1>'+esc(row.form_title)+'</h1><div class="sub">Chart Growth Hub &middot; '
    +   'signed copy &middot; version '+esc(row.version)+'</div></div>'
    + '<table class="meta">'
    +   '<tr><td class="k">Signed by</td><td><strong>'+esc(row.full_name)+'</strong></td>'
    +       '<td class="k">Member ID</td><td>'+esc(row.member_id||'-')+'</td></tr>'
    +   '<tr><td class="k">Typed signature</td><td>'+esc(row.typed_name)+'</td>'
    +       '<td class="k">Email</td><td>'+esc(row.email)+'</td></tr>'
    +   '<tr><td class="k">Date of birth</td><td>'+esc(row.birthdate||'not on file')+'</td>'
    +       '<td class="k">Age 18+</td><td>'+(row.confirmed_18?'confirmed':'NOT CONFIRMED')+'</td></tr>'
    +   '<tr><td class="k">Accepted at</td><td>'+esc(row.accepted_at)+'</td>'
    +       '<td class="k">Timezone</td><td>'+esc(row.timezone||'-')+'</td></tr>'
    +   (opts.showIp
            ? '<tr><td class="k">Network address</td><td>'+esc(row.ip||'not recorded')+'</td>'
              + '<td class="k">Source</td><td>'+esc(row.source==='offline'?'paper copy':'portal')+'</td></tr>'
            : '')
    +   '<tr><td class="k">Record ID</td><td colspan="3" style="font-family:monospace;font-size:9px">'
    +       esc(row.id)+'</td></tr>'
    + '</table>'
    + '<div class="sig">'
    +   '<div class="sigcell">'
    +     '<div class="sigimgwrap">'
    +       (row.signature_png
              ? '<img class="sigimg" src="'+esc(row.signature_png)+'" alt="signature">'
              : '<span class="signone">no drawn signature on this record</span>')
    +     '</div>'
    +     '<div class="sigrule"></div>'
    +     '<div class="sigcap">Signature / initials</div>'
    +   '</div>'
    +   '<div class="sigcell">'
    +     '<div class="signame">'+esc(row.full_name)+'</div>'
    +     '<div class="sigrule"></div>'
    +     '<div class="sigcap">Full name &mdash; electronic signature</div>'
    +   '</div>'
    + '</div>'
    + '<h2>Confirmations ticked</h2><table class="cl">'+rows+'</table>'
    + '<h2>Document as presented</h2><pre>'+esc(row.document_text)+'</pre>'
    + '</div>'
    + foot
    + '</body></html>';
}
// Off-screen iframe rather than window.open(): popup blockers kill the latter
// even on a click, and an iframe keeps the portal itself out of the printout.
function printSignedCopy(row, opts){
  const old = document.getElementById('cg-agree-print');
  if(old) old.remove();
  const f = document.createElement('iframe');
  f.id = 'cg-agree-print'; f.setAttribute('aria-hidden','true');
  f.style.cssText = 'position:fixed;left:-10000px;top:0;width:1024px;height:768px;border:0';
  document.body.appendChild(f);
  const d = f.contentDocument;
  d.open(); d.write(signedCopyHTML(row, opts)); d.close();
  const go = () => { try{ f.contentWindow.focus(); f.contentWindow.print(); }catch(e){} };
  if(d.readyState === 'complete') setTimeout(go, 250); else f.onload = () => setTimeout(go, 250);
}

/* ── sequential flow ─────────────────────────────────────────────────────
   Every student — existing and new — signs all three documents. They are
   presented one at a time with a progress line rather than as one enormous
   scroll, because a member who is shown 30 clauses at once ticks them without
   reading, which is exactly what these forms exist to prevent. */
function AgreementFlow({client, session, member, status, onDone, onAllDone}){
  const A = window.CG_AGREEMENTS;
  const pending = A ? A.order.filter(k => agreementNeeded(status, k)) : [];

  useEffect(()=>{ if(pending.length === 0 && onAllDone) onAllDone(); /* eslint-disable-next-line */ },
    [pending.length]);

  if(!A || pending.length === 0) return null;

  const total = A.order.length;
  const doneCount = total - pending.length;
  const current = pending[0];

  return <div className="ag-modal"><div>
    <div className="ag-progress">
      <div className="ag-progress-top">
        <strong>Before you continue</strong>
        <span>{doneCount + 1} of {total}</span>
      </div>
      <div className="ag-progress-bar">
        <div style={{width: Math.round(doneCount/total*100) + '%'}}/>
      </div>
      <div className="ag-progress-list">
        {A.order.map(k => {
          const d = A.docs[k];
          const signed = !agreementNeeded(status, k);
          const isNow = k === current;
          return <span key={k} className={'ag-step'+(signed?' done':'')+(isNow?' now':'')}>
            {signed ? '✓ ' : ''}{d.short}
          </span>;
        })}
      </div>
    </div>
    {/* key forces a fresh mount per document — without it React reuses the
        instance and the previous document's ticks and typed name carry over. */}
    <AgreementGate key={current} client={client} session={session} member={member}
      formKey={current} embedded={true} onDone={onDone}/>
  </div></div>;
}

window.AgreementFlow      = AgreementFlow;
window.cgSignedCopyPrint  = printSignedCopy;
window.cgSignedCopyHTML   = signedCopyHTML;
window.AgreementGate       = AgreementGate;
window.cgAgreementStatus   = agreementStatus;
window.cgAgreementNeeded   = agreementNeeded;
window.cgAgreementNameOk   = nameMatches;   // exported for tests
})();
