// PaymentModal — Messenger upgrade flow. UX rhythm mirrors EchoUploader's
// SubModal (period pick → receipt-style confirm → verifying → success/error),
// but the transaction mechanics deliberately do NOT match EchoUploader: no
// MeshJS, no client-side tx building. EchoWatch's frontend has zero build step
// (React UMD + in-browser Babel only) and CEO decision 2026-07-12 kept it that
// way — the user sends payment from their own wallet by whatever means they
// like, then pastes the resulting tx hash here for on-chain verification.
// Backend contract: GET /api/pay/quote, POST /api/pay/confirm (routes/pay.ts).
const payInputStyle = {
  width: '100%', padding: '10px 12px', borderRadius: 8, fontSize: 12,
  fontFamily: 'var(--font-mono)', border: '1px solid var(--border-color)',
  background: 'transparent', color: 'var(--text-primary)', outline: 'none',
};
// disabled must be visually distinct from enabled (matches Onboarding's
// Continue button convention) — a same-looking disabled Confirm Payment button
// let a user believe a click landed when it silently did nothing (UI review, 2026-07-15).
const payBtn = (primary, disabled) => ({
  padding: '12px 20px', borderRadius: 9999, whiteSpace: 'nowrap',
  fontSize: 11, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase',
  cursor: disabled ? 'not-allowed' : 'pointer',
  border: '1px solid', borderColor: disabled ? 'var(--border-color)' : (primary ? 'var(--text-primary)' : 'var(--border-color)'),
  background: disabled ? 'var(--border-color)' : (primary ? 'var(--text-primary)' : 'transparent'),
  color: disabled ? 'var(--text-secondary)' : (primary ? 'var(--bg-primary)' : 'var(--text-secondary)'),
});
const payRow = { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12 };
const delay = (ms) => new Promise((r) => setTimeout(r, ms));

// tx_not_found is expected right after a real send (Blockfrost indexing lag) —
// same retry semantics as EchoUploader's confirmation poll: 6 tries, 15s apart,
// anything else surfaces immediately rather than burning the whole window.
const RETRY_MAX = 6;
const RETRY_MS = 15000;

function formatDate(iso) {
  if (!iso) return '';
  try { return new Date(iso).toISOString().slice(0, 10); } catch (e) { return iso; }
}

function CopyField({ value, label }) {
  const [copied, setCopied] = React.useState(false);
  const copy = async () => {
    try { await navigator.clipboard.writeText(value); setCopied(true); setTimeout(() => setCopied(false), 1500); }
    catch (e) { /* clipboard unavailable — text is still selectable below */ }
  };
  return (
    <div>
      <div className="label-tech" style={{ marginBottom: 6 }}>{label}</div>
      <div style={{ display: 'flex', gap: 8, alignItems: 'flex-start' }}>
        <div className="value-tech" style={{ flex: 1 }}>{value}</div>
        <button onClick={copy} style={{ ...payBtn(false), padding: '6px 10px', fontSize: 9, flex: 'none' }}>
          {copied ? '✓' : '⧉'}
        </button>
      </div>
    </div>
  );
}

