/* ============================================================
   REYAH PLATFORM, CLIENT APP
   Clinic-facing one-stop deliverability + pipeline dashboard
   ============================================================ */
/* Data binding. C / METRICS / NAV are re-derived after REYAH_DATA.load()
   resolves (mock or live) in the bootstrap at the bottom of this file, so the
   `useMock:false` switch in integrations/config.js actually flows live data in. */
let C = window.RYH.client;

/* ---- metric definitions (each opens a drawer over time) ---- */
const pct = v => v+"%";
function buildMetrics(){
  const METRICS = {
  contacted:{ id:"contacted", icon:"users", label:"Doctors contacted", value:C.kpis.contacted.toLocaleString(), raw:C.kpis.contacted, kind:"cumulative",
    delta:C.deltas.contacted, deltaLabel:"vs last 30 days", color:"#FF4A1C",
    breakdownTitle:"By channel", breakdown:[
      {label:"Email",value:"1,747",pct:78,kind:"info"},{label:"SMS",value:"486",pct:22,kind:"pos"},{label:"Voicemail",value:"212",pct:10,kind:""}],
    note:"Unique doctors reached at least once across all sending domains and channels, sourced by specialty and geography and AHPRA-checked for VR status." },
  replyRate:{ id:"replyRate", icon:"reply", label:"Reply rate", value:C.kpis.replyRate, unit:"%", raw:C.kpis.replyRate, kind:"rate",
    delta:C.deltas.replyRate, deltaUnit:"pts", deltaLabel:"vs last 30 days", color:"#2A6FDB", fmt:pct,
    breakdownTitle:"Replies by day of sequence", breakdown:[
      {label:"Day 1 email",value:"4%",pct:22,kind:"info"},{label:"Day 4 email",value:"6%",pct:33,kind:"info"},
      {label:"Day 6 email",value:"5%",pct:28,kind:"info"},{label:"Day 10 final + one-pager",value:"9%",pct:50,kind:"info"}],
    note:"Share of contacted doctors who replied. Day 10 (final email + one-pager) is consistently the highest-reply touch." },
  positiveRate:{ id:"positiveRate", icon:"target", label:"Positive reply rate", value:C.kpis.positiveRate, unit:"%", raw:C.kpis.positiveRate, kind:"rate",
    delta:C.deltas.positiveRate, deltaUnit:"pts", deltaLabel:"vs last 30 days", color:"#FFFFFF", feature:true, fmt:pct,
    note:"North-star metric, the share of replies that are positive (open to a conversation). Doctor-to-doctor messaging drives this far higher than recruiter-style outreach." },
  positives:{ id:"positives", icon:"zap", label:"Positive replies", value:C.kpis.positiveReplies, raw:C.kpis.positiveReplies, kind:"cumulative",
    delta:C.deltas.positiveReplies, deltaLabel:"vs last 30 days", color:"#1F8A5B",
    breakdownTitle:"Pipeline stage", breakdown:[
      {label:"New positive reply",value:"3",pct:30,kind:"pos"},{label:"In conversation",value:"2",pct:20,kind:"pos"},
      {label:"Interview booked",value:"2",pct:20,kind:"pos"},{label:"Hired",value:"2",pct:20,kind:"pos"}],
    note:"Doctors who responded with interest. Respond same-day. Conversion drops sharply after 24 hours." },
  interviews:{ id:"interviews", icon:"calendar", label:"Interviews", value:C.kpis.interviews, raw:C.kpis.interviews, kind:"cumulative",
    delta:+2, deltaLabel:"vs last 30 days", color:"#FF4A1C" },
  bookings:{ id:"bookings", icon:"book", label:"Calls booked", value:C.kpis.bookings, raw:C.kpis.bookings, kind:"cumulative",
    delta:C.deltas.bookings, deltaLabel:"vs last 30 days", color:"#2A6FDB" },
  hires:{ id:"hires", icon:"hire", label:"Hires", value:C.kpis.hires, raw:C.kpis.hires, kind:"cumulative",
    delta:C.deltas.hires, deltaLabel:"this campaign", color:"#1F8A5B",
    note:"Doctors placed and started. Reyah keeps launching campaigns until a hire lands; placements carry a 3-month replacement guarantee." },
  response:{ id:"response", icon:"clock", label:"Avg response time", value:"38", unit:"min", raw:38, kind:"time",
    delta:-12, deltaUnit:"min", deltaGood:true, deltaLabel:"faster vs last 30 days", color:"#FF4A1C", target:"Same-day",
    note:"How fast your team responds to a positive reply. Reply speed is the #1 conversion lever. Same-day responses massively outperform 3+ days." },
  deliver:{ id:"deliver", icon:"send", label:"Deliverability", value:C.deliverability.avgDeliver, unit:"%", raw:C.deliverability.avgDeliver, kind:"rate",
    delta:+0.4, deltaUnit:"pts", deltaLabel:"vs last 30 days", color:"#1F8A5B", fmt:pct,
    breakdownTitle:"By sending domain", breakdown:C.domains.map(d=>({label:d.dom,value:d.deliver+"%",pct:d.deliver,kind:d.status==="healthy"?"pos":"warn"})),
    note:"Share of emails that reach the inbox across all dedicated sending domains. SPF/DKIM/DMARC are enforced and domains are warmed for 3 weeks before volume sends." },
  warmup:{ id:"warmup", icon:"shield", label:"Warm-up health", value:"100", unit:"%", raw:100, kind:"rate",
    delta:0, deltaLabel:"domains on schedule", color:"#1F8A5B", fmt:pct,
    note:"Share of sending domains tracking on their warm-up schedule. New domains ramp gradually to protect deliverability." }
};
  // Deltas are derived from each metric's own trend data (drop-in for live data).
  Object.keys(METRICS).forEach(function(k){ METRICS[k]=trendDelta(METRICS[k]); });
  return METRICS;
}
let METRICS = buildMetrics();
const SITE=(window.REYAH_CONFIG&&window.REYAH_CONFIG.siteBase)||"../site";

/* ---- nav config ---- */
function buildNav(){ return [
  {label:"Overview", links:[
    {id:"overview", label:"Overview", icon:"grid"},
    {id:"deliverability", label:"Deliverability", icon:"send"}
  ]},
  {label:"Pipeline", links:[
    {id:"replies", label:"Inbox", icon:"inbox"},
    {id:"pipeline", label:"CRM", icon:"zap"},
    {id:"bookings", label:"Call bookings", icon:"calendar", count:C.kpis.bookings},
    {id:"hires", label:"Hires", icon:"hire"}
  ]},
  {label:"Database", links:[
    {id:"doctors", label:"Doctors contacted", icon:"users"}
  ]},
  {label:"Account", links:[
    {id:"settings", label:"Settings", icon:"settings"}
  ]}
]; }
let NAV = buildNav();

const TITLES={
  overview:["Overview","Campaign performance at a glance"],
  deliverability:["Deliverability","Inbox placement & domain health"],
  replies:["Inbox","Every doctor response in one place"],
  pipeline:["CRM","Your pipeline, synced with GoHighLevel"],
  bookings:["Call bookings","Interviews & intro calls"],
  hires:["Hires","Doctors placed this campaign"],
  doctors:["Doctors contacted","Your full sourced database"],
  integrations:["Integrations","Connect your calendar, CRM and email"],
  settings:["Settings","Manage your workspace and alerts"]
};

