// SettingsPanel — slide-over: stack selection, notification channel binding,
// language. Channels are real backend state (ew_channels): email verifies via
// 6-digit code; webhook/discord verify via a live test ping.
const inputStyle = {
  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 (UI review, 2026-07-15) —
// previously identical, so a disabled Send Code/Confirm/Save button looked
// clickable and gave no feedback on why a click did nothing.
const smallBtn = (primary, disabled) => ({
  padding: '9px 14px', borderRadius: 8, whiteSpace: 'nowrap',
  fontSize: 10, 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)'),
});

function ChannelErr({ msg }) {
  if (!msg) return null;
  return <p style={{ margin: '6px 0 0', fontSize: 10, color: 'var(--sev-breaking)' }}>{msg}</p>;
}

// Client mirror of the backend isEntitled() check. plan alone isn't expiry-aware
// (GET /api/me returns the raw 'messenger' string even when lapsed), so compare
// paidUntil against now.
function isEntitledClient(me) {
  return !!(me && me.plan === 'messenger' && me.paidUntil && new Date(me.paidUntil).getTime() > Date.now());
}

// Maps a backend error .code to a user-facing message. messenger_required (402
// from a gated channel bind) must say so, not the content-free generic (finding 9).
function channelErrText(t, code) {
  if (code === 'messenger_required') return t('channels.messengerRequired');
  if (code === 'test_failed') return t('channels.testFailed');
  return t('error.generic');
}

