/* ============================================================
   Shipstar UI kit — React components shared across all targets.
   Loaded after React + Babel, before each page's app script.
   Exposes everything on window so multiple babel scripts can share.
   ============================================================ */
const { useState, useEffect, useRef, useCallback, useLayoutEffect } = React;

/* ---------------- Icons (stroke, 1.6, Lucide-ish) ---------------- */
const ICONS = {
  x:'M5 5l14 14M19 5L5 19',
  check:'M5 12.5l4.5 4.5L19 7',
  copy:'M9 9h10v10H9zM5 15V5h10',
  alert:'M12 8v5M12 16.5v.5M10.3 4l-7 12a2 2 0 001.7 3h14a2 2 0 001.7-3l-7-12a2 2 0 00-3.4 0z',
  info:'M12 11v6M12 7.5v.5M12 21a9 9 0 100-18 9 9 0 000 18z',
  warn:'M12 9v4M12 16.5v.5M12 21a9 9 0 100-18 9 9 0 000 18z',
  download:'M12 4v11M7 11l5 5 5-5M5 20h14',
  pencil:'M4 20h4L18.5 9.5a2 2 0 00-3-3L5 17v3z',
  trash:'M4 7h16M9 7V5h6v2M6 7l1 13h10l1-13',
  truck:'M3 6h11v9H3zM14 9h4l3 3v3h-7M6.5 18.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM17.5 18.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z',
  box:'M3.5 8L12 4l8.5 4-8.5 4zM3.5 8v8l8.5 4M20.5 8v8l-8.5 4M12 12v8',
  user:'M12 12a4 4 0 100-8 4 4 0 000 8zM5 20a7 7 0 0114 0',
  arrowR:'M5 12h14M13 6l6 6-6 6',
  arrowL:'M19 12H5M11 6l-6 6 6 6',
  chevR:'M9 6l6 6-6 6',
  clock:'M12 7v5l3 2M12 21a9 9 0 100-18 9 9 0 000 18z',
  doc:'M14 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V8zM14 3v5h5',
  spark:'M12 3l2.2 6.3L20.5 12l-6.3 2.2L12 20.5l-2.2-6.3L3.5 12l6.3-2.2z',
  cart:'M5 6h15l-1.6 8H7zM7 6L6 3H3M8.5 20a1 1 0 100-2 1 1 0 000 2zM17 20a1 1 0 100-2 1 1 0 000 2z',
  pin:'M12 21s7-6.2 7-11a7 7 0 10-14 0c0 4.8 7 11 7 11zM12 12.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5z',
  calendar:'M4 7h16v13H4zM4 7l0-2h16v2M8 3v4M16 3v4M8 12h2M14 12h2M8 16h2M14 16h2',
  bolt:'M13 2L4 14h6l-1 8 9-12h-6z',
  ship:'M3 16l1.5-5h15L21 16M5 11V7h6l3 4M12 4v3M6 19c1.2 1 2 1 3 0s1.8-1 3 0 1.8 1 3 0 1.8-1 3 0',
  search:'M11 18a7 7 0 100-14 7 7 0 000 14zM20 20l-4-4',
  bell:'M18 9a6 6 0 10-12 0c0 7-3 8-3 8h18s-3-1-3-8M13.7 21a2 2 0 01-3.4 0',
  menu:'M3 6h18M3 12h18M3 18h18',
  chat:'M21 12a8 8 0 01-11.5 7.2L3 21l1.8-6.5A8 8 0 1121 12z',
  plus:'M12 5v14M5 12h14',
  filter:'M3 5h18l-7 8v5l-4 2v-7z',
  scan:'M4 8V5a1 1 0 011-1h3M16 4h3a1 1 0 011 1v3M20 16v3a1 1 0 01-1 1h-3M8 20H5a1 1 0 01-1-1v-3M7 12h10',
};
function Icon({ name, size=18, className='', style={}, stroke=1.6 }){
  const d = ICONS[name] || ICONS.info;
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round"
      className={className} style={style} aria-hidden="true">
      {d.split('M').filter(Boolean).map((seg,i)=><path key={i} d={'M'+seg} />)}
    </svg>
  );
}

/* ---------------- Button (loading-aware) ---------------- */
function Button({ variant='ghost', size='', loading=false, icon, iconRight, block=false, children, className='', ...rest }){
  const cls = ['btn','btn-'+variant, size && 'btn-'+size, block && 'btn-block', className].filter(Boolean).join(' ');
  return (
    <button className={cls} disabled={loading||rest.disabled} {...rest}>
      {loading && <span className="spin" />}
      {!loading && icon && <Icon name={icon} size={size==='lg'?18:16} />}
      {children}
      {!loading && iconRight && <Icon name={iconRight} size={16} />}
    </button>
  );
}