/* =====================================================================
   SCREENS
===================================================================== */
function KpiCard({m, onOpen}){
  return (
    <div className={"kpi clickable"+(m.feature?" feature":"")} onClick={function(){onOpen(m);}}>
      <div className="kpi__top"><span className="ic"><Icon n={m.icon} /></span><span className="lab">{m.label}</span></div>
      <div className="kpi__num">{m.value}{m.unit&&<span className="u">{m.unit}</span>}</div>
      <div className="kpi__sub">
        {m.delta!=null && <span className={"delta "+((m.deltaGood?-m.delta:m.delta)>=0?"up":"down")}><Icon n={m.delta>=0?"arrowUp":"arrowDown"} />{m.delta>=0?"+":""}{m.delta}{m.deltaUnit||""}</span>}
        <span>{m.deltaLabel}</span>
      </div>
    </div>
  );
}

function Overview({openMetric, leads, goInbox}){
  const [range,setRange]=React.useState("30d");
  const pending=(leads||C.leads).filter(function(l){ return l.sentiment==="pos" && l.unread; });
  const sent=window.RYH.genSeries("ov-sent",range,42,"count");
  const pos=window.RYH.genSeries("ov-pos",range,4,"count");
  return (
    <div className="stack-gap">
      {pending.length>0 && (
      <div className="pcard" style={{background:'var(--ink-bg)',color:'#fff',border:'none',overflow:'hidden',position:'relative'}}>
        <div style={{position:'absolute',inset:0,background:'radial-gradient(60% 120% at 92% -10%,rgba(255,74,28,0.45),transparent 60%)',pointerEvents:'none'}} />
        <div className="pcard__body" style={{display:'flex',alignItems:'center',gap:20,flexWrap:'wrap',position:'relative'}}>
          <span style={{width:46,height:46,borderRadius:12,background:'rgba(255,255,255,0.14)',display:'flex',alignItems:'center',justifyContent:'center',fontSize:22,flex:'0 0 auto'}}><Icon n="alert" /></span>
          <div style={{flex:1,minWidth:200}}>
            <b style={{fontSize:'1.05rem',fontWeight:600}}>{pending.length} positive {pending.length>1?"replies need":"reply needs"} a response today</b>
            <p style={{fontSize:'var(--fs-sm)',color:'rgba(255,255,255,0.78)',marginTop:3}}>{pending[0].name} {pending.length>1?("and "+(pending.length-1)+" more are"):"is"} waiting on a reply. Same-day responses win far more doctors, so respond before the day is out.</p>
          </div>
          <button className="btn btn--white btn--sm" onClick={function(){ goInbox(pending[0].id); }}>Respond now <Icon n="arrowRt" /></button>
        </div>
      </div>
      )}

      {/* hero KPIs */}
      <div className="kpis">
        <KpiCard m={METRICS.positiveRate} onOpen={openMetric} />
        <KpiCard m={METRICS.contacted} onOpen={openMetric} />
        <KpiCard m={METRICS.replyRate} onOpen={openMetric} />
        <KpiCard m={METRICS.positives} onOpen={openMetric} />
      </div>
      <div className="kpis">
        <KpiCard m={METRICS.bookings} onOpen={openMetric} />
        <KpiCard m={METRICS.interviews} onOpen={openMetric} />
        <KpiCard m={METRICS.hires} onOpen={openMetric} />
        <KpiCard m={METRICS.response} onOpen={openMetric} />
      </div>

      {/* main chart + funnel */}
      <div className="grid-32">
        <PCard title="Sends & positive replies" sub="Daily outbound volume vs positive responses"
          action={<Seg options={[{value:"7d",label:"7D"},{value:"30d",label:"30D"},{value:"3m",label:"3M"},{value:"12m",label:"12M"}]} value={range} onChange={setRange} />}>
          <AreaLine labels={sent.labels} height={230} series={[
            {data:sent.data, color:"#FF4A1C", name:"Sends"},
            {data:pos.data, color:"#1F8A5B", name:"Positive replies"}
          ]} />
          <div className="legend" style={{marginTop:14}}>
            <span><i style={{background:'#FF4A1C'}} />Sends</span>
            <span><i style={{background:'#1F8A5B'}} />Positive replies</span>
          </div>
        </PCard>
        <PCard title="Conversion funnel" sub="From contact to hire">
          <Funnel steps={C.funnel} />
        </PCard>
      </div>

      {/* deliverability snapshot + activity */}
      <div className="grid-23">
        <PCard title="Deliverability" sub="Inbox placement" action={<button className="btn btn--ghost btn--sm" onClick={function(){openMetric(METRICS.deliver);}}>Details</button>}>
          <div style={{display:'flex',alignItems:'center',gap:22,flexWrap:'wrap'}}>
            <Donut size={150} center={{value:C.deliverability.avgDeliver+"%",label:"delivered"}} segments={[
              {value:C.deliverability.avgDeliver,color:"#1F8A5B"},
              {value:100-C.deliverability.avgDeliver,color:"#F1EDE6"}
            ]} />
            <div style={{flex:1,minWidth:170,display:'flex',flexDirection:'column',gap:14}}>
              <div><div style={{display:'flex',justifyContent:'space-between',fontSize:'var(--fs-sm)',marginBottom:6}}><span className="muted">Open rate</span><b>{C.deliverability.openRate}%</b></div><Meter pct={C.deliverability.openRate} kind="info" /></div>
              <div><div style={{display:'flex',justifyContent:'space-between',fontSize:'var(--fs-sm)',marginBottom:6}}><span className="muted">Bounce rate</span><b>{C.deliverability.avgBounce}%</b></div><Meter pct={C.deliverability.avgBounce*8} kind="warn" /></div>
              <div><div style={{display:'flex',justifyContent:'space-between',fontSize:'var(--fs-sm)',marginBottom:6}}><span className="muted">Spam complaints</span><b>{C.deliverability.spamRate}%</b></div><Meter pct={C.deliverability.spamRate*20} kind="pos" /></div>
            </div>
          </div>
        </PCard>
        <PCard title="Recent activity" sub="Live campaign events">
          <Feed items={C.feed} />
        </PCard>
      </div>
    </div>
  );
}