function VerifiedRow({ label, target, onRemove, t }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
      <span style={{ fontSize: 12, color: 'var(--text-primary)', flex: 'none' }}>{label}</span>
      <span style={{ fontSize: 10, color: 'var(--text-secondary)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{target}</span>
      <span style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.15em', color: 'var(--status-ok)', flex: 'none' }}>VERIFIED</span>
      <button onClick={onRemove} style={{ ...smallBtn(false), padding: '6px 10px' }}>{t('channels.remove')}</button>
    </div>
  );
}

// Email: enter address → code lands in inbox → confirm.
function EmailBind({ t, session, row, refresh }) {
  const [email, setEmail] = React.useState('');
  const [code, setCode] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');

  if (row && row.verified) {
    return <VerifiedRow label="Email" target={row.target} t={t}
      onRemove={() => window.EWApi.removeChannel(session.token, 'email').then(refresh)} />;
  }

  const sendCode = async () => {
    setBusy(true); setErr('');
    try { await window.EWApi.bindEmail(session.token, email.trim()); refresh(); }
    catch (e) { setErr(channelErrText(t, e.code)); }
    finally { setBusy(false); }
  };
  const confirm = async () => {
    setBusy(true); setErr('');
    try { await window.EWApi.confirmEmail(session.token, code.trim()); setCode(''); refresh(); }
    catch (e) { setErr(channelErrText(t, e.code)); }
    finally { setBusy(false); }
  };

  return (
    <div>
      <div style={{ fontSize: 12, marginBottom: 8 }}>Email</div>
      {row ? (
        <>
          <p style={{ margin: '0 0 8px', fontSize: 10, color: 'var(--text-secondary)' }}>{t('channels.codeSent')} ({row.target})</p>
          <div style={{ display: 'flex', gap: 8 }}>
            <input style={inputStyle} value={code} onChange={e => setCode(e.target.value)}
              inputMode="numeric" maxLength={6} placeholder="000000" aria-label="Verification code" />
            <button disabled={busy || code.trim().length !== 6} onClick={confirm} style={smallBtn(true, busy || code.trim().length !== 6)}>{t('channels.confirm')}</button>
          </div>
        </>
      ) : (
        <div style={{ display: 'flex', gap: 8 }}>
          <input style={inputStyle} value={email} onChange={e => setEmail(e.target.value)}
            type="email" placeholder="you@example.com" aria-label="Email" />
          <button disabled={busy || !email.includes('@')} onClick={sendCode} style={smallBtn(true, busy || !email.includes('@'))}>{t('channels.sendCode')}</button>
        </div>
      )}
      <ChannelErr msg={err} />
    </div>
  );
}

// Webhook / Discord: URL is verified by an immediate test delivery.
function UrlBind({ t, session, row, refresh, type, label, placeholder }) {
  const [url, setUrl] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');

  if (row && row.verified) {
    return <VerifiedRow label={label} target={row.target} t={t}
      onRemove={() => window.EWApi.removeChannel(session.token, type).then(refresh)} />;
  }

  const save = async () => {
    setBusy(true); setErr('');
    try { await window.EWApi.bindUrl(session.token, type, url.trim()); setUrl(''); refresh(); }
    catch (e) { setErr(channelErrText(t, e.code)); }
    finally { setBusy(false); }
  };

  return (
    <div>
      <div style={{ fontSize: 12, marginBottom: 8 }}>{label}</div>
      <div style={{ display: 'flex', gap: 8 }}>
        <input style={inputStyle} value={url} onChange={e => setUrl(e.target.value)}
          placeholder={placeholder} aria-label={label + ' URL'} />
        <button disabled={busy || !url.startsWith('https://')} onClick={save} style={smallBtn(true, busy || !url.startsWith('https://'))}>{t('channels.save')}</button>
      </div>
      <ChannelErr msg={err} />
    </div>
  );
}

function ChannelSection({ t, session, me, refresh, onUpgrade }) {
  if (!window.EWApi.hasApi()) {
    return <p style={{ margin: 0, fontSize: 11, color: 'var(--text-secondary)' }}>{t('channels.offline')}</p>;
  }
  if (!session) {
    return <p style={{ margin: 0, fontSize: 11, color: 'var(--text-secondary)' }}>{t('channels.loginFirst')}</p>;
  }
  // Push channels are a Messenger perk once payments go live. While dark
  // (me.paymentsEnabled false) everyone sees the forms — matching the backend,
  // which lets everyone bind until PAYMENTS_ENABLED flips. Once live, a free user
  // sees an upgrade prompt instead of forms that would only 402 (finding 9).
  if (me && me.paymentsEnabled && !isEntitledClient(me)) {
    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        <p style={{ margin: 0, fontSize: 11, lineHeight: 1.6, color: 'var(--text-secondary)' }}>{t('channels.messengerRequired')}</p>
        <button onClick={onUpgrade} style={smallBtn(true)}>{t('settings.plan.upgrade')}</button>
      </div>
    );
  }
  const channels = (me && me.channels) || [];
  const get = (type) => channels.find(c => c.type === type);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
      <EmailBind t={t} session={session} row={get('email')} refresh={refresh} />
      <UrlBind t={t} session={session} row={get('webhook')} refresh={refresh}
        type="webhook" label="Webhook" placeholder="https://your-endpoint.example/hook" />
      <UrlBind t={t} session={session} row={get('discord')} refresh={refresh}
        type="discord" label="Discord" placeholder="https://discord.com/api/webhooks/…" />
      <p style={{ margin: 0, fontSize: 10, lineHeight: 1.6, color: 'var(--text-secondary)' }}>{t('settings.notifHint')}</p>
    </div>
  );
}

// Plan status + upgrade entry point.
function PlanSection({ t, session, me, onUpgrade }) {
  if (!window.EWApi.hasApi()) {
    return <p style={{ margin: 0, fontSize: 11, color: 'var(--text-secondary)' }}>{t('channels.offline')}</p>;
  }
  if (!session || !me) {
    return <p style={{ margin: 0, fontSize: 11, color: 'var(--text-secondary)' }}>{t('channels.loginFirst')}</p>;
  }
  const entitled = isEntitledClient(me);
  return (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
      <div>
        <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>
          {t(entitled ? 'settings.plan.messenger' : 'settings.plan.newsboy')}
        </div>
        {me.paidUntil && (
          <div style={{ fontSize: 10, color: 'var(--text-secondary)', marginTop: 2 }}>
            {t(entitled ? 'settings.plan.until' : 'settings.plan.expired', {
              date: new Date(me.paidUntil).toISOString().slice(0, 10),
            })}
          </div>
        )}
      </div>
      {!entitled && (
        <button onClick={onUpgrade} style={smallBtn(true)}>{t('settings.plan.upgrade')}</button>
      )}
    </div>
  );
}