/* ---------------- Pill ---------------- */
function Pill({ tone='gray', dot=false, children }){
  return <span className={'pill pill-'+tone}>{dot && <span className="dot" />}{children}</span>;
}

/* ---------------- KV row ---------------- */
function KV({ k, label, children, value, mono=false }){
  return (
    <div className="kv">
      <span className="kv-k">{k||label}</span>
      <span className="kv-v" style={mono?{fontFamily:'var(--font-mono)'}:null}>{children!==undefined?children:value}</span>
    </div>
  );
}

/* ---------------- Field wrapper ---------------- */
function Field({ label, hint, children, htmlFor }){
  return (
    <div style={{marginBottom:14}}>
      {label && <label className="lbl" htmlFor={htmlFor}>{label}</label>}
      {children}
      {hint && <div className="field-hint">{hint}</div>}
    </div>
  );
}

/* ---------------- InfoCallout ---------------- */
function InfoCallout({ tone='blue', icon, children, title }){
  const ic = icon || (tone==='red'?'alert':tone==='amber'?'warn':tone==='green'?'check':'info');
  return (
    <div className={'callout callout-'+tone}>
      <Icon name={ic} size={16} className="callout-icn" />
      <div>{title && <strong style={{display:'block',marginBottom:2}}>{title}</strong>}{children}</div>
    </div>
  );
}

/* ---------------- CopyButton ---------------- */
function CopyButton({ text, label }){
  const [done,setDone] = useState(false);
  const copy = ()=>{
    const t = text;
    try{ navigator.clipboard?.writeText(t); }catch(e){}
    setDone(true); setTimeout(()=>setDone(false),1400);
  };
  return (
    <button className="copybtn" onClick={copy} title="Copy">
      <span>{label||text}</span>
      <Icon name={done?'check':'copy'} size={14} className="ic" style={done?{color:'var(--green)'}:null} />
      {done && <span style={{fontFamily:'var(--font)',fontSize:11,color:'var(--green)',fontWeight:600}}>copied</span>}
    </button>
  );
}