function Deliverability({openMetric}){
  return (
    <div className="stack-gap">
      <div className="kpis">
        <KpiCard m={METRICS.deliver} onOpen={openMetric} />
        <KpiCard m={{...METRICS.replyRate, id:"open", label:"Open rate", value:C.deliverability.openRate, raw:C.deliverability.openRate, icon:"mail", delta:+4, color:"#2A6FDB"}} onOpen={openMetric} />
        <KpiCard m={{id:"bounce", label:"Bounce rate", value:C.deliverability.avgBounce, unit:"%", raw:C.deliverability.avgBounce, kind:"time", icon:"alert", delta:-0.3, deltaUnit:"pts", deltaGood:true, deltaLabel:"vs last 30 days", color:"#C77B12", fmt:pct, note:"Bounced sends are auto-suppressed across all domains to protect sender reputation."}} onOpen={openMetric} />
        <KpiCard m={METRICS.warmup} onOpen={openMetric} />
      </div>
      <PCard title="Sending domains" sub="2-4 dedicated domains per campaign, never your clinic's primary domain">
        <div className="dhrow" style={{borderBottom:'1px solid var(--line)',paddingTop:0,fontSize:'11px',textTransform:'uppercase',letterSpacing:'0.06em',color:'var(--muted)',fontWeight:600}}>
          <span>Domain</span><span>Sent</span><span>Delivered</span><span>Auth</span><span>Status</span>
        </div>
        {C.domains.map(function(d,i){
          return (
            <div className="dhrow" key={i}>
              <div><div className="dom">{d.dom}</div><div className="mini">{d.warm}</div></div>
              <div style={{fontVariantNumeric:'tabular-nums',fontWeight:600,color:'var(--ink)'}}>{d.sent}</div>
              <div><b style={{color:'var(--ink)'}}>{d.deliver}%</b><Meter pct={d.deliver} kind={d.status==='healthy'?'pos':'warn'} /></div>
              <div className="auth">
                <span className="authpill">SPF</span><span className="authpill">DKIM</span>
                <span className={"authpill"+(d.dmarc?"":" off")}>DMARC</span>
              </div>
              <div>{d.status==="healthy"?<Tag kind="pos" dot>Healthy</Tag>:<Tag kind="warn" dot>Warming</Tag>}</div>
            </div>
          );
        })}
      </PCard>
      <div className="grid-2">
        <PCard title="Channel mix" sub="Touches sent this campaign">
          <div style={{display:'flex',alignItems:'center',gap:24,flexWrap:'wrap'}}>
            <Donut size={150} center={{value:"2,445",label:"touches"}} segments={C.channels.map(c=>({value:c.value,color:c.cls==='info'?'#2A6FDB':c.cls==='pos'?'#1F8A5B':'#FF4A1C'}))} />
            <div style={{flex:1,minWidth:150,display:'flex',flexDirection:'column',gap:12}}>
              {C.channels.map(function(c,i){
                const col=c.cls==='info'?'#2A6FDB':c.cls==='pos'?'#1F8A5B':'#FF4A1C';
                return <div key={i} style={{display:'flex',alignItems:'center',gap:10}}><i style={{width:11,height:11,borderRadius:3,background:col}} /><span style={{fontSize:'var(--fs-sm)',color:'var(--ink-2)',flex:1}}>{c.label}</span><b style={{fontVariantNumeric:'tabular-nums'}}>{c.value.toLocaleString()}</b></div>;
              })}
            </div>
          </div>
        </PCard>
        <PCard title="Compliance" sub="Compliant by design. Our system, our liability">
          <div style={{display:'flex',flexDirection:'column',gap:14}}>
            {[["Publicly-available data only","Every contact sourced from public records"],
              ["DNC & suppression screening","Checked on every single send"],
              ["SPAM Act 2003 compliant","Auto unsubscribe / STOP handling"],
              ["Full audit trail","Every send logged and retained"]].map(function(r,i){
              return <div key={i} style={{display:'flex',gap:13,alignItems:'flex-start'}}>
                <span style={{width:28,height:28,borderRadius:8,background:'var(--pos-wash)',color:'var(--pos)',display:'flex',alignItems:'center',justifyContent:'center',flex:'0 0 auto',fontSize:15}}><Icon n="check" /></span>
                <div><b style={{fontSize:'var(--fs-sm)',fontWeight:600,color:'var(--ink)'}}>{r[0]}</b><p style={{fontSize:'13px',color:'var(--muted)',marginTop:1}}>{r[1]}</p></div>
              </div>;
            })}
          </div>
        </PCard>
      </div>
    </div>
  );
}