function SettingsPanel({ open, onClose, t, lang, setLang, selected, setSelected, session, me, refreshMe }) {
  const { STACK_ITEMS } = window.EchoWatchData;
  const { LangRow, CheckSquare } = window.EWShared;
  const [paymentOpen, setPaymentOpen] = React.useState(false);
  const toggleStack = (id) => setSelected((s) => s.includes(id) ? s.filter(x => x !== id) : [...s, id]);

  React.useEffect(() => {
    if (!open) return undefined;
    // Don't let Escape close Settings while PaymentModal is up — otherwise a
    // single press fires both listeners and unmounts the modal mid-confirm
    // (finding 8). When payment is open, its own Escape handler closes it first.
    const onKey = (e) => { if (e.key === 'Escape' && !paymentOpen) onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [open, onClose, paymentOpen]);

  if (!open) return null;
  return (
    <>
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 30, background: 'rgba(0,0,0,0.5)',
      display: 'flex', justifyContent: 'flex-end' }}>
      <div role="dialog" aria-modal="true" aria-label={t('settings.title')} onClick={(e) => e.stopPropagation()} style={{
        width: 'min(380px, 100vw)', height: '100%', background: 'var(--bg-primary)', borderLeft: '1px solid var(--border-color)',
        padding: 28, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 28,
      }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <h2 style={{ margin: 0, fontSize: 16, fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase' }}>{t('settings.title')}</h2>
          <button onClick={onClose} aria-label="Close" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-secondary)', fontSize: 15 }}>✕</button>
        </div>

        <div>
          <div className="label-tech" style={{ marginBottom: 12 }}>{t('settings.plan')}</div>
          <PlanSection t={t} session={session} me={me} onUpgrade={() => setPaymentOpen(true)} />
        </div>

        <div>
          <div className="label-tech" style={{ marginBottom: 12 }}>{t('settings.stack')}</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {STACK_ITEMS.map((s) => {
              const on = selected.includes(s.id);
              return (
                <button key={s.id} onClick={() => toggleStack(s.id)} aria-pressed={on} style={{
                  display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', textAlign: 'left',
                  borderRadius: 8, cursor: 'pointer', border: '1px solid',
                  borderColor: on ? 'var(--text-primary)' : 'var(--border-color)',
                  background: on ? 'var(--bg-secondary)' : 'transparent',
                  color: 'var(--text-primary)', fontSize: 12,
                }}>
                  <CheckSquare on={on} size={14} />
                  {s.name}
                </button>
              );
            })}
          </div>
        </div>

        <div>
          <div className="label-tech" style={{ marginBottom: 12 }}>{t('settings.notifications')}</div>
          <ChannelSection t={t} session={session} me={me} refresh={refreshMe} onUpgrade={() => setPaymentOpen(true)} />
        </div>

        <div>
          <div className="label-tech" style={{ marginBottom: 12 }}>{t('settings.language')}</div>
          <LangRow lang={lang} setLang={setLang} />
        </div>

        <button onClick={onClose} style={{
          marginTop: 'auto', width: '100%', padding: '14px 0', borderRadius: 9999, border: 'none', cursor: 'pointer',
          background: 'var(--text-primary)', color: 'var(--bg-primary)',
          fontSize: 12, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase',
        }}>{t('settings.done')}</button>
      </div>
    </div>

    {/* Sibling, not a child of the backdrop above — a click closing PaymentModal
        must not bubble into SettingsPanel's own backdrop onClick and close both. */}
    <window.PaymentModal
      open={paymentOpen} onClose={() => setPaymentOpen(false)}
      t={t} session={session} refreshMe={refreshMe}
    />
    </>
  );
}
window.SettingsPanel = SettingsPanel;