function PaymentModal({ open, onClose, t, session, refreshMe }) {
  const [step, setStep] = React.useState('pick'); // pick | pay | confirming | success | error
  const [period, setPeriod] = React.useState('month');
  const [quote, setQuote] = React.useState(null);
  const [quoteErrCode, setQuoteErrCode] = React.useState('');
  const [quoteLoading, setQuoteLoading] = React.useState(false);
  const [txHash, setTxHash] = React.useState('');
  const [errCode, setErrCode] = React.useState('');
  const [paidUntil, setPaidUntil] = React.useState(null);
  const busyRef = React.useRef(false); // guards the retry loop against a second submit
  // Generation counter (audit findings 7 & 8): each open/unmount bumps it,
  // invalidating any in-flight submit() loop so it stops touching state and
  // frees busyRef — no dead Confirm button, no setState-after-unmount.
  const runRef = React.useRef(0);

  React.useEffect(() => {
    if (!open) return undefined;
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [open, onClose]);

  // Invalidate any orphaned poll loop on unmount so it can't setState afterward.
  React.useEffect(() => () => { runRef.current++; busyRef.current = false; }, []);

  // Fresh open: reset the step machine AND cancel any loop still running from a
  // previous open (bump runRef, free busyRef) so the reopened modal is truly
  // fresh and interactive.
  React.useEffect(() => {
    if (open) {
      runRef.current++; busyRef.current = false;
      setStep('pick'); setTxHash(''); setErrCode(''); setPaidUntil(null);
    }
  }, [open]);

  React.useEffect(() => {
    if (!open || !session) return;
    setQuoteLoading(true); setQuoteErrCode('');
    window.EWApi.payQuote(session.token, period)
      .then(setQuote)
      .catch((e) => setQuoteErrCode(e.code || 'generic'))
      .finally(() => setQuoteLoading(false));
  }, [open, session, period]);

  if (!open) return null;

  const errText = (code) => t('pay.err.' + code) === 'pay.err.' + code ? t('error.generic') : t('pay.err.' + code);

  const submit = async () => {
    if (busyRef.current) return;
    const hash = txHash.trim().toLowerCase();
    if (!/^[0-9a-f]{64}$/.test(hash)) { setErrCode('bad_request'); setStep('error'); return; }
    const myRun = ++runRef.current;
    busyRef.current = true;
    setStep('confirming');
    const token = quote && quote.quoteToken;
    for (let attempt = 0; attempt <= RETRY_MAX; attempt++) {
      if (attempt > 0) await delay(RETRY_MS);
      if (runRef.current !== myRun) return; // superseded by a reopen, or unmounted
      try {
        const res = await window.EWApi.payConfirm(session.token, hash, period, token);
        refreshMe(); // reflect entitlement even if this run was superseded/unmounted
        if (runRef.current === myRun) { setPaidUntil(res.paidUntil); setStep('success'); busyRef.current = false; }
        return;
      } catch (e) {
        if (runRef.current !== myRun) return;
        if (e.code === 'tx_not_found' && attempt < RETRY_MAX) continue;
        // A prior (possibly orphaned) attempt may already have redeemed this tx —
        // resync so the Plan section reflects the real, granted entitlement.
        if (e.code === 'tx_already_used') refreshMe();
        setErrCode(e.code || 'generic');
        setStep('error');
        busyRef.current = false;
        return;
      }
    }
  };

  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 31,
      background: 'rgba(0,0,0,0.6)', backdropFilter: 'blur(6px)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
      <div role="dialog" aria-modal="true" aria-label={t('pay.title')}
        onClick={(e) => e.stopPropagation()} className="ew-card"
        style={{ width: 'min(420px, 100%)', maxHeight: '90vh', overflowY: 'auto', padding: 28, borderRadius: 16 }}>
        <span className="corner-tl" /><span className="corner-tr" />
        <span className="corner-bl" /><span className="corner-br" />

        <h2 style={{ margin: '0 0 20px', fontSize: 14, fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase' }}>
          {t('pay.title')}
        </h2>

        {quoteErrCode && (quoteErrCode === 'http_404' || quoteErrCode === 'payments_unconfigured') ? (
          <p style={{ margin: 0, fontSize: 12, color: 'var(--text-secondary)', textAlign: 'center', padding: '20px 0' }}>
            {t('pay.unavailable')}
          </p>
        ) : step === 'pick' ? (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
            <div style={{ display: 'flex', gap: 8 }}>
              {['month', 'year'].map((p) => (
                <button key={p} onClick={() => setPeriod(p)} aria-pressed={period === p} style={{
                  flex: 1, padding: '16px 12px', borderRadius: 10, cursor: 'pointer', textAlign: 'left',
                  border: '1px solid', borderColor: period === p ? 'var(--text-primary)' : 'var(--border-color)',
                  background: period === p ? 'var(--bg-secondary)' : 'transparent',
                }}>
                  <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--text-primary)' }}>
                    {t('pay.period.' + p)}
                  </div>
                  <div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text-primary)', marginTop: 4 }}>
                    ${p === 'month' ? '10' : '100'}
                  </div>
                  {p === 'year' && <div style={{ fontSize: 9, color: 'var(--status-ok)', marginTop: 2 }}>{t('pay.period.yearSave')}</div>}
                </button>
              ))}
            </div>
            {quoteLoading ? (
              <p style={{ margin: 0, fontSize: 11, color: 'var(--text-secondary)', textAlign: 'center' }}>{t('feed.loading')}</p>
            ) : (
              <button disabled={!quote} onClick={() => setStep('pay')} style={{ ...payBtn(true, !quote), width: '100%' }}>
                {t('pay.continue')}
              </button>
            )}
          </div>
        ) : step === 'pay' ? (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
            <p style={{ margin: 0, fontSize: 12, lineHeight: 1.6, color: 'var(--text-secondary)' }}>{t('pay.instructions')}</p>

            {quote && quote.stable && (
              <div style={payRow}>
                <span className="label-tech">{t('pay.payWithStable', { ticker: quote.stable.ticker })}</span>
                <span className="value-tech">{(Number(quote.stable.amount) / (10 ** quote.stable.decimals)).toFixed(2)} {quote.stable.ticker}</span>
              </div>
            )}
            {quote && quote.ada && (
              <div>
                <div style={payRow}>
                  <span className="label-tech">{t('pay.payWithAda')}</span>
                  {/* Round UP from exact lovelace so paying the shown figure always
                      clears the pinned amount (never a display-rounding underpay). */}
                  <span className="value-tech">{(Math.ceil(Number(quote.ada.lovelace) / 1e4) / 100).toFixed(2)} ADA</span>
                </div>
                <p style={{ margin: '4px 0 0', fontSize: 9, color: 'var(--text-secondary)', opacity: 0.7 }}>{t('pay.oracleNote')}</p>
              </div>
            )}
            {quote && <CopyField value={quote.treasuryAddress} label={t('pay.treasury')} />}

            <p style={{ margin: 0, fontSize: 10, lineHeight: 1.6, color: 'var(--sev-advisory)' }}>{t('pay.walletNote')}</p>

            <div>
              <div className="label-tech" style={{ marginBottom: 6 }}>{t('pay.txHashLabel')}</div>
              <input style={payInputStyle} value={txHash} onChange={(e) => setTxHash(e.target.value)}
                placeholder="a1b2c3…" aria-label={t('pay.txHashLabel')} spellCheck={false} />
            </div>

            <div style={{ display: 'flex', gap: 8 }}>
              <button onClick={() => setStep('pick')} style={payBtn(false)}>{t('pay.back')}</button>
              <button disabled={!/^[0-9a-f]{64}$/i.test(txHash.trim())} onClick={submit}
                style={{ ...payBtn(true, !/^[0-9a-f]{64}$/i.test(txHash.trim())), flex: 1 }}>{t('pay.confirm')}</button>
            </div>
          </div>
        ) : step === 'confirming' ? (
          <div style={{ textAlign: 'center', padding: '20px 0' }}>
            <MotionRing />
            <p style={{ margin: '20px 0 0', fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>{t('pay.confirming')}</p>
            <p style={{ margin: '8px 0 0', fontSize: 11, color: 'var(--text-secondary)' }}>{t('pay.confirmingSub')}</p>
          </div>
        ) : step === 'success' ? (
          <div style={{ textAlign: 'center', padding: '12px 0' }}>
            <div className="dot-breathe" style={{ display: 'inline-flex', width: 12, height: 12, borderRadius: '50%', background: 'var(--status-ok)', marginBottom: 16 }} />
            <p style={{ margin: 0, fontSize: 16, fontWeight: 700, color: 'var(--text-primary)' }}>{t('pay.success.title')}</p>
            <p style={{ margin: '8px 0 24px', fontSize: 11, color: 'var(--text-secondary)' }}>{t('pay.success.until', { date: formatDate(paidUntil) })}</p>
            <button onClick={onClose} style={{ ...payBtn(true), width: '100%' }}>{t('pay.done')}</button>
          </div>
        ) : (
          <div>
            <p style={{ margin: '0 0 4px', fontSize: 13, fontWeight: 700, color: 'var(--sev-breaking)' }}>{t('pay.error.title')}</p>
            <p style={{ margin: '0 0 20px', fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.6 }}>{errText(errCode)}</p>
            <button onClick={() => setStep('pay')} style={{ ...payBtn(true), width: '100%' }}>{t('pay.back')}</button>
          </div>
        )}

        <p style={{ margin: '18px 0 0', fontSize: 9, letterSpacing: '0.1em', color: 'var(--text-secondary)', opacity: 0.6, textAlign: 'center', textTransform: 'uppercase' }}>
          [ CLICK OUTSIDE TO DISMISS ]
        </p>
      </div>
    </div>
  );
}

// Plain CSS spinner — no icon font, matches the codebase's geometry-only rule.
function MotionRing() {
  return (
    <span style={{
      display: 'inline-block', width: 32, height: 32, borderRadius: '50%',
      border: '2px solid var(--border-color)', borderTopColor: 'var(--text-primary)',
      animation: 'ew-spin 0.8s linear infinite',
    }} />
  );
}
window.PaymentModal = PaymentModal;