function Replies({leads, domains, onAppend, onRead, sendChannel, openLead, initLeadId, onRetry}){
  const inbox=leads.filter(function(l){ return l.thread && l.thread.length>0; });
  const [selId,setSelId]=React.useState(initLeadId || (inbox[0]&&inbox[0].id) || null);
  const sel=inbox.find(function(m){return m.id===selId;}) || inbox[0] || null;
  const [chan,setChan]=React.useState(sel&&sel.chan==="sms"?"sms":"email");
  const [draft,setDraft]=React.useState("");
  const [filter,setFilter]=React.useState("All");
  // Email-only composer state. fromDomain defaults to the first healthy sending
  // domain (fallback: first available). Subject prefills "Re: <orig>" per thread.
  const doms=domains||[];
  const defDom=(doms.find(function(d){return d.status==="healthy";})||doms[0]||{}).dom||"";
  const [subject,setSubject]=React.useState(sel?("Re: "+(sel.subject||sel.lastSubject||"your enquiry")):"");
  const [preheader,setPreheader]=React.useState("");
  const [fromDomain,setFromDomain]=React.useState(defDom);
  const [files,setFiles]=React.useState([]);
  const [pending,setPending]=React.useState(0); // attachment uploads in flight
  const fileInput=React.useRef(null);
  // Tracks the currently-selected lead so a late-resolving upload (started on a
  // different thread) can't bleed its attachment into whoever's open now.
  const selIdRef=React.useRef(null);
  selIdRef.current=sel?sel.id:null;
  if(!sel){
    return <PCard><div style={{padding:'52px 24px',textAlign:'center',color:'var(--muted)'}}><div style={{fontSize:30,marginBottom:10,opacity:.45,display:'flex',justifyContent:'center'}}><Icon n="inbox" /></div><b style={{display:'block',color:'var(--ink-2)',marginBottom:4}}>No replies yet</b><span className="sm">Doctor replies land here the moment they come in.</span></div></PCard>;
  }
  const shown=inbox.filter(function(m){ return filter==="All" || (filter==="Positive" && m.sentiment==="pos"); });
  function pick(m){ setSelId(m.id); onRead(m.id); setChan(m.chan==="sms"?"sms":"email"); setSubject("Re: "+(m.subject||m.lastSubject||"your enquiry")); setPreheader(""); setFiles([]); }
  function pickFiles(e){
    var chosen=Array.prototype.slice.call(e.target.files||[]);
    if(fileInput.current) fileInput.current.value="";
    var forLead=sel.id;
    chosen.forEach(function(file){
      setPending(function(n){ return n+1; });
      window.REYAH_DATA.uploadAttachment(file, forLead)
        .then(function(ref){ setFiles(function(fs){ return selIdRef.current===forLead ? fs.concat([ref]) : fs; }); })
        .catch(function(){ if(window.ryToast) window.ryToast("Couldn't attach "+file.name); })
        .finally(function(){ setPending(function(n){ return n-1; }); });
    });
  }
  function removeFile(id){ setFiles(function(fs){ return fs.filter(function(f){ return f.id!==id; }); }); }
  function send(){
    const v=draft.trim(); if(!v) return;
    if(chan==="email"){
      onAppend(sel.id, {who:"My Local Doc", us:true, chan:"email", time:"now", t:v, subject:subject, preheader:preheader, fromDomain:fromDomain, attachments:files});
      setDraft(""); setPreheader(""); setFiles([]); setSubject("Re: "+(sel.subject||sel.lastSubject||"your enquiry"));
      return;
    }
    onAppend(sel.id, {who:"My Local Doc", us:true, chan:chan, time:"now", t:v});
    setDraft("");
  }
  return (
    <PCard pad={false}>
      <div className="inbox">
        <div className="inbox__list">
          <div className="inbox__filter">
            <span className={"chip"+(filter==="All"?" active":"")} onClick={function(){setFilter("All");}}>All <small>{inbox.length}</small></span>
            <span className={"chip"+(filter==="Positive"?" active":"")} onClick={function(){setFilter("Positive");}}>Positive <small>{inbox.filter(function(m){return m.sentiment==="pos";}).length}</small></span>
          </div>
          {shown.map(function(m){
            const last=m.thread[m.thread.length-1];
            return (
              <div key={m.id} className={"msg"+(selId===m.id?" active":"")+(m.unread?" unread":"")} onClick={function(){pick(m);}}>
                <span className="av">{m.init}</span>
                <div style={{minWidth:0}}>
                  <div className="msg__top"><b>{m.name}</b><time>{m.last}</time></div>
                  <div className="msg__pre">{last?last.t:""}</div>
                  <div className="msg__tags"><Chan c={m.chan} />{sentimentTag(m.sentiment)}</div>
                </div>
              </div>
            );
          })}
        </div>
        <div className="thread">
          <div className="thread__head">
            <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',gap:10}}>
              <h3>{sel.name}</h3>
              <button className="btn btn--ghost btn--sm" onClick={function(){openLead(sel.id);}}><Icon n="edit" /> Lead page</button>
            </div>
            <div className="meta"><span className="muted xs">{sel.loc} · {sel.vr}</span>{sentimentTag(sel.sentiment)}</div>
            <div className="thread__chan">
              <button className="chanbtn" onClick={function(){sendChannel("call",sel);}}><Icon n="phone" />Call</button>
              <button className="chanbtn" onClick={function(){sendChannel("voicemail",sel);}}><Icon n="voicemail" />Voicemail</button>
              <button className="chanbtn" onClick={function(){sendChannel("voicenote",sel);}}><Icon n="mic" />Voice note</button>
            </div>
          </div>
          <div className="thread__body">
            {sel.thread.map(function(b,i){
              var cls="bubble "+(b.us?"us":"them")+(b.status==="failed"?" bubble--failed":"")+(b.status==="sending"?" bubble--sending":"");
              return (
                <div key={b.id||i} className={cls}>
                  <span className="who">{b.who}{b.chan?(" · "+b.chan.toUpperCase()):""}</span>
                  {b.chan==="email" && b.subject && <div className="bubble__subject">{b.subject}</div>}
                  {b.t}
                  {b.attachments && b.attachments.length>0 && (
                    <div className="bubble__atts">
                      {b.attachments.map(function(f){ return <span className="att-chip att-chip--sm" key={f.id}>{f.name} · {Math.round((f.size||0)/1024)}KB</span>; })}
                    </div>
                  )}
                  {b.status==="sending" && <span className="bubble__status">Sending…</span>}
                  {b.status==="failed" && <span className="bubble__status">Not sent · {b.chan==="email"?"PlusVibe":"GoHighLevel"} rejected <button className="bubble__retry" onClick={function(){onRetry(sel.id,b.id);}}>Retry</button></span>}
                </div>
              );
            })}
          </div>
          <div className="composer">
            <div className="composer__tabs">
              <button className={"composer__tab"+(chan==="email"?" active":"")} onClick={function(){setChan("email");}}><Icon n="mail" />Email · PlusVibe</button>
              <button className={"composer__tab"+(chan==="sms"?" active":"")} onClick={function(){setChan("sms");}}><Icon n="message" />SMS · GHL</button>
            </div>
            {chan==="email" ? (
              <div className="composer__email">
                <input className="composer__subject" value={subject} onChange={function(e){setSubject(e.target.value);}} placeholder="Subject" />
                <input className="composer__preheader" value={preheader} onChange={function(e){setPreheader(e.target.value);}} placeholder="Preheader (optional preview text)" />
                {files.length>0 && (
                  <div className="composer__chips">
                    {files.map(function(f){ return (
                      <span className="att-chip" key={f.id}>{f.name} · {Math.round((f.size||0)/1024)}KB
                        <button className="att-chip__x" onClick={function(){removeFile(f.id);}} aria-label="Remove">×</button></span>
                    ); })}
                  </div>
                )}
                <div className="composer__row">
                  <textarea className="composer__body" value={draft} onChange={function(e){setDraft(e.target.value);}} rows={3}
                    placeholder="Write an email… reads doctor-to-doctor, never as Reyah"
                    onInput={function(e){e.target.style.height='auto';e.target.style.height=e.target.scrollHeight+'px';}} />
                  <select className="composer__domain" value={fromDomain} onChange={function(e){setFromDomain(e.target.value);}} title="Send from">
                    {doms.map(function(d){ return <option key={d.dom} value={d.dom}>{d.dom}</option>; })}
                  </select>
                  <button className="composer__attach" onClick={function(){if(fileInput.current)fileInput.current.click();}} title="Attach" aria-label="Attach file">+</button>
                  <input ref={fileInput} type="file" multiple style={{display:'none'}} accept=".pdf,.png,.jpg,.jpeg,.gif,.webp,.docx" onChange={pickFiles} />
                  {pending>0 && <span className="muted xs">Uploading…</span>}
                  <button className="btn btn--primary btn--sm" onClick={send} disabled={pending>0}>Send <Icon n="send" /></button>
                </div>
              </div>
            ) : (
              <div className="composer__row">
                <button className="iconbtn" aria-label="Record voice message" title="Record a voice message (via GoHighLevel)" onClick={function(){window.ryToast("Recording voice message… sends via GoHighLevel");}}><Icon n="mic" /></button>
                <input value={draft} onChange={function(e){setDraft(e.target.value);}} onKeyDown={function(e){if(e.key==="Enter")send();}} placeholder="Write a text message…" />
                <button className="btn btn--primary btn--sm" onClick={send}>Send <Icon n="send" /></button>
              </div>
            )}
            <div className="composer__hint"><Icon n="shield" />{chan==="email"?"Sends from your dedicated domain via PlusVibe.":"Sends via GoHighLevel from your campaign number."}</div>
          </div>
        </div>
      </div>
    </PCard>
  );
}

function Pipeline({leads, stages, onMove, openLead}){
  const [drag,setDrag]=React.useState(null);
  const [over,setOver]=React.useState(null);
  function drop(col){ if(drag){ onMove(drag, col); } setDrag(null); setOver(null); }
  return (
    <PCard title="CRM pipeline" sub="Drag a doctor between stages, changes sync straight to GoHighLevel" pad={true}>
      <div className="kanban">
        {stages.map(function(col){
          const cards=leads.filter(function(l){return l.stage===col;});
          return (
            <div className={"kcol"+(over===col?" dragover":"")} key={col}
              onDragOver={function(e){e.preventDefault(); if(over!==col)setOver(col);}}
              onDragLeave={function(){ setOver(function(o){return o===col?null:o;}); }}
              onDrop={function(){drop(col);}}>
              <div className="kcol__head"><b>{col}</b><span className="n">{cards.length}</span></div>
              {cards.map(function(c){
                return (
                  <div className={"kcard"+(drag===c.id?" dragging":"")} key={c.id} draggable
                    onDragStart={function(e){ setDrag(c.id); e.dataTransfer.effectAllowed="move"; }}
                    onDragEnd={function(){ setDrag(null); setOver(null); }}
                    onClick={function(){ if(!drag) openLead(c.id); }}>
                    <div className="kcard__name"><span className="av">{c.init}</span><div><b>{c.name}</b><span>{c.loc} · {c.vr}</span></div></div>
                    <p style={{fontSize:'12.5px',color:'var(--muted)',marginTop:9,lineHeight:1.4,display:'-webkit-box',WebkitLineClamp:2,WebkitBoxOrient:'vertical',overflow:'hidden'}}>{c.notes}</p>
                    <div className="kcard__meta"><Chan c={c.chan} /><span style={{fontSize:'11px',color:'var(--faint)'}}>{c.last}</span></div>
                  </div>
                );
              })}
              {cards.length===0 && <div style={{fontSize:'12px',color:'var(--faint)',textAlign:'center',padding:'16px 0'}}>Drop here</div>}
            </div>
          );
        })}
      </div>
    </PCard>
  );
}

