/* =========================================================================
   MIZUONE — CORE: ikon, helper, komponen bersama
   ========================================================================= */
const { useState, useEffect, useRef } = React;
const imgSrc = (k) => (window.MZ_MEDIA && window.MZ_MEDIA[k]) || 'assets/' + k;
const imgOpt = (k) => (window.MZ_MEDIA && window.MZ_MEDIA[k]) || undefined;

/* ---- helper format ---- */
const fmtRp = (n)=> 'Rp ' + Number(n).toLocaleString('id-ID');
const fmtRpShort = (n)=> n>=1000 ? 'Rp '+(n/1000).toLocaleString('id-ID')+'rb' : 'Rp '+n;

/* ---- ikon (stroke currentColor) ---- */
const Ic = {
  drop:  (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12 3s6 6.4 6 10.5A6 6 0 1 1 6 13.5C6 9.4 12 3 12 3Z"/></svg>),
  home:  (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M3 11.5 12 4l9 7.5"/><path d="M5 10v10h14V10"/><path d="M10 20v-6h4v6"/></svg>),
  bolt:  (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8Z"/></svg>),
  card:  (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="2.5" y="5" width="19" height="14" rx="2.5"/><path d="M2.5 9.5h19"/><path d="M6 15h4"/></svg>),
  camera:(p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M4 8h3l1.5-2h7L17 8h3a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z"/><circle cx="12" cy="13" r="3.5"/></svg>),
  arrow: (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M5 12h14M13 6l6 6-6 6"/></svg>),
  back:  (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M19 12H5M11 18l-6-6 6-6"/></svg>),
  plus:  (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" {...p}><path d="M12 5v14M5 12h14"/></svg>),
  check: (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M20 6 9 17l-5-5"/></svg>),
  clock: (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5l3 1.8"/></svg>),
  user:  (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.2 3.6-7 8-7s8 2.8 8 7"/></svg>),
  wallet:(p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M3 7.5A2.5 2.5 0 0 1 5.5 5H17a2 2 0 0 1 2 2v.5"/><rect x="3" y="7.5" width="18" height="12" rx="2.5"/><path d="M16 13.5h.01"/></svg>),
  gift:  (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="3.5" y="8" width="17" height="5" rx="1"/><path d="M5 13v8h14v-8M12 8v13"/><path d="M12 8S10.5 4 8.3 4.2C6.8 4.3 6.4 6 7.2 7c.9 1 4.8 1 4.8 1Zm0 0s1.5-4 3.7-3.8C17.2 4.3 17.6 6 16.8 7c-.9 1-4.8 1-4.8 1Z"/></svg>),
  shield:(p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12 3 5 6v6c0 4 3 6.5 7 8 4-1.5 7-4 7-8V6l-7-3Z"/><path d="m9 12 2 2 4-4"/></svg>),
  list:  (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M8 6h13M8 12h13M8 18h13M3.5 6h.01M3.5 12h.01M3.5 18h.01"/></svg>),
  qr:    (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><path d="M14 14h3v3M21 21v.01M17 21h.01M21 17h.01"/></svg>),
  phone: (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="6" y="2.5" width="12" height="19" rx="2.5"/><path d="M10.5 18.5h3"/></svg>),
  tap:   (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M9 11V6.5a1.8 1.8 0 0 1 3.6 0V11"/><path d="M12.6 11V9a1.7 1.7 0 0 1 3.4 0v2"/><path d="M16 11v-.5a1.6 1.6 0 0 1 3.2 0V14c0 3.5-2.2 6.5-6 6.5-2.2 0-3.6-.9-4.7-2.3l-3-4c-.7-1 .1-2.3 1.3-2.1.5.1.9.4 1.2.8L9 14V6.5"/></svg>),
  ext:   (p)=>(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M14 4h6v6M20 4l-9 9"/><path d="M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/></svg>),
};

/* ---- art (gambar) ---- */
function DropletArt({ className="w-8 h-8" }){ return <img src="assets/droplet.svg" alt="" aria-hidden="true" className={className}/>; }
function Logo({ className="h-8" }){ return <img src={imgSrc('logo-mizuone-color.png')} alt="MizuOne" className={className+" w-auto"}/>; }

/* =========================================================================
   WATER HEADER — animasi air signature + ripple on click
   ========================================================================= */
function WaterHeader({ className='', children }){
  const ref = useRef(null);
  const [ripples, setRipples] = useState([]);
  const last = useRef(0);
  const spawn = (cx, cy, scale=0.9)=>{
    const el = ref.current; if(!el) return;
    const r = el.getBoundingClientRect();
    const size = Math.max(r.width, r.height) * scale;
    const id = Date.now()+Math.random();
    setRipples(rs=>[...rs,{ id, x:cx-r.left, y:cy-r.top, size }]);
    setTimeout(()=>setRipples(rs=>rs.filter(p=>p.id!==id)), 720);
  };
  const bubbles = React.useMemo(()=>Array.from({length:11}).map((_,i)=>({
    left:4+(i*8.6)%92, size:5+(i%4)*4, dur:7+(i%5)*2.2, delay:(i*0.9)%6
  })),[]);
  return (
    <div ref={ref}
      onClick={(e)=>spawn(e.clientX,e.clientY,0.95)}
      onPointerMove={(e)=>{ if(e.pointerType==='touch')return; const now=Date.now(); if(now-last.current<480)return; last.current=now; spawn(e.clientX,e.clientY,0.5); }}
      className={"overflow-hidden wh-flow text-white select-none "+(/\babsolute\b/.test(className)?'':'relative')+" "+className}
      role="img" aria-label="Animasi air mengalir">
      <div className="wh-caustic" aria-hidden="true"></div>
      <div className="wh-glint" aria-hidden="true"></div>
      <div className="wh-bubbles" aria-hidden="true">
        {bubbles.map((b,i)=>(<span key={i} className="wh-bubble" style={{ left:b.left+'%', width:b.size, height:b.size, animationDuration:b.dur+'s', animationDelay:b.delay+'s' }}></span>))}
      </div>
      {ripples.map(r=>(<span key={r.id} className="wh-ripple" style={{ left:r.x, top:r.y, width:r.size, height:r.size }}></span>))}
      <div className="relative z-10">{children}</div>
      <svg className="wh-waves" viewBox="0 0 1440 64" preserveAspectRatio="none" aria-hidden="true">
        <g className="w1"><path d="M0 32 C 180 6 360 58 720 32 C 1080 6 1260 58 1440 32 L1440 64 L0 64 Z" fill="rgba(248,250,252,.55)"/><path d="M1440 32 C 1620 6 1800 58 2160 32 C 2520 6 2700 58 2880 32 L2880 64 L1440 64 Z" fill="rgba(248,250,252,.55)"/></g>
        <g className="w2"><path d="M0 40 C 200 20 380 60 720 40 C 1060 20 1240 60 1440 40 L1440 64 L0 64 Z" fill="#F8FAFC"/><path d="M1440 40 C 1640 20 1820 60 2160 40 C 2500 20 2680 60 2880 40 L2880 64 L1440 64 Z" fill="#F8FAFC"/></g>
      </svg>
    </div>
  );
}

/* =========================================================================
   PRIMITIVES
   ========================================================================= */
function Card({ className='', children, ...rest }){
  return <div className={"ds-card rounded-2xl border border-slate-200/80 bg-white shadow-card "+className} {...rest}>{children}</div>;
}

function Button({ variant='primary', size='md', className='', children, icon, iconRight, ...rest }){
  const base = "inline-flex items-center justify-center gap-2 font-semibold rounded-xl transition active:scale-[.98] focus:outline-none focus-visible:ring-4 focus-visible:ring-mizu/30 disabled:opacity-50 disabled:pointer-events-none";
  const sizes = { sm:"px-3 py-2 text-sm min-h-[40px]", md:"px-4 py-2.5 text-[15px] min-h-[44px]", lg:"px-5 py-3.5 text-base min-h-[52px]" };
  const variants = {
    primary:"bg-mizu text-white hover:bg-mizu-600 shadow-soft",
    secondary:"bg-mizu-light text-mizu-700 hover:bg-[#d6e9ff]",
    outline:"border border-slate-300 dark:border-slate-600 text-ink dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 bg-white dark:bg-slate-900",
    ghost:"bg-transparent text-mizu-700 dark:text-mizu-light hover:bg-mizu-light/70 dark:hover:bg-slate-800",
    white:"bg-white text-mizu-700 hover:bg-white/90 shadow-soft",
    glass:"bg-white/15 text-white border border-white/35 hover:bg-white/25 backdrop-blur",
    success:"bg-success text-white hover:brightness-95 shadow-soft",
  };
  return <button className={[base,sizes[size],variants[variant],className].join(' ')} {...rest}>{icon}{children}{iconRight}</button>;
}

function Badge({ tone='mizu', children, className='' }){
  const tones = {
    mizu:"bg-mizu-light text-mizu-700",
    success:"bg-success/12 text-success",
    promo:"bg-warning text-[#3d2a00]",
    warning:"bg-warning/20 text-[#946200]",
    slate:"bg-slate-100 text-slate-500",
  };
  return <span className={"inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-bold tracking-wide "+tones[tone]+" "+className}>{children}</span>;
}

const inputCls = "w-full rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-900 px-3.5 py-3 text-[15px] min-h-[48px] outline-none focus:border-mizu focus:ring-4 focus:ring-mizu/15 placeholder:text-slate-400 transition";

/* Header layar dengan back + judul. light=teks putih (di atas header air). */
function ScreenTop({ onBack, title, light=false }){
  return (
    <div className="flex items-center gap-2 mb-4">
      <button onClick={onBack} aria-label="Kembali"
        className={"w-11 h-11 grid place-items-center rounded-xl active:scale-95 transition shrink-0 "+(light
          ? "bg-white/15 border border-white/30 text-white backdrop-blur hover:bg-white/25"
          : "bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 shadow-card")}>
        <Ic.back className="w-5 h-5"/>
      </button>
      <h1 className={"text-xl font-extrabold tracking-tight "+(light?'text-white':'')}>{title}</h1>
    </div>
  );
}

/* =========================================================================
   FAUX QR — placeholder QR deterministik
   ========================================================================= */
function FauxQR({ size=160 }){
  const n = 21, cell = size/n;
  const finder = (gx,gy)=>{ // true bila piksel di area finder pattern
    for(const [ox,oy] of [[0,0],[n-7,0],[0,n-7]]){
      if(gx>=ox&&gx<ox+7&&gy>=oy&&gy<oy+7){
        const ix=gx-ox, iy=gy-oy;
        const edge = ix===0||ix===6||iy===0||iy===6;
        const core = ix>=2&&ix<=4&&iy>=2&&iy<=4;
        return edge||core;
      }
    }
    return null;
  };
  const cells = [];
  for(let y=0;y<n;y++) for(let x=0;x<n;x++){
    const f = finder(x,y);
    let on;
    if(f!==null) on=f;
    else { const inFinderClear = (x<8&&y<8)||(x>n-9&&y<8)||(x<8&&y>n-9); on = inFinderClear ? false : ((x*7+y*13+x*y)%3===0); }
    if(on) cells.push(<rect key={x+'-'+y} x={x*cell} y={y*cell} width={cell} height={cell} rx={cell*0.18}/>);
  }
  return (
    <svg viewBox={`0 0 ${size} ${size}`} width={size} height={size} className="block" role="img" aria-label="Kode QR pembayaran">
      <rect x="0" y="0" width={size} height={size} fill="#fff"/>
      <g fill="#0f2740">{cells}</g>
    </svg>
  );
}

/* =========================================================================
   PAYMENT MODAL — Faspay + countdown 15 menit + status
   Buka redirect_url Faspay di tab baru (bukan QR code)
   ========================================================================= */
function QrisModal({ open, amount=0, qrContent=null, orderId=null, onClose, onPaid, checkStatus=null }){
  const [secs, setSecs] = useState(15*60);
  const [paid, setPaid] = useState(false);

  useEffect(()=>{
    if(!open) return;
    setSecs(15*60); setPaid(false);
    const tick = setInterval(()=>setSecs(s=> s>0 ? s-1 : 0), 1000);
    return ()=>clearInterval(tick);
  },[open]);

  // Poll status setiap 3 detik bila orderId tersedia
  useEffect(()=>{
    if(!open || !orderId || paid || !window.AquAPI) return;
    const fn = checkStatus || window.AquAPI.checkTopupStatus;
    const poll = setInterval(async()=>{
      try {
        const r = await fn(orderId);
        if(r.status === 'success') setPaid(true);
      } catch(e){}
    }, 3000);
    return ()=>clearInterval(poll);
  },[open, orderId, paid, checkStatus]);

  useEffect(()=>{
    if(!paid) return;
    const t = setTimeout(()=> onPaid && onPaid(), 1500);
    return ()=>clearTimeout(t);
  },[paid]);

  if(!open) return null;
  const mm = String(Math.floor(secs/60)).padStart(2,'0');
  const ss = String(secs%60).padStart(2,'0');
  return (
    <div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4">
      <div className="absolute inset-0 bg-slate-900/55 backdrop-blur-sm" onClick={onClose}></div>
      <Card className="relative w-full sm:max-w-sm pop rounded-b-none sm:rounded-2xl p-6 text-center overflow-hidden">
        <div className="flex items-center justify-between mb-1">
          <Badge tone="mizu">Pembayaran · Faspay</Badge>
          <button onClick={onClose} aria-label="Tutup" className="w-8 h-8 grid place-items-center rounded-lg text-slate-400 hover:bg-slate-100">✕</button>
        </div>
        {!paid ? (
        secs === 0 ? (
          <div className="py-4 text-center">
            <div className="mx-auto w-16 h-16 rounded-full bg-warning/15 text-[#946200] grid place-items-center mb-3"><Ic.clock className="w-8 h-8"/></div>
            <h3 className="text-lg font-extrabold">Sesi Kedaluwarsa</h3>
            <p className="text-slate-500 text-sm mt-1 sub">Waktu habis. Silakan buat pembayaran baru.</p>
            <Button className="mt-4" onClick={onClose}>Tutup &amp; Buat Baru</Button>
          </div>
        ) : (
          <React.Fragment>
            <h3 className="text-lg font-extrabold mt-2">Selesaikan pembayaran</h3>
            <p className="text-slate-500 text-sm sub">Total <b className="text-ink">{fmtRp(amount)}</b></p>
            <div className="mx-auto mt-5 w-full flex flex-col items-center gap-3">
              {qrContent ? (
                <a
                  href={qrContent}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="w-full inline-flex items-center justify-center gap-2 rounded-xl px-5 py-3.5 bg-mizu text-white font-bold text-base shadow-md hover:opacity-90 active:scale-95 transition-all"
                >
                  <svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
                  Buka Halaman Pembayaran
                </a>
              ) : (
                <div className="w-full py-3 rounded-xl bg-slate-100 text-slate-400 text-sm">Menyiapkan pembayaran…</div>
              )}
              <p className="text-xs text-slate-400 sub">Halaman pembayaran akan terbuka di tab baru</p>
            </div>
            <div className="mt-4 inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-warning/15 text-[#946200] text-sm font-semibold">
              <Ic.clock className="w-4 h-4"/> Berlaku <span className="tabular-nums">{mm}:{ss}</span>
            </div>
            <div className="mt-3 flex items-center justify-center gap-2 text-sm text-slate-500 sub">
              <span className="w-2 h-2 rounded-full bg-warning animate-pulse"></span> Menunggu konfirmasi pembayaran…
            </div>
            <button onClick={()=>setPaid(true)} className="mt-4 text-sm font-semibold text-mizu hover:underline">Sudah Bayar →</button>
          </React.Fragment>
        )
        ) : (
          <div className="py-4">
            <div className="mx-auto w-20 h-20 rounded-full bg-success/12 text-success grid place-items-center mb-4 pop"><Ic.check className="w-10 h-10"/></div>
            <h3 className="text-xl font-extrabold">Pembayaran diterima ✓</h3>
            <p className="text-slate-500 mt-1 sub">{fmtRp(amount)} berhasil dibayar</p>
          </div>
        )}
      </Card>
    </div>
  );
}

/* =========================================================================
   BOTTOM NAV — member (Home · Topup · Riwayat · Akun)
   ========================================================================= */
function BottomNav({ active, flow, go, notice }){
  const home = flow==='kartu' ? 'kartu-home' : 'digital-home';
  const topup = flow==='kartu' ? 'kartu-topup' : 'digital-topup';
  const items = [
    { key:'home',    label:'Beranda', icon:Ic.home,   onClick:()=>go(home) },
    { key:'topup',   label:'Topup',   icon:Ic.wallet, onClick:()=>go(topup) },
    { key:'riwayat', label:'Riwayat', icon:Ic.clock,  onClick:()=>go(flow==='kartu'?'kartu-riwayat':'digital-riwayat') },
    { key:'akun',    label:'Akun',    icon:Ic.user,   onClick:()=>go(flow==='kartu'?'kartu-akun':'digital-akun') },
  ];
  return (
    <nav className="sticky bottom-0 z-30 bg-white/95 dark:bg-slate-950/90 backdrop-blur border-t border-slate-200/80 dark:border-slate-800" aria-label="Navigasi member">
      <div className="max-w-md mx-auto px-2 grid grid-cols-4 pb-[max(0px,env(safe-area-inset-bottom))]">
        {items.map((it)=>{
          const I = it.icon; const on = active===it.key;
          return (
            <button key={it.key} onClick={it.onClick} aria-current={on?'page':undefined} className="flex flex-col items-center py-2.5 gap-1 active:scale-95 transition">
              <I className={"w-6 h-6 "+(on?'text-mizu':'text-slate-400')}/>
              <span className={"text-[11px] font-semibold "+(on?'text-mizu':'text-slate-400')}>{it.label}</span>
            </button>
          );
        })}
      </div>
    </nav>
  );
}

/* =========================================================================
   FASPAY REDIRECT — interstitial (BUKAN QR in-app; redirect ke hosted page)
   ========================================================================= */
function FaspayRedirect({ open, onDone }){
  useEffect(()=>{
    if(!open) return;
    const t = setTimeout(()=> onDone && onDone(), 2800);
    return ()=>clearTimeout(t);
  },[open]);
  if(!open) return null;
  return (
    <div className="fixed inset-0 z-50 wh-flow text-white grid place-items-center select-none" role="alertdialog" aria-label="Mengarahkan ke pembayaran Faspay">
      <div className="wh-caustic" aria-hidden="true"></div>
      <div className="wh-glint" aria-hidden="true"></div>
      <div className="relative text-center px-8 max-w-xs">
        <div className="mx-auto mb-6 relative w-20 h-20">
          <div className="absolute inset-0 rounded-full border-4 border-white/25 border-t-white animate-spin"></div>
          <div className="absolute inset-0 grid place-items-center"><DropletArt className="w-9 h-9 brightness-0 invert"/></div>
        </div>
        <h3 className="text-lg font-extrabold leading-snug">Mengarahkan ke halaman pembayaran Faspay…</h3>
        <p className="text-white/85 mt-2 text-sm">Jangan tutup halaman ini.</p>
        <p className="text-white/60 mt-5 text-xs">Pembayaran diproses aman oleh Faspay (QRIS)</p>
      </div>
    </div>
  );
}

/* =========================================================================
   PIN SHEET — PIN aplikasi 4-digit (keamanan dompet; demo: 1234)
   ========================================================================= */
function PinSheet({ open, onClose, onSuccess, onVerify=null, expect='1234', hint=true, errMsg='PIN salah, coba lagi', title='Masukkan PIN', subtitle='PIN melindungi saldomu dari penggunaan orang lain.' }){
  const [pin, setPin] = useState('');
  const [err, setErr] = useState(false);
  const [busy, setBusy] = useState(false);
  useEffect(()=>{ if(open){ setPin(''); setErr(false); setBusy(false); } },[open]);
  useEffect(()=>{
    if(pin.length<4) return;
    if(onVerify){
      setBusy(true);
      onVerify(pin).then(r=>{
        if(r && (r.status==='ok' || r.status==='no_pin')){
          onSuccess && onSuccess(pin, r.status);
        } else {
          setErr(true);
          setTimeout(()=>{ setPin(''); setErr(false); setBusy(false); }, 600);
        }
      }).catch(()=>{
        setErr(true);
        setTimeout(()=>{ setPin(''); setErr(false); setBusy(false); }, 600);
      });
      return;
    }
    if(expect===null || pin===expect){ const t=setTimeout(()=> onSuccess && onSuccess(pin), 200); return ()=>clearTimeout(t); }
    const t=setTimeout(()=>{ setErr(true); setTimeout(()=>{ setPin(''); setErr(false); }, 600); }, 80);
    return ()=>clearTimeout(t);
  },[pin]);
  if(!open) return null;
  const press = (d)=>{ if(busy) return; setPin(p=> p.length<4 ? p+d : p); };
  const modal = (
    <div style={{position:'fixed',inset:0,zIndex:9999,display:'flex',alignItems:'center',justifyContent:'center',padding:'1rem'}}>
      <div style={{position:'absolute',inset:0,background:'rgba(15,23,42,.6)',backdropFilter:'blur(4px)'}} onClick={onClose}></div>
      <Card className="relative w-full max-w-sm pop rounded-2xl p-6 text-center">
        <div className="w-12 h-12 rounded-2xl bg-mizu-light text-mizu grid place-items-center mx-auto mb-3"><Ic.shield className="w-6 h-6"/></div>
        <h3 className="text-lg font-extrabold">{title}</h3>
        <p className="text-slate-500 text-sm mt-1 sub">{subtitle}</p>
        <div className="flex justify-center gap-3.5 mt-6">
          {[0,1,2,3].map(i=>(<span key={i} className={"w-4 h-4 rounded-full border-2 transition "+(err?'border-danger':'border-mizu')+" "+(i<pin.length?(err?'bg-danger border-danger':'bg-mizu'):'bg-transparent')}></span>))}
        </div>
        <p className={"text-danger text-sm mt-3 font-semibold transition "+(err?'opacity-100':'opacity-0')}>{errMsg}</p>
        <div className={"grid grid-cols-3 gap-2.5 mt-3 max-w-[280px] mx-auto transition "+(busy?'opacity-50 pointer-events-none':'')}>
          {['1','2','3','4','5','6','7','8','9'].map(d=>(<button key={d} onClick={()=>press(d)} className="h-14 rounded-xl bg-slate-100 dark:bg-slate-800 text-xl font-bold text-ink dark:text-slate-100 active:scale-95 transition">{d}</button>))}
          <button onClick={onClose} className="h-14 rounded-xl text-sm font-semibold text-slate-400 active:scale-95 transition">Batal</button>
          <button onClick={()=>press('0')} className="h-14 rounded-xl bg-slate-100 dark:bg-slate-800 text-xl font-bold text-ink dark:text-slate-100 active:scale-95 transition">0</button>
          <button onClick={()=>setPin(p=>p.slice(0,-1))} aria-label="Hapus" className="h-14 rounded-xl text-slate-500 text-xl grid place-items-center active:scale-95 transition">⌫</button>
        </div>
        {busy && <p className="text-xs text-slate-400 mt-3">Memverifikasi…</p>}
        {hint && !onVerify && expect!==null && <p className="text-xs text-slate-400 mt-4">PIN demo: <b className="text-slate-500">1234</b></p>}
      </Card>
    </div>
  );
  return ReactDOM.createPortal(modal, document.body);
}

/* ---- container responsif standar ---- */
const wrapWide = "max-w-lg sm:max-w-2xl lg:max-w-5xl mx-auto";
const wrapNarrow = "max-w-lg sm:max-w-xl mx-auto";

/* =========================================================================
   TAUTAN EKSTERNAL & HELPER
   ========================================================================= */
const LINKS = {
  wa: 'https://wa.me/6285105840777',                 // Asisten Mizu (WhatsApp)
  tg: 'https://t.me/MizukiBot',                       // Asisten Mizuki (Telegram) — TODO: username bot final
  ig: 'https://instagram.com/mizuone.id',              // TODO: ganti ke IG resmi
};
const openExt = (url)=>{ try{ window.open(url, '_blank', 'noopener,noreferrer'); }catch(e){} };

/* ID transaksi Faspay dummy yang menyerupai format asli */
function faspayId(){
  const d = new Date(); const p = n=>String(n).padStart(2,'0');
  const rnd = Math.floor(Math.random()*1e6).toString().padStart(6,'0');
  return 'FSP'+d.getFullYear()+p(d.getMonth()+1)+p(d.getDate())+rnd;
}

/* Bentuk objek struk dari satu baris riwayat */
function mkStruk(row, flow){
  const topup = row.type==='topup';
  const transfer = row.type==='transfer';
  return {
    type: row.type, flow,
    title: topup ? (flow==='kartu'?'Topup Kartu':'Topup Saldo') : transfer ? 'Transfer ke Kartu' : 'Beli Air'+(row.liter?` ${row.liter}L`:''),
    amt: row.amt, liter: row.liter || 0,
    date: row.d || row.date || 'Hari ini', time: row.time || '',
    via: row.via || (topup?'QRIS Faspay':transfer?'Saldo digital → Kartu fisik':'Saldo digital'),
    method: topup ? 'QRIS · Faspay' : transfer ? 'Saldo digital' : (row.via || (flow==='kartu'?'Tap kartu di mesin':'Saldo digital')),
    status: 'Berhasil', id: row.orderId || faspayId(),
  };
}

/* =========================================================================
   OTP RESEND — countdown lalu tombol kirim ulang
   ========================================================================= */
function OtpResend({ seconds=60, onResend }){
  const [s, setS] = useState(seconds);
  useEffect(()=>{ if(s<=0) return; const t=setTimeout(()=>setS(x=>x-1),1000); return ()=>clearTimeout(t); },[s]);
  return (
    <p className="text-center text-sm text-slate-500 mt-3 sub">
      {s>0
        ? <React.Fragment>Tidak menerima kode? Kirim ulang dalam <b className="text-ink dark:text-slate-100 tabular-nums">{s}s</b></React.Fragment>
        : <button onClick={()=>{ setS(seconds); onResend && onResend(); }} className="font-semibold text-mizu hover:underline">Kirim ulang OTP</button>}
    </p>
  );
}

/* =========================================================================
   CHAT FAB — asisten mengambang (Mizu WA · Mizuki Telegram)
   ========================================================================= */
function ChatFab(){
  const [open, setOpen] = useState(false);
  const Opt = ({ img, name, plat, tint, onClick })=>(
    <button onClick={onClick} className="flex items-center gap-3 pl-2 pr-4 py-2 rounded-2xl bg-white dark:bg-slate-800 shadow-soft border border-slate-200/70 dark:border-slate-700 active:scale-95 hover:shadow-lg transition pop">
      <span className="w-11 h-11 rounded-xl grid place-items-center overflow-hidden shrink-0" style={{background:tint}}>
        <img src={img} alt="" className="h-10 w-auto"/>
      </span>
      <span className="text-left leading-tight">
        <span className="block text-[13px] font-bold text-ink dark:text-slate-100">{name}</span>
        <span className="block text-[11px] text-slate-400">{plat}</span>
      </span>
    </button>
  );
  return (
    <React.Fragment>
      {open && <div className="fixed inset-0 z-40" onClick={()=>setOpen(false)} aria-hidden="true"></div>}
      <div className="fixed right-4 bottom-[5.5rem] z-50 flex flex-col items-end gap-2.5">
        {open && (
          <div className="flex flex-col items-end gap-2.5 mb-1">
            <Opt img={imgSrc('mascot-mizu-shoes.png')} name="Chat Mizu" plat="WhatsApp" tint="#E7FCE3" onClick={()=>{ setOpen(false); openExt(LINKS.wa); }}/>
            <Opt img={imgSrc('mascot-mizuki-shoes.png')} name="Chat Mizuki" plat="Telegram" tint="#E7F3FB" onClick={()=>{ setOpen(false); openExt(LINKS.tg); }}/>
          </div>
        )}
        <button onClick={()=>setOpen(o=>!o)} aria-label="Asisten MizuOne" aria-expanded={open}
          className="relative w-16 h-16 rounded-full wh-flow text-white shadow-soft grid place-items-center active:scale-95 hover:scale-105 transition overflow-hidden">
          <div className="wh-glint" aria-hidden="true"></div>
          {open
            ? <span className="relative text-2xl font-bold">✕</span>
            : <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" className="relative w-8 h-8"><path d="M21 11.5a8.4 8.4 0 0 1-12.4 7.4L3 20.5l1.6-5.4A8.4 8.4 0 1 1 21 11.5Z"/><path d="M12 7.5s3 2.8 3 4.6a3 3 0 1 1-6 0C9 10.3 12 7.5 12 7.5Z" fill="currentColor" stroke="none" opacity=".9"/></svg>}
          {!open && <span className="absolute -top-0.5 -right-0.5 w-3.5 h-3.5 rounded-full bg-success border-2 border-white"></span>}
        </button>
      </div>
    </React.Fragment>
  );
}

Object.assign(window, {
  React, useState, useEffect, useRef,
  fmtRp, fmtRpShort, Ic, DropletArt, Logo,
  WaterHeader, Card, Button, Badge, inputCls, ScreenTop, FauxQR, QrisModal, BottomNav,
  FaspayRedirect, PinSheet,
  LINKS, openExt, faspayId, mkStruk, OtpResend, ChatFab,
  wrapWide, wrapNarrow,
});