/* ---------------- Modal shell ---------------- */
function Modal({ open=true, onClose, size='md', icon, iconTone='brand', eyebrow, title, subtitle,
                children, footer, confirm=false, dismissable=true, hideClose=false }){
  const [shown,setShown] = useState(false);
  const ref = useRef(null);
  const prevFocus = useRef(null);
  const dragY = useRef(0);

  const requestClose = useCallback(()=>{
    if(!dismissable) return;
    setShown(false);
    setTimeout(()=>{ onClose && onClose(); }, 200);
  },[onClose,dismissable]);

  useEffect(()=>{
    prevFocus.current = document.activeElement;
    const sbw = window.innerWidth - document.documentElement.clientWidth;
    document.body.style.overflow='hidden';
    if(sbw>0) document.body.style.paddingRight = sbw+'px';
    const r = setTimeout(()=>setShown(true), 16);
    // focus first focusable
    const t = setTimeout(()=>{
      const f = ref.current?.querySelector('input,select,textarea,button,[tabindex]');
      f?.focus();
    },80);
    return ()=>{
      clearTimeout(r); clearTimeout(t);
      document.body.style.overflow='';
      document.body.style.paddingRight='';
      try{ prevFocus.current?.focus(); }catch(e){}
    };
  },[]);

  const onKey = (e)=>{
    if(e.key==='Escape'){ e.stopPropagation(); requestClose(); }
    if(e.key==='Tab'){
      const f = [...ref.current.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')];
      if(!f.length) return;
      const first=f[0], last=f[f.length-1];
      if(e.shiftKey && document.activeElement===first){ e.preventDefault(); last.focus(); }
      else if(!e.shiftKey && document.activeElement===last){ e.preventDefault(); first.focus(); }
    }
  };

  // bottom-sheet drag-to-dismiss (mobile)
  const onGripDown = (e)=>{
    const startY = (e.touches?e.touches[0]:e).clientY;
    const sheet = ref.current;
    const move=(ev)=>{
      const y=(ev.touches?ev.touches[0]:ev).clientY - startY;
      if(y>0){ dragY.current=y; sheet.style.transform=`translateY(${y}px)`; sheet.style.transition='none'; }
    };
    const up=()=>{
      sheet.style.transition='';
      if(dragY.current>90){ requestClose(); } else { sheet.style.transform=''; }
      dragY.current=0;
      window.removeEventListener('mousemove',move); window.removeEventListener('mouseup',up);
      window.removeEventListener('touchmove',move); window.removeEventListener('touchend',up);
    };
    window.addEventListener('mousemove',move); window.addEventListener('mouseup',up);
    window.addEventListener('touchmove',move,{passive:false}); window.addEventListener('touchend',up);
  };

  return (
    <div className={'modal-backdrop'+(confirm?' confirm':'')+(shown?' in':'')}
         onMouseDown={(e)=>{ if(e.target===e.currentTarget) requestClose(); }}
         onKeyDown={onKey}>
      <div className={'modal modal-'+size} ref={ref} role="dialog" aria-modal="true"
           aria-label={title} onMouseDown={e=>e.stopPropagation()}>
        <div className="modal-grip" onMouseDown={onGripDown} onTouchStart={onGripDown} />
        {(title||eyebrow||icon) &&          <div className="modal-head">
            {icon && <div className="modal-head-icn" style={iconTone!=='brand'?headIconTone(iconTone):null}><Icon name={icon} size={19} /></div>}
            <div style={{flex:1,minWidth:0}}>
              {eyebrow && <div className="modal-eyebrow">{eyebrow}</div>}
              {title && <div className="modal-title">{title}</div>}
              {subtitle && <div className="modal-sub">{subtitle}</div>}
            </div>
            {!hideClose && dismissable && <button className="modal-x" onClick={requestClose} aria-label="Close"><Icon name="x" size={18} /></button>}
          </div>}
        {children && <div className="modal-body">{children}</div>}
        {footer && <div className="modal-foot">{footer}</div>}      </div>
    </div>
  );
}
function headIconTone(tone){
  const map={ green:['var(--green-soft)','#0f6b40'], red:['var(--red-soft)','#a82a24'],
    amber:['var(--amber-soft)','#9a5b16'], blue:['var(--blue-soft)','#1a40b0'], fg:['#11203510','var(--fg)'] };
  const [bg,fg]=map[tone]||map.blue; return {background:bg,color:fg};
}

/* ---------------- Confirm dialog ---------------- */
function ConfirmDialog({ tone='red', icon='alert', title, message, confirmLabel='Confirm', cancelLabel='Cancel',
                        onConfirm, onClose, busy=false }){
  return (
    <Modal size="sm" confirm icon={icon} iconTone={tone} title={title} subtitle={message}
      onClose={onClose}
      footer={<>
        <Button variant="ghost" onClick={onClose}>{cancelLabel}</Button>
        <Button variant={tone==='red'?'danger':'primary'} loading={busy} onClick={onConfirm}>{confirmLabel}</Button>
      </>}>
      {null}
    </Modal>
  );
}

/* ---------------- Tabs (animated underline + crossfade) ---------------- */
function Tabs({ tabs, value, onChange }){
  const wrap = useRef(null);
  const [ink,setInk] = useState({left:0,width:0});
  useLayoutEffect(()=>{
    const i = tabs.findIndex(t=>t.id===value);
    const el = wrap.current?.children[i];
    if(el) setInk({left:el.offsetLeft, width:el.offsetWidth});
  },[value,tabs]);
  return (
    <div className="tabs" ref={wrap}>
      {tabs.map(t=>(
        <button key={t.id} className={'tab'+(t.id===value?' active':'')} onClick={()=>onChange(t.id)}>{t.label}</button>
      ))}
      <span className="tab-ink" style={{left:ink.left,width:ink.width}} />
    </div>
  );
}

/* ---------------- Skeleton ---------------- */
function Skeleton({ h=16, w='100%', r=6, style={} }){
  return <div className="sk" style={{height:h,width:w,borderRadius:r,...style}} />;
}

/* ---------------- EmptyState ---------------- */
function EmptyState({ icon='box', title, sub, cta, onCta }){
  return (
    <div style={{textAlign:'center',padding:'40px 20px',color:'var(--fg-3)'}}>
      <div style={{width:48,height:48,borderRadius:14,background:'var(--bg-2)',display:'inline-flex',
        alignItems:'center',justifyContent:'center',color:'var(--fg-3)',marginBottom:14}}>
        <Icon name={icon} size={24} />
      </div>
      <div style={{fontFamily:'var(--font-tight)',fontWeight:700,fontSize:15,color:'var(--fg)',marginBottom:4}}>{title}</div>
      {sub && <div style={{fontSize:13,maxWidth:300,margin:'0 auto 16px'}}>{sub}</div>}
      {cta && <Button variant="brand" size="sm" onClick={onCta}>{cta}</Button>}
    </div>
  );
}

/* ---------------- Toast host ---------------- */
function ToastHost(){
  const [items,setItems] = useState([]);
  useEffect(()=>{
    const h = (e)=>{
      const id = Math.random();
      setItems(s=>[...s,{id,...e.detail}]);
      setTimeout(()=>setItems(s=>s.filter(x=>x.id!==id)), e.detail.duration||2600);
    };
    window.addEventListener('shipstar-toast',h);
    return ()=>window.removeEventListener('shipstar-toast',h);
  },[]);
  return (
    <div className="toast-wrap">
      {items.map(it=>(
        <div className="toast" key={it.id}>
          {it.tone && <Icon name={it.tone==='green'?'check':'info'} size={16}
            style={{color:it.tone==='green'?'#7ee0a8':'#fff'}} />}
          {it.msg}
        </div>
      ))}
    </div>
  );
}
function toast(msg, tone='green', duration){
  window.dispatchEvent(new CustomEvent('shipstar-toast',{detail:{msg,tone,duration}}));
}

/* ---------------- count-up hook ---------------- */
function useCountUp(target, { duration=900, decimals=0, prefix='', suffix='', start=true }={}){
  const [val,setVal] = useState(start?0:target);
  const timer = useRef(null);
  useEffect(()=>{
    if(!start){ setVal(target); return; }
    const reduce = window.matchMedia('(prefers-reduced-motion:reduce)').matches;
    if(reduce){ setVal(target); return; }
    const t0 = Date.now(); const from = 0; const step = 1000/60;
    clearInterval(timer.current);
    timer.current = setInterval(()=>{
      const p = Math.min(1,(Date.now()-t0)/duration);
      const e = 1-Math.pow(1-p,3);
      setVal(from+(target-from)*e);
      if(p>=1) clearInterval(timer.current);
    }, step);
    return ()=>clearInterval(timer.current);
  },[target,start]);
  const fmt = prefix + Number(val).toLocaleString('en-US',{minimumFractionDigits:decimals,maximumFractionDigits:decimals}) + suffix;
  return fmt;
}

Object.assign(window, {
  Icon, Button, Pill, KV, Field, InfoCallout, CopyButton,
  Modal, ConfirmDialog, Tabs, Skeleton, EmptyState, ToastHost, toast, useCountUp, useRpc,
});

/* ErrorBoundary — one page's exception shows a Retry card, never blanks the app */
class ErrorBoundary extends React.Component {
  constructor(p){ super(p); this.state={err:null}; }
  static getDerivedStateFromError(err){ return {err}; }
  componentDidCatch(err){ try{ console.warn('Page error:', err && err.message); }catch(e){} }
  render(){
    if(this.state.err){
      return (
        <div className="page" style={{maxWidth:520}}>
          <div style={{background:'var(--bg-elev)',border:'1px solid var(--border)',borderRadius:'var(--r-md)',padding:'28px 24px',textAlign:'center'}}>
            <div style={{width:46,height:46,borderRadius:12,background:'var(--red-soft)',color:'var(--red)',display:'inline-flex',alignItems:'center',justifyContent:'center',marginBottom:14}}><Icon name="warn" size={24} /></div>
            <div style={{fontFamily:'var(--font-tight)',fontWeight:700,fontSize:16,marginBottom:5}}>This page hit an error</div>
            <div style={{fontSize:13,color:'var(--fg-3)',marginBottom:16,lineHeight:1.5}}>{String(this.state.err && this.state.err.message || 'Something went wrong rendering this view.')}</div>
            <Button variant="primary" icon="arrowR" onClick={()=>this.setState({err:null})}>Retry</Button>
          </div>
        </div>
      );
    }
    return this.props.children;
  }
}
Object.assign(window, { ErrorBoundary });

/* useRpc — load-time read against getSb().rpc (mock now, real on port). Never throws. */
function useRpc(name, args, deps){
  const [s,setS] = useState({ loading:true, data:null, error:null });
  useEffect(()=>{
    let live=true; setS(p=>({...p,loading:true}));
    Promise.resolve(window.getSb && window.getSb().rpc(name, args)).then(r=>{
      if(!live) return;
      setS({ loading:false, data:r?r.data:null, error:r?r.error:null });
      if(r && r.error) toast(r.error.message||'Request failed','red');
    });
    return ()=>{ live=false; };
  }, deps||[]);
  return s;
}
Object.assign(window, { useRpc });