function Bookings({openLead}){
  const [bk,setBk]=React.useState(C.bookings);
  function setBkStatus(leadId,status){ setBk(function(list){ return list.map(function(x){ return x.leadId===leadId?Object.assign({},x,{status:status}):x; }); }); }
  function toggleStatus(e,b){
    e.stopPropagation();
    var ns=b.status==="confirmed"?"pending":"confirmed";
    setBkStatus(b.leadId,ns); // optimistic
    window.REYAH_DATA.act("bookingUpdate",{leadId:b.leadId, bookingId:b.bookingId, ghlId:b.ghlId, date:b.date, time:b.time, type:b.type, status:ns})
      .then(function(){ window.ryToast(ns==="confirmed"?(b.name+" confirmed · synced to calendar"):(b.name+" set to pending")); })
      .catch(function(){ setBkStatus(b.leadId,b.status); window.ryToast("Couldn't update booking — reverted"); });
  }
  const groups=[];
  bk.forEach(function(b){
    let g=groups.find(function(x){return x.day===b.day;});
    if(!g){ g={day:b.day, items:[]}; groups.push(g); }
    g.items.push(b);
  });
  return (
    <div className="stack-gap">
      <div className="pcard"><div className="pcard__body" style={{display:'flex',alignItems:'center',gap:14,flexWrap:'wrap'}}>
        <span style={{width:42,height:42,borderRadius:11,background:'var(--info-wash)',color:'var(--info)',display:'flex',alignItems:'center',justifyContent:'center',fontSize:19,flex:'0 0 auto'}}><Icon n="calendar" /></span>
        <div style={{flex:1,minWidth:200}}><b style={{fontWeight:600,color:'var(--ink)'}}>{bk.length} upcoming calls &amp; interviews</b><p style={{fontSize:'var(--fs-sm)',color:'var(--muted)',marginTop:2}}>Synced from your Google Calendar and GoHighLevel. Tap a booking to open the doctor's lead page.</p></div>
        <button className="btn btn--ghost btn--sm" onClick={function(){window.ryToast("Synced with Google Calendar");}}><Icon n="calendar" /> Sync calendar</button>
      </div></div>
      {groups.map(function(g,gi){
        return (
          <div className="bk-day" key={gi}>
            <div className="bk-day__h"><span>{g.day}</span><span className="ln"></span><span>{g.items.length} booking{g.items.length>1?"s":""}</span></div>
            {g.items.map(function(b,i){
              return (
                <div className="bk" key={i} onClick={function(){openLead(b.leadId);}}>
                  <div className="bk__time"><b>{b.time}</b><span>{b.dur}</span></div>
                  <div className="bk__who"><span className="av">{b.init}</span><div><b>{b.name}</b><span>{b.type} · {b.mode}</span></div></div>
                  <div style={{display:'flex',flexDirection:'column',alignItems:'flex-end',gap:7}}>
                    <button className="bk__status" onClick={function(e){toggleStatus(e,b);}} title="Toggle confirmation">{b.status==="confirmed"?<Tag kind="pos" dot>Confirmed</Tag>:<Tag kind="warn" dot>Pending</Tag>}</button>
                    <span style={{fontSize:'12px',color:'var(--muted)'}}>with {b.with}</span>
                  </div>
                </div>
              );
            })}
          </div>
        );
      })}
    </div>
  );
}

function Hires({openLead, leads, statusOverrides, onStatus}){
  // Source of truth = canonical hires[] (backend auto-populates these from any
  // opportunity at the Hired/Won stage, and overlays the persisted status onto
  // them). Then merge in any locally-Hired lead not yet reflected there, so a
  // pipeline drag shows up immediately. Key everything by the hire's leadId
  // (= GHL contact id) so the pill, persist, and read-overlay all agree.
  var seen={}; var cards=[];
  function st(key,fallback){ return (statusOverrides && statusOverrides[key]) || fallback || "onboarding"; }
  (C.hires||[]).forEach(function(h){
    seen[h.leadId]=true;
    cards.push({ key:h.leadId, openId:h.leadId, name:h.name, init:h.init,
      meta:[h.loc,h.type,h.days].filter(Boolean).join(" · "),
      start:h.start||"—", source:h.source||"Email", status:st(h.leadId, h.status) });
  });
  (leads||[]).filter(function(l){return /hired|won/i.test(l.stage||"");}).forEach(function(l){
    var key=l.ghlId||l.id; if(seen[key]) return; seen[key]=true; var hr=l.hire||{};
    cards.push({ key:key, openId:l.id, name:l.name, init:l.init,
      meta:[l.loc, hr.type||"Full-time", hr.days].filter(Boolean).join(" · "),
      start:hr.start||"—", source:hr.source||(l.chan==="sms"?"SMS":"Email"), status:st(key) });
  });
  if(!cards.length) return (
    <div className="stack-gap"><div className="pcard"><div className="pcard__body muted sm" style={{padding:'26px 22px',textAlign:'center'}}>No hires yet. Move a doctor to the <b style={{color:'var(--ink-2)'}}>Hired</b> stage in the pipeline and they'll appear here automatically.</div></div></div>
  );
  return (
    <div className="stack-gap">
      <div className="grid-2">
        {cards.map(function(h,i){
          return (
            <div className="pcard hover" key={h.key||i} style={{cursor:'pointer'}} onClick={function(){openLead(h.openId);}}><div className="pcard__body" style={{display:'flex',gap:16,alignItems:'flex-start'}}>
              <span className="av" style={{width:52,height:52,borderRadius:14,background:'var(--accent-wash)',color:'var(--accent-ink)',display:'flex',alignItems:'center',justifyContent:'center',fontWeight:700,fontSize:16,flex:'0 0 auto'}}>{h.init}</span>
              <div style={{flex:1}}>
                <div style={{display:'flex',justifyContent:'space-between',gap:10,alignItems:'flex-start'}}>
                  <div><b style={{fontSize:'1.05rem',fontWeight:600,color:'var(--ink)'}}>{h.name}</b><div className="muted xs" style={{marginTop:2}}>{h.meta}</div></div>
                  <StatusPill value={h.status} onChange={function(v){ onStatus&&onStatus(h.key,v); }} />
                </div>
                <div style={{display:'flex',gap:18,marginTop:14,flexWrap:'wrap'}}>
                  <div><div className="muted" style={{fontSize:'11px',textTransform:'uppercase',letterSpacing:'0.05em',fontWeight:600}}>Start</div><b style={{fontSize:'var(--fs-sm)'}}>{h.start}</b></div>
                  <div><div className="muted" style={{fontSize:'11px',textTransform:'uppercase',letterSpacing:'0.05em',fontWeight:600}}>Won via</div><b style={{fontSize:'var(--fs-sm)'}}>{h.source}</b></div>
                </div>
              </div>
            </div></div>
          );
        })}
      </div>
    </div>
  );
}

function Doctors({leads, openLead}){
  const [filter,setFilter]=React.useState("All");
  const stages=["All","Positive reply","Conversation","Interview","Hired","Sequenced","Opted out"];
  const rows=leads.filter(function(d){ return filter==="All" || d.stage.indexOf(filter)===0; });
  return (
    <PCard pad={false}
      title="Sourced doctors"
      sub={leads.length+" doctors · AHPRA-checked for VR status"}
      action={<button className="btn btn--ghost btn--sm" onClick={function(){window.ryToast("Exported "+rows.length+" doctors to CSV");}}><Icon n="download" /> Export</button>}>
      <div style={{padding:'14px 18px',borderBottom:'1px solid var(--line-soft)',display:'flex',gap:8,flexWrap:'wrap'}}>
        {stages.map(function(s){ return <span key={s} className={"chip"+(filter===s?" active":"")} onClick={function(){setFilter(s);}} style={{cursor:'pointer'}}>{s}</span>; })}
      </div>
      <div className="tbl-wrap">
        <table className="tbl">
          <thead><tr><th>Doctor</th><th>Location</th><th>Fellowship</th><th>Stage</th><th>Channel</th><th>Last activity</th><th>Sentiment</th></tr></thead>
          <tbody>
            {rows.map(function(d){
              return (
                <tr key={d.id} onClick={function(){openLead(d.id);}}>
                  <td><div className="docname"><span className="av">{d.init}</span><div><b>{d.name}</b><span>{d.spec}</span></div></div></td>
                  <td className="muted">{d.loc}</td>
                  <td><Tag kind="neutral">{d.vr}</Tag></td>
                  <td style={{fontWeight:500,color:'var(--ink-2)'}}>{d.stage}</td>
                  <td><Chan c={d.chan} /></td>
                  <td className="muted">{d.last}</td>
                  <td>{sentimentTag(d.sentiment)}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </PCard>
  );
}

/* =====================================================================
   APP SHELL
===================================================================== */
function Integrations(){
  const [list,setList]=React.useState(C.integrations||[]);
  const [modal,setModal]=React.useState(null);
  const cfg = window.REYAH_CONFIG || {};
  const live = !cfg.useMock;
  function connect(it){
    // Live Google Calendar → navigate the browser to the OAuth consent endpoint.
    if(live && it.id==="gcal"){
      const tid = (window.RYH && window.RYH.tenantId) || "";
      const base = (cfg.connect && cfg.connect.googleCalendar) || "/api/connect/google";
      window.location.href = base + (base.indexOf("?")>=0?"&":"?") + "tenantId=" + encodeURIComponent(tid);
      return;
    }
    // Mock / other: optimistic local toggle.
    setList(list.map(x=>x.id===it.id?Object.assign({},x,{status:"connected"}):x));
    setModal(null);
  }
  async function disconnect(it){
    if(live && it.id==="gcal"){
      try {
        const token = await window.REYAH_AUTH.csrf();
        const url = (cfg.connect && cfg.connect.googleCalendarDisconnect) || "/api/connect/google/disconnect";
        const res = await fetch(url, {
          method:"POST",
          headers:{ "Content-Type":"application/json", "x-csrf-token":token },
          credentials:"include",
          body: JSON.stringify({ tenantId:(window.RYH && window.RYH.tenantId) || "" })
        });
        if(!res.ok) throw new Error("disconnect failed");
        window.ryToast("Google Calendar disconnected");
      } catch(e){ window.ryToast("Couldn't disconnect Google Calendar"); return; }
    }
    setList(list.map(x=>x.id===it.id?Object.assign({},x,{status:"disconnected"}):x));
  }
  return (
    <div className="stack-gap">
      <div className="pcard"><div className="pcard__body" style={{display:'flex',gap:16,alignItems:'flex-start',flexWrap:'wrap'}}>
        <span style={{width:42,height:42,borderRadius:11,background:'var(--accent-wash)',color:'var(--accent-2)',display:'flex',alignItems:'center',justifyContent:'center',fontSize:19,flex:'0 0 auto'}}><Icon n="globe" /></span>
        <div style={{flex:1,minWidth:240}}>
          <b style={{fontWeight:600,color:'var(--ink)'}}>Connect your tools</b>
          <p style={{fontSize:'var(--fs-sm)',color:'var(--muted)',marginTop:3,maxWidth:'72ch'}}>Reyah operates your email and CRM for you. Connect your Google Calendar so every call booking and interview lands in your own diary automatically.</p>
        </div>
      </div></div>
      <div className="intg-grid">
        {list.map(function(it){ return <IntegrationCard key={it.id} it={it} data={C} onConnect={setModal} onDisconnect={disconnect} />; })}
      </div>
      {modal && <ConnectModal it={modal} onClose={function(){setModal(null);}} onConfirm={connect} />}
    </div>
  );
}

function Settings(){
  const [tab,setTab]=React.useState("Account");
  const tabs=["Account","Notifications","Campaign","Integrations","Security"];
  const [s,setS]=React.useState({
    name:C.name, contact:C.user.name, email:"anita@mylocaldoc.com.au", tz:"AEST (Melbourne)",
    nEmail:true, nSms:true, nSlack:true, nDaily:false, reAlert:true,
    sender:"Dr Lena Park", target:"Same-day (24h ceiling)", autopause:true,
    twofa:true
  });
  function up(k,v){ setS(Object.assign({},s,{[k]:v})); }
  return (
    <div className="stack-gap" style={{maxWidth:960, margin:'0 auto', width:'100%'}}>
      <div className="settabs">{tabs.map(function(x){ return <button key={x} className={tab===x?"active":""} onClick={function(){setTab(x);}}>{x}</button>; })}</div>
      {tab==="Integrations" ? <Integrations /> : <React.Fragment>
      <PCard>
        {tab==="Account" && <div>
          <SRow title="Practice name"><input className="setinput" value={s.name} onChange={function(e){up('name',e.target.value);}} /></SRow>
          <SRow title="Primary contact"><input className="setinput" value={s.contact} onChange={function(e){up('contact',e.target.value);}} /></SRow>
          <SRow title="Login email" desc="Used for sign-in and alerts."><input className="setinput" type="email" value={s.email} onChange={function(e){up('email',e.target.value);}} /></SRow>
          <SRow title="Time zone" desc="Drives report dates and send windows."><RSelect value={s.tz} options={["AEST (Melbourne)","AEST (Sydney)","AWST (Perth)","NZST (Auckland)"]} onChange={function(v){up('tz',v);}} /></SRow>
        </div>}
        {tab==="Notifications" && <div>
          <SRow title="Email alerts" desc="Positive replies and daily activity."><Toggle on={s.nEmail} onChange={function(v){up('nEmail',v);}} /></SRow>
          <SRow title="SMS alerts" desc="Urgent positive-reply alerts by text."><Toggle on={s.nSms} onChange={function(v){up('nSms',v);}} /></SRow>
          <SRow title="Slack alerts" desc="Post positive replies to your team channel."><Toggle on={s.nSlack} onChange={function(v){up('nSlack',v);}} /></SRow>
          <SRow title="Re-alert until actioned" desc="Re-notify at 4h, 12h and 24h until you respond."><Toggle on={s.reAlert} onChange={function(v){up('reAlert',v);}} /></SRow>
          <SRow title="Daily summary email" desc="A 7am digest of yesterday's campaign activity."><Toggle on={s.nDaily} onChange={function(v){up('nDaily',v);}} /></SRow>
        </div>}
        {tab==="Campaign" && <div>
          <SRow title="Outreach sender name" desc="Doctor-to-doctor messaging always reads from a real doctor, never Reyah."><input className="setinput" value={s.sender} onChange={function(e){up('sender',e.target.value);}} /></SRow>
          <SRow title="Response-time target" desc="Reply speed is the #1 conversion lever."><RSelect value={s.target} options={["Same-day (24h ceiling)","Within 4 hours","Within 1 hour"]} onChange={function(v){up('target',v);}} /></SRow>
          <SRow title="Auto-pause on opt-out" desc="Immediately suppress a doctor who replies STOP / unsubscribe."><Toggle on={s.autopause} onChange={function(v){up('autopause',v);}} /></SRow>
        </div>}
        {tab==="Security" && <div>
          <SRow title="Two-factor authentication" desc="Require a one-time code at sign-in."><Toggle on={s.twofa} onChange={function(v){up('twofa',v);}} /></SRow>
          <SRow title="Password" desc="Last changed 3 weeks ago."><button className="btn btn--ghost btn--sm" onClick={function(){window.ryToast("Password reset link sent to your email");}}>Change password</button></SRow>
          <SRow title="Compliance" desc="How Reyah keeps your outreach compliant by design."><a className="btn btn--ghost btn--sm" href={SITE+"/compliance/index.html"} target="_blank" rel="noopener">View policy</a></SRow>
          <SRow title="Privacy" desc="How your data is handled."><a className="btn btn--ghost btn--sm" href={SITE+"/privacy/index.html"} target="_blank" rel="noopener">View privacy</a></SRow>
        </div>}
      </PCard>
      <div style={{display:'flex',justifyContent:'flex-end',gap:10}}>
        <button className="btn btn--ghost btn--sm" onClick={function(){window.ryToast("Changes discarded");}}>Cancel</button>
        <button className="btn btn--primary btn--sm" onClick={function(){window.ryToast("Settings saved");}}>Save changes</button>
      </div>
      </React.Fragment>}
    </div>
  );
}

var _mid=0;  // monotonic id for outbound message bubbles (per session)
function ClientApp(){
  const [screen,setScreen]=React.useState("overview");
  const [metric,setMetric]=React.useState(null);
  const [noti,setNoti]=React.useState(false);
  const [notifs,setNotifs]=React.useState(C.notifications);
  const [leads,setLeads]=React.useState(C.leads);
  const [leadId,setLeadId]=React.useState(null);
  const [inboxLeadId,setInboxLeadId]=React.useState(null);
  const [hireStatus,setHireStatus]=React.useState({});
  function setHireStatusFor(leadId,status){
    var prev;
    setHireStatus(function(m){ prev=m[leadId]; var n=Object.assign({},m); n[leadId]=status; return n; });
    window.REYAH_DATA.act("hireStatus",{leadId:leadId, status:status})
      .then(function(){ window.ryToast(status==="onboarding"?"Marked Onboarding":"Marked Guarantee active"); })
      .catch(function(){
        setHireStatus(function(m){ var n=Object.assign({},m); if(prev===undefined){delete n[leadId];}else{n[leadId]=prev;} return n; });
        window.ryToast("Couldn't save status — reverted");
      });
  }
  // Browser-history wiring: mirror the active view into history so Back walks
  // sections / closes the lead drawer instead of leaving the app to sign-in.
  const popping = React.useRef(false);
  React.useEffect(function(){
    if(popping.current){ popping.current=false; return; }
    var st={screen:screen, leadId:leadId};
    if(window.history.state){ window.history.pushState(st,""); } else { window.history.replaceState(st,""); }
  },[screen,leadId]);
  React.useEffect(function(){
    function onPop(e){ popping.current=true; var st=e.state||{}; setScreen(st.screen||"overview"); setLeadId(st.leadId||null); setMetric(null); setNoti(false); }
    window.addEventListener("popstate",onPop);
    return function(){ window.removeEventListener("popstate",onPop); };
  },[]);
  const unread=notifs.filter(function(n){return n.unread;}).length;
  const t=TITLES[screen];
  const lead=leadId?leads.find(function(l){return l.id===leadId;}):null;

  const searchIndex=React.useMemo(function(){
    function stageKind(s){ if(/positive|interview|hired/i.test(s)) return "pos"; if(/opted out/i.test(s)) return "bad"; return "neutral"; }
    function clip(s,n){ return s&&s.length>n?s.slice(0,n-1)+"…":s; }
    var idx=[];
    leads.forEach(function(l){
      var thread=(l.thread||[]).map(function(m){return m.t;}).join(" ");
      var kw=[l.name,l.spec,l.vr,l.loc,l.email,l.phone,l.stage,l.notes,l.owner,thread].join(" ").toLowerCase();
      idx.push({id:"doc-"+l.id, cat:"Doctors", av:l.init, label:l.name, sub:[l.spec,l.loc].filter(Boolean).join(" · "),
        tag:l.stage, tagKind:stageKind(l.stage), kw:kw, onPick:function(){ setLeadId(l.id); }});
      var reply=(l.thread||[]).filter(function(m){return !m.us;}).slice(-1)[0];
      if(reply||l.unread){
        idx.push({id:"rep-"+l.id, cat:"Replies", av:l.init, label:l.name, sub:reply?clip(reply.t,64):"Awaiting reply",
          tag:l.unread?"Unread":null, tagKind:"accent", kw:kw, onPick:function(){ goInbox(l.id); }});
      }
    });
    (C.bookings||[]).forEach(function(b){
      idx.push({id:"bk-"+b.leadId, cat:"Bookings", av:b.init, label:b.name, sub:[b.type,b.day+" "+b.time].join(" · "),
        tag:b.status, tagKind:b.status==="confirmed"?"pos":"neutral",
        kw:[b.name,b.type,b.day,b.time,b.mode,b.with,b.status].join(" ").toLowerCase(), onPick:function(){ setLeadId(b.leadId); }});
    });
    (C.hires||[]).forEach(function(h){
      idx.push({id:"hr-"+h.leadId, cat:"Hires", av:h.init, label:h.name, sub:[h.type,h.loc,h.start].filter(Boolean).join(" · "),
        tag:h.status, tagKind:"pos",
        kw:[h.name,h.loc,h.type,h.start,h.days,h.source,h.status].join(" ").toLowerCase(), onPick:function(){ setLeadId(h.leadId); }});
    });
    if(C.campaign){
      idx.push({id:"camp", cat:"Campaigns", av:C.initials, label:C.campaign, sub:[C.launched,C.plan].filter(Boolean).join(" · "),
        kw:[C.campaign,C.name,C.plan,C.launched].join(" ").toLowerCase(), onPick:function(){ setScreen("overview"); }});
    }
    return idx;
  },[leads]);

  function updateLead(id,patch){ setLeads(function(ls){return ls.map(function(l){return l.id===id?Object.assign({},l,patch):l;});}); }
  function moveStage(id,stage){
    var l=leads.find(function(x){return x.id===id;});
    var prev=l&&l.stage;
    updateLead(id,{stage:stage});
    window.REYAH_DATA.act("stage",{leadId:l&&l.id, ghlId:l&&l.ghlId, stage:stage})
      .then(function(){ window.ryToast((l?l.name:"Doctor")+" moved to "+stage+" · synced to GoHighLevel"); })
      .catch(function(){ if(prev!=null) updateLead(id,{stage:prev}); window.ryToast("Couldn't sync to GoHighLevel — move reverted"); });
  }
  function saveNote(id,note){
    var l=leads.find(function(x){return x.id===id;});
    var prev=l?l.notes:undefined;
    updateLead(id,{notes:note});
    window.REYAH_DATA.act("note",{leadId:l&&l.id, ghlId:l&&l.ghlId, note:note})
      .then(function(){ window.ryToast("Note saved · synced to GoHighLevel"); })
      .catch(function(){ updateLead(id,{notes:prev}); window.ryToast("Couldn't save note to GoHighLevel"); });
  }
  function markRead(id){ updateLead(id,{unread:false}); window.REYAH_DATA.act("read",{leadId:id, read:true}).catch(function(){}); }
  // Patch a single bubble (matched by id) inside a lead's thread.
  function setBubble(id,mid,patch){
    setLeads(function(ls){return ls.map(function(x){
      if(x.id!==id) return x;
      return Object.assign({},x,{thread:x.thread.map(function(b){ return b.id===mid?Object.assign({},b,patch):b; })});
    });});
  }
  // Fire the provider send for one bubble; flip status sent/failed on settle.
  // Failure stays on the bubble (Retry), so no error toast here.
  function sendBubble(id,msg){
    var l=leads.find(function(x){return x.id===id;});
    var payload=(msg.chan==="email")
      ? {leadId:l&&l.id, ghlId:l&&l.ghlId, body:msg.t, subject:msg.subject, preheader:msg.preheader, fromDomain:msg.fromDomain, attachments:msg.attachments}
      : {leadId:l&&l.id, ghlId:l&&l.ghlId, body:msg.t};
    window.REYAH_DATA.act(msg.chan==="sms"?"sms":"email",payload)
      .then(function(){ setBubble(id,msg.id,{status:"sent"}); window.ryToast(msg.chan==="email"?"Email sent · via PlusVibe":"SMS sent · via GHL"); })
      .catch(function(){ setBubble(id,msg.id,{status:"failed"}); });
  }
  function appendMsg(id,msg){
    var m=Object.assign({},msg,{id:"m"+(++_mid), status:"sending"});
    setLeads(function(ls){return ls.map(function(x){ return x.id===id?Object.assign({},x,{thread:x.thread.concat([m]), unread:false, last:"now"}):x; });});
    sendBubble(id,m);
  }
  function retryMsg(id,mid){
    var l=leads.find(function(x){return x.id===id;});
    var b=l&&l.thread.find(function(x){return x.id===mid;});
    if(!b || b.status==="sending") return;  // ignore retry while a send is in flight
    setBubble(id,mid,{status:"sending"});
    sendBubble(id,b);
  }
  function goInbox(id){ setInboxLeadId(id); setLeadId(null); setNoti(false); setScreen("replies"); }
  function sendChannel(k,l){
    if(k==="email"||k==="sms"){ setLeadId(null); setInboxLeadId(l.id); setScreen("replies"); window.ryToast("Composing "+(k==="email"?"email via PlusVibe":"SMS via GHL")+" to "+l.name); return; }
    var m={call:"Calling "+l.name+" via GoHighLevel…", voicemail:"Ringless voicemail queued to "+l.name+" · via GHL", voicenote:"Voice note sent to "+l.name+" · via GHL"};
    // NOTE(backend-contract): the /send/* schema requires a `body` (message text)
    // for every channel — but call/voicemail/voicenote are click-to-initiate and
    // the UI has no text body to supply, so these currently get a 422. Identifiers
    // are correct; resolving needs the backend to make `body` optional for voice
    // channels (or the UI to collect a script). See VERIFICATION.md escalations.
    window.REYAH_DATA.act(k,{leadId:l.id, ghlId:l.ghlId, phone:l.phone})
      .then(function(){ window.ryToast(m[k]||"Done"); })
      .catch(function(){ window.ryToast("Couldn't reach GoHighLevel — please try again"); });
  }
  function render(){
    switch(screen){
      case "overview": return <Overview openMetric={setMetric} leads={leads} goInbox={goInbox} />;
      case "deliverability": return <Deliverability openMetric={setMetric} />;
      case "replies": return <Replies key={inboxLeadId||"inbox"} leads={leads} domains={C.domains} initLeadId={inboxLeadId} onAppend={appendMsg} onRead={markRead} sendChannel={sendChannel} openLead={setLeadId} onRetry={retryMsg} />;
      case "pipeline": return <Pipeline leads={leads} stages={C.pipelineStages} onMove={moveStage} openLead={setLeadId} />;
      case "bookings": return <Bookings openLead={setLeadId} />;
      case "hires": return <Hires openLead={setLeadId} leads={leads} statusOverrides={hireStatus} onStatus={setHireStatusFor} />;
      case "doctors": return <Doctors leads={leads} openLead={setLeadId} />;
      case "integrations": return <Integrations />;
      case "settings": return <Settings />;
      default: return null;
    }
  }
  // Live Inbox badge: count unread doctor threads; the pill (and its red "hot"
  // state) disappears once every reply has been read.
  var inboxUnread = leads.filter(function(l){ return l.thread && l.thread.length>0 && l.unread; }).length;
  var navItems = NAV.map(function(g){ return Object.assign({}, g, { links: g.links.map(function(l){
    return l.id==="replies" ? Object.assign({}, l, { count: inboxUnread||null, hot: inboxUnread>0 }) : l;
  }) }); });
  return (
    <div className="shell">
      <Sidebar
        org={{name:C.name, sub:C.plan, init:C.initials, onClick:function(){ window.ryToast("Single-clinic workspace on this plan"); }}}
        items={navItems} active={screen} onNav={function(s){ setScreen(s); setLeadId(null); setNoti(false); }} user={C.user}
        footerExtra={<a className="navitem" href={SITE+"/index.html"} style={{marginBottom:6}}><Icon n="globe" /><span>Back to site</span></a>} />
      <div className="main">
        <Topbar title={t[0]} sub={t[1]} searchIndex={searchIndex} right={
          <React.Fragment>
            <span className="tag tag--accent" style={{marginRight:4}}><span className="d" />{C.launched}</span>
            <button className="iconbtn" aria-label="Notifications" onClick={function(){setNoti(!noti);}}><Icon n="bell" />{unread>0 && <span className="dot" />}</button>
          </React.Fragment>
        } />
        <div className="content">{render()}</div>
      </div>
      {metric && <MetricDrawer metric={metric} onClose={function(){setMetric(null);}} />}
      {lead && <LeadDrawer lead={lead} stages={C.pipelineStages} onClose={function(){setLeadId(null);}} onMoveStage={moveStage} onSaveNote={saveNote} onSend={sendChannel} onOpenInbox={function(l){ setLeadId(null); setInboxLeadId(l.id); setScreen("replies"); }} />}
      {noti && <NotificationsPanel items={notifs} onClose={function(){setNoti(false);}} onAllRead={function(){setNotifs(notifs.map(function(n){return Object.assign({},n,{unread:false});}));}} />}
    </div>
  );
}

/* ---- bootstrap: guard session (live) → load canonical data → render ----
   Mock mode: REYAH_DATA.load() returns window.REYAH_MOCK instantly, no guard.
   Live mode: GET /api/me first (401/wrong-role redirect handled in requireSession),
   then GET /api/dashboard. A 501 → "Coming soon"; any other failure → error screen
   with retry. The loading splash in #root shows until the first render replaces it. */
(async function bootstrap(){
  const cfg = window.REYAH_CONFIG || { useMock:true };
  const root = ReactDOM.createRoot(document.getElementById("root"));

  if (!cfg.useMock){
    try {
      const me = await window.REYAH_AUTH.requireSession("client");
      if (!me) return; // a redirect is in flight
    } catch (e) {
      root.render(<BootScreen kind="error" message="We couldn't verify your session. Please try again." onRetry={function(){ window.location.reload(); }} />);
      return;
    }
  }

  try {
    const d = await window.REYAH_DATA.load();
    if (d && d.client) { window.RYH.client = d.client; }
    if (d && d.admin)  { window.RYH.admin  = d.admin;  }
  } catch (e) {
    if (cfg.useMock) {
      console.error("[Reyah] mock load failed:", e); // keep prototype resilient
    } else if (e && e.status === 501) {
      root.render(<BootScreen kind="comingsoon" message="This workspace is being provisioned. Check back shortly." />);
      return;
    } else {
      root.render(<BootScreen kind="error" message="We couldn't load your dashboard. Please try again." onRetry={function(){ window.location.reload(); }} />);
      return;
    }
  }

  C = window.RYH.client;          // re-bind to whatever load() returned
  METRICS = buildMetrics();
  NAV = buildNav();
  root.render(<ClientApp />);
})();
