// cwr-ftp.jsx — FTP / FTPS compatibility surface
// ─────────────────────────────────────────────────────────────────
// Surfaces the FTP/FTPS subset of CwrTransport with:
//   1. Server list (per-society FTP/FTPS configuration)
//   2. Connection tester with security flags (TLS mode, passive, fingerprint)
//   3. File browser sketch (queued / outbox / archive folders)
//   4. Manual upload / re-test row actions
//   5. Security audit panel (cleartext FTP warnings, TLS verification status)
//
// Mounted as a tab inside CWR Acks → "FTP" alongside SFTP-managed societies.
// ─────────────────────────────────────────────────────────────────
(function () {
  'use strict';
  if (typeof window === 'undefined' || !window.React) return;
  const { useState, useEffect, useMemo } = React;

  const Mono = ({ children, size = 10, color = 'var(--ink-3)', upper = true, style = {} }) => (
    <span className="ff-mono" style={{ fontSize: size, color, letterSpacing: '.12em', textTransform: upper ? 'uppercase' : 'none', ...style }}>{children}</span>
  );

  // Severity chip used throughout
  const Chip = ({ kind, children }) => {
    const map = {
      ok:    { bg: 'transparent', fg: '#0a8754', bd: '#0a8754' },
      warn:  { bg: 'transparent', fg: '#a35418', bd: '#a35418' },
      err:   { bg: '#a32a18',     fg: '#fff',     bd: '#a32a18' },
      muted: { bg: 'transparent', fg: 'var(--ink-3)', bd: 'var(--rule)' },
      ink:   { bg: 'var(--ink)',  fg: 'var(--bg)',    bd: 'var(--ink)' },
    }[kind] || { bg: 'transparent', fg: 'var(--ink-2)', bd: 'var(--rule)' };
    return (
      <span className="ff-mono upper" style={{
        fontSize: 9, padding: '3px 7px', letterSpacing: '.12em', fontWeight: 600,
        background: map.bg, color: map.fg, border: '1px solid ' + map.bd,
      }}>{children}</span>
    );
  };

  // Build the list of societies that use FTP or FTPS as primary transport.
  function useFtpSocieties() {
    return useMemo(() => {
      const T = window.CwrTransport;
      if (!T) return [];
      return Object.entries(T.TRANSPORTS || {})
        .filter(([_, cfg]) => cfg.primary === 'FTP' || cfg.primary === 'FTPS' || cfg.fallback === 'FTP' || cfg.fallback === 'FTPS')
        .map(([code, cfg]) => {
          const cred = T.getCredentials ? T.getCredentials(code) : null;
          return { code, cfg, cred };
        });
    }, []);
  }

  function FtpTab() {
    const societies = useFtpSocieties();
    const [tests, setTests] = useState({});      // { societyCode: testResult }
    const [testing, setTesting] = useState({});
    const [queueRev, setQueueRev] = useState(0);
    const [selected, setSelected] = useState(null);

    function runTest(code) {
      const T = window.CwrTransport;
      if (!T) return;
      setTesting(s => ({ ...s, [code]: true }));
      // Simulate latency
      setTimeout(() => {
        const r = T.testConnection(code);
        setTests(s => ({ ...s, [code]: { ...r, ts: Date.now() } }));
        setTesting(s => ({ ...s, [code]: false }));
      }, 400 + Math.floor(Math.random() * 600));
    }
    function runTestAll() {
      societies.forEach(s => runTest(s.code));
    }

    // Counts
    const counts = useMemo(() => {
      const r = { total: societies.length, ftp: 0, ftps: 0, tested: 0, ok: 0, warn: 0, err: 0 };
      societies.forEach(s => {
        if (s.cfg.primary === 'FTP') r.ftp++; else r.ftps++;
        const t = tests[s.code];
        if (t) {
          r.tested++;
          if (t.ok && !t.warning) r.ok++;
          else if (t.ok && t.warning) r.warn++;
          else r.err++;
        }
      });
      return r;
    }, [societies, tests]);

    return (
      <div>
        {/* Header */}
        <div style={{ paddingBottom: 22, borderBottom: '1px solid var(--rule)', marginBottom: 22 }}>
          <Mono size={10} style={{ display: 'block', marginBottom: 8, letterSpacing: '.16em' }}>TRANSPORT · FTP / FTPS</Mono>
          <div style={{ fontSize: 22, fontWeight: 600, letterSpacing: '-0.02em', marginBottom: 6 }}>FTP compatibility</div>
          <div style={{ fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.5, maxWidth: 820 }}>
            Some societies — particularly in Latin America, Eastern Europe, and East Asia — still
            distribute and ingest CWR via plain FTP or FTPS rather than SFTP or HTTPS portals.
            ASTRO supports both: <strong>FTPS explicit (AUTH TLS)</strong> on port 21 and{' '}
            <strong>FTPS implicit</strong> on port 990, plus <strong>plain FTP</strong> for
            legacy partners. Cleartext FTP is flagged as a security warning on every transfer.
          </div>
        </div>

        {/* Counter row */}
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)', marginBottom: 22 }}>
          {[
            { l: 'CONFIGURED',     v: counts.total,  c: 'var(--ink)' },
            { l: 'FTPS',           v: counts.ftps,   c: 'var(--ink)' },
            { l: 'PLAIN FTP',      v: counts.ftp,    c: '#a35418' },
            { l: 'TESTS PASSED',   v: counts.ok,     c: '#0a8754' },
            { l: 'TESTS FAILED',   v: counts.err,    c: '#a32a18' },
          ].map((s, i) => (
            <div key={s.l} style={{ padding: '18px 22px', borderRight: i < 4 ? '1px solid var(--rule)' : 0 }}>
              <Mono size={9}>{s.l}</Mono>
              <div className="ff-mono num" style={{ fontSize: 26, fontWeight: 600, marginTop: 6, color: s.c }}>{s.v}</div>
            </div>
          ))}
        </div>

        {/* Action row */}
        <div style={{ display: 'flex', gap: 10, marginBottom: 16, alignItems: 'center' }}>
          <button onClick={runTestAll} className="ff-mono upper" style={{
            padding: '10px 18px', background: 'var(--ink)', color: 'var(--bg)', border: 0,
            fontSize: 11, letterSpacing: '.14em', fontWeight: 600, cursor: 'pointer',
          }}>Test all connections</button>
          <span style={{ flex: 1 }}/>
          <Mono size={10}>{counts.tested}/{counts.total} TESTED</Mono>
        </div>

        {/* Society table */}
        <div style={{ border: '1px solid var(--rule)' }}>
          <div className="ff-mono upper" style={{
            display: 'grid', gridTemplateColumns: '110px 70px 1fr 80px 100px 80px 100px 110px',
            gap: 10, padding: '10px 14px', fontSize: 9, color: 'var(--ink-3)',
            background: 'var(--bg-2)', borderBottom: '1px solid var(--rule)', letterSpacing: '.12em',
          }}>
            <span>SOCIETY</span><span>PROTOCOL</span><span>HOST</span>
            <span>PORT</span><span>MODE</span><span>PASV</span><span style={{ textAlign: 'right' }}>STATUS</span>
            <span style={{ textAlign: 'right' }}>ACTIONS</span>
          </div>
          {societies.map(({ code, cfg, cred }) => {
            const t = tests[code];
            const isTesting = testing[code];
            const ftpCfg = cred?.ftp || cred?.ftps;
            const proto = cfg.primary === 'FTP' ? 'FTP' : 'FTPS';
            return (
              <div key={code} onClick={() => setSelected(code)} style={{
                display: 'grid', gridTemplateColumns: '110px 70px 1fr 80px 100px 80px 100px 110px',
                gap: 10, padding: '12px 14px', alignItems: 'center', fontSize: 12,
                borderBottom: '1px solid var(--rule-soft)', cursor: 'pointer',
                background: selected === code ? 'var(--bg-2)' : 'transparent',
              }}>
                <span style={{ fontWeight: 500 }}>{code.replace('_', ' ')}</span>
                <Chip kind={proto === 'FTP' ? 'warn' : 'muted'}>{proto}</Chip>
                <span className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-2)' }}>{ftpCfg?.host || '—'}</span>
                <span className="ff-mono num" style={{ fontSize: 11 }}>{ftpCfg?.port || '—'}</span>
                <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-2)' }}>{ftpCfg?.mode || (proto === 'FTP' ? 'cleartext' : '—')}</span>
                <span style={{ fontSize: 11 }}>{ftpCfg?.passive ? '✓' : '—'}</span>
                <span style={{ textAlign: 'right' }}>
                  {isTesting ? <Mono size={9}>TESTING…</Mono>
                   : !t       ? <Mono size={9}>NEVER</Mono>
                   : t.ok && !t.warning ? <Chip kind="ok">OK · {t.latencyMs}MS</Chip>
                   : t.ok && t.warning  ? <Chip kind="warn">{t.warning.replace('_',' ')}</Chip>
                   : <Chip kind="err">{t.error}</Chip>}
                </span>
                <span style={{ textAlign: 'right' }}>
                  <button onClick={e => { e.stopPropagation(); runTest(code); }}
                    className="ff-mono upper" style={{
                      padding: '5px 10px', fontSize: 9, letterSpacing: '.1em',
                      background: 'transparent', border: '1px solid var(--rule)', cursor: 'pointer',
                    }} disabled={isTesting}>{isTesting ? '…' : 'TEST'}</button>
                </span>
              </div>
            );
          })}
        </div>

        {/* Detail panel */}
        {selected && <FtpDetail code={selected} onClose={() => setSelected(null)} test={tests[selected]} onRetest={() => runTest(selected)}/>}

        {/* Security audit */}
        <SecurityAudit societies={societies} tests={tests}/>

        {/* Spec reference */}
        <SpecReference/>
      </div>
    );
  }

  // ─── Detail drawer (inline expansion, editable) ─────────────────
  // Society-specific FTP credentials editor:
  //   • Host / Port / Username / Password (encrypted) / Directories
  //   • TLS mode + verify + fingerprint (FTPS only)
  //   • Passive mode toggle
  //   • Inbox / Outbox / Archive / Ack remote paths
  //   • Reveal-password gesture, save/revert, retest, list remote
  function FtpDetail({ code, onClose, test, onRetest }) {
    const T = window.CwrTransport;
    const cred = T?.getCredentials ? T.getCredentials(code) : null;
    const cfg = T?.TRANSPORTS?.[code];
    if (!cred || !cfg) return null;
    const slotKey = cred.ftp ? 'ftp' : 'ftps';
    const proto = cfg.primary === 'FTP' ? 'FTP' : 'FTPS';

    // Working copy of the editable fields. Encrypted password stays opaque
    // until the user types a new plaintext, at which point we re-encrypt.
    const initial = useMemo(() => {
      const s = cred[slotKey];
      return {
        host: s.host || '',
        port: s.port || (proto === 'FTPS' && s.mode === 'implicit' ? 990 : 21),
        user: s.user || '',
        password: '',                       // empty = leave existing enc unchanged
        passwordEnc: s.passwordEnc || '',
        mode: s.mode || (proto === 'FTP' ? 'cleartext' : 'explicit'),
        passive: s.passive !== false,
        tlsVerify: s.tlsVerify !== false,
        fingerprint: s.fingerprint || '',
        directories: {
          inbox:   (s.directories && s.directories.inbox)   || '/cwr/in',
          outbox:  (s.directories && s.directories.outbox)  || '/cwr/out',
          archive: (s.directories && s.directories.archive) || '/cwr/archive',
          ack:     (s.directories && s.directories.ack)     || '/cwr/ack',
        },
      };
    }, [code, cred, slotKey, proto]);

    const [form, setForm] = useState(initial);
    const [showPw, setShowPw] = useState(false);
    const [savedAt, setSavedAt] = useState(null);
    const [files, setFiles] = useState(null);
    const [filesLoading, setFilesLoading] = useState(false);
    const [browseDir, setBrowseDir] = useState('outbox');

    // Re-init when society changes
    useEffect(() => { setForm(initial); setShowPw(false); setSavedAt(null); setFiles(null); }, [code]);

    const dirty = useMemo(() => JSON.stringify(form) !== JSON.stringify({ ...initial, password: form.password || '' }), [form, initial]);

    function patch(p) { setForm(f => ({ ...f, ...p })); }
    function patchDir(k, v) { setForm(f => ({ ...f, directories: { ...f.directories, [k]: v } })); }

    function save() {
      if (!T.updateCredentials) return;
      const r = T.updateCredentials(code, {
        host: form.host,
        port: Number(form.port),
        user: form.user,
        password: form.password || null,
        mode: form.mode,
        passive: form.passive,
        tlsVerify: form.tlsVerify,
        fingerprint: form.fingerprint,
        directories: form.directories,
      });
      if (r.ok) {
        setSavedAt(Date.now());
        // refresh form to drop typed plaintext + pick up new enc blob
        setForm(f => ({ ...f, password: '', passwordEnc: r.slot.passwordEnc }));
      }
    }
    function revert() { setForm(initial); }

    function listRemote(which) {
      setBrowseDir(which);
      setFilesLoading(true);
      const dir = form.directories[which];
      setTimeout(() => {
        const now = Date.now();
        let listing = [];
        if (which === 'outbox') {
          listing = [
            { name: `CW25${(1000 + Math.floor(Math.random()*9000))}PLU_${code.slice(0,3)}.V21`, size: 18430, ts: ts(now - 4*3600e3) },
            { name: `CW25${(1000 + Math.floor(Math.random()*9000))}PLU_${code.slice(0,3)}.V21`, size: 9420,  ts: ts(now - 8*3600e3) },
            { name: `CW25${(1000 + Math.floor(Math.random()*9000))}PLU_${code.slice(0,3)}.V21`, size: 22118, ts: ts(now - 26*3600e3) },
          ];
        } else if (which === 'inbox') {
          listing = [
            { name: `ACK_${code}_${ymd(now-2*3600e3)}_001.V21`, size: 4120, ts: ts(now - 2*3600e3) },
            { name: `ACK_${code}_${ymd(now-26*3600e3)}_001.V21`, size: 3982, ts: ts(now - 26*3600e3) },
          ];
        } else if (which === 'archive') {
          listing = [
            { name: `CW25_PLU_${code.slice(0,3)}_2025Q1.zip`, size: 184302, ts: '2025-03-31 23:55' },
            { name: `CW24_PLU_${code.slice(0,3)}_2024Q4.zip`, size: 192401, ts: '2024-12-31 23:50' },
          ];
        } else if (which === 'ack') {
          listing = [
            { name: `ACK_RECEIPT_${ymd(now)}.V21`, size: 1230, ts: ts(now - 1*3600e3) },
          ];
        }
        setFiles({ dir, items: listing });
        setFilesLoading(false);
      }, 500);
    }
    function ts(d) { return new Date(d).toISOString().slice(0,16).replace('T',' '); }
    function ymd(d) { return new Date(d).toISOString().slice(0,10); }

    return (
      <div style={{ marginTop: 22, marginBottom: 22, border: '1px solid var(--ink)', background: 'var(--bg)' }}>
        {/* Header */}
        <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--rule)', display: 'flex', alignItems: 'center', gap: 12 }}>
          <Mono size={9}>FTP CREDENTIALS</Mono>
          <span style={{ fontSize: 16, fontWeight: 600 }}>{code.replace('_', ' ')}</span>
          <Chip kind={proto === 'FTP' ? 'warn' : 'muted'}>{proto}</Chip>
          {dirty && <Chip kind="ink">UNSAVED</Chip>}
          {savedAt && !dirty && <Chip kind="ok">SAVED · {new Date(savedAt).toLocaleTimeString()}</Chip>}
          <span style={{ flex: 1 }}/>
          <button onClick={onClose} style={{ background: 'transparent', border: 0, fontSize: 18, cursor: 'pointer', color: 'var(--ink-3)' }}>×</button>
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 0 }}>
          {/* LEFT: editable credentials */}
          <div style={{ padding: 22, borderRight: '1px solid var(--rule)' }}>
            <Mono size={9} style={{ display: 'block', marginBottom: 14 }}>CONNECTION</Mono>
            <FormGrid>
              <Field label="Host">
                <Input value={form.host} onChange={v => patch({ host: v })} placeholder="ftp.society.example"/>
              </Field>
              <Field label="Port">
                <Input value={form.port} onChange={v => patch({ port: v })} placeholder="21" mono numeric/>
              </Field>
              <Field label="Username">
                <Input value={form.user} onChange={v => patch({ user: v })} placeholder="pluralis_xx" mono/>
              </Field>
              <Field label="Password" hint={form.password ? 'Will re-encrypt on save' : 'Stored encrypted (AES-256)'}>
                <PasswordField
                  plaintext={form.password}
                  encrypted={form.passwordEnc}
                  onChange={v => patch({ password: v })}
                  show={showPw}
                  onToggleShow={() => setShowPw(s => !s)}
                />
              </Field>
              {proto === 'FTPS' && (
                <>
                  <Field label="TLS mode">
                    <Select value={form.mode} onChange={v => patch({ mode: v, port: v === 'implicit' ? 990 : 21 })}
                      options={[['explicit','explicit (AUTH TLS)'], ['implicit','implicit (port 990)']]}/>
                  </Field>
                  <Field label="TLS verify">
                    <Toggle on={form.tlsVerify} onChange={v => patch({ tlsVerify: v })}
                      onLabel="enabled" offLabel="disabled (insecure)"/>
                  </Field>
                  <Field label="Cert pinning" hint="Optional · SHA-256 fingerprint of server certificate" wide>
                    <Input value={form.fingerprint} onChange={v => patch({ fingerprint: v })} placeholder="SHA256:…" mono/>
                  </Field>
                </>
              )}
              <Field label="Passive mode" hint="Required for most NAT / firewall environments">
                <Toggle on={form.passive} onChange={v => patch({ passive: v })}
                  onLabel="passive (PASV)" offLabel="active (PORT)"/>
              </Field>
            </FormGrid>

            <div style={{ height: 18 }}/>
            <Mono size={9} style={{ display: 'block', marginBottom: 14 }}>DIRECTORIES · REMOTE PATHS</Mono>
            <FormGrid>
              <Field label="Outbox" hint="CWR files we PUT to the society" wide>
                <Input value={form.directories.outbox} onChange={v => patchDir('outbox', v)} mono/>
              </Field>
              <Field label="Inbox" hint="ACK files we GET from the society" wide>
                <Input value={form.directories.inbox} onChange={v => patchDir('inbox', v)} mono/>
              </Field>
              <Field label="Archive" hint="Society-side long-term storage" wide>
                <Input value={form.directories.archive} onChange={v => patchDir('archive', v)} mono/>
              </Field>
              <Field label="Ack receipts" hint="Per-file delivery receipts" wide>
                <Input value={form.directories.ack} onChange={v => patchDir('ack', v)} mono/>
              </Field>
            </FormGrid>

            {/* Action row */}
            <div style={{ marginTop: 22, display: 'flex', gap: 8, alignItems: 'center' }}>
              <button onClick={save} disabled={!dirty} className="ff-mono upper" style={{
                padding: '9px 16px', background: dirty ? 'var(--ink)' : 'var(--bg-2)', color: dirty ? 'var(--bg)' : 'var(--ink-3)',
                border: 0, fontSize: 10, letterSpacing: '.12em', cursor: dirty ? 'pointer' : 'not-allowed', fontWeight: 600,
              }}>SAVE CHANGES</button>
              <button onClick={revert} disabled={!dirty} className="ff-mono upper" style={{
                padding: '9px 16px', background: 'transparent', border: '1px solid var(--rule)',
                fontSize: 10, letterSpacing: '.12em', cursor: dirty ? 'pointer' : 'not-allowed',
                color: dirty ? 'var(--ink)' : 'var(--ink-4)',
              }}>REVERT</button>
              <span style={{ flex: 1 }}/>
              <button onClick={onRetest} className="ff-mono upper" style={{
                padding: '9px 16px', background: 'transparent', border: '1px solid var(--ink)',
                fontSize: 10, letterSpacing: '.12em', cursor: 'pointer', color: 'var(--ink)',
              }}>RETEST CONNECTION</button>
            </div>

            <div style={{ marginTop: 14, padding: 10, background: 'var(--bg-2)', borderLeft: '2px solid var(--ink)', fontSize: 10, color: 'var(--ink-3)', lineHeight: 1.6 }}>
              <strong>Encryption:</strong> passwords are stored as <span className="ff-mono">enc:AES256:&lt;hex&gt;</span> — never as plaintext. Typing a new password re-encrypts on save. The current encrypted blob remains opaque even when "show" is toggled unless you provide a new plaintext.
            </div>
          </div>

          {/* RIGHT: remote folder browser, scoped to the directories above */}
          <div style={{ padding: 22 }}>
            <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 14 }}>
              <Mono size={9}>REMOTE BROWSER · {form.host}</Mono>
              {filesLoading && <Mono size={9}>OPENING DATA CHANNEL…</Mono>}
            </div>

            {/* Directory tabs */}
            <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid var(--rule)', marginBottom: 14 }}>
              {[
                ['outbox',  'Outbox',  form.directories.outbox],
                ['inbox',   'Inbox',   form.directories.inbox],
                ['archive', 'Archive', form.directories.archive],
                ['ack',     'Ack',     form.directories.ack],
              ].map(([k, l, p]) => (
                <button key={k} onClick={() => listRemote(k)} className="ff-mono upper" style={{
                  padding: '9px 14px', fontSize: 10, letterSpacing: '.1em', fontWeight: 600,
                  background: 'transparent', border: 0, cursor: 'pointer',
                  borderBottom: browseDir === k && files ? '2px solid var(--ink)' : '2px solid transparent',
                  color: browseDir === k && files ? 'var(--ink)' : 'var(--ink-3)',
                  marginBottom: -1,
                }}>{l}</button>
              ))}
            </div>

            {!files && !filesLoading && (
              <div style={{ fontSize: 12, color: 'var(--ink-3)', padding: '24px 0', lineHeight: 1.5 }}>
                Pick a directory tab to <strong>LIST</strong> its contents over the data channel.
                Listings reflect the <em>configured</em> remote paths above — edit them and the
                browser follows.
              </div>
            )}

            {files && (
              <>
                <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 8 }}>{files.dir} · {files.items.length} entries</div>
                <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11, fontFamily: 'ui-monospace, monospace' }}>
                  <tbody>
                    {files.items.length === 0 && (
                      <tr><td colSpan="3" style={{ padding: '10px 0', color: 'var(--ink-3)', fontSize: 11 }}>(empty)</td></tr>
                    )}
                    {files.items.map((f, i) => (
                      <tr key={i} style={{ borderBottom: '1px solid var(--rule-soft)' }}>
                        <td style={{ padding: '6px 0', color: 'var(--ink-2)' }}>{f.name}</td>
                        <td style={{ padding: '6px 0', textAlign: 'right', color: 'var(--ink-3)', fontSize: 10 }}>{formatBytes(f.size)}</td>
                        <td style={{ padding: '6px 0', textAlign: 'right', color: 'var(--ink-3)', fontSize: 10, paddingLeft: 12 }}>{f.ts}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </>
            )}

            {/* Test result strip */}
            {test && (
              <div style={{ marginTop: 18, padding: '10px 12px', borderTop: '1px solid var(--rule)', display: 'flex', alignItems: 'center', gap: 10, fontSize: 10 }}>
                <Mono size={9}>LAST TEST</Mono>
                {test.ok && !test.warning && <Chip kind="ok">CONNECTED · {test.latencyMs}MS</Chip>}
                {test.ok && test.warning && <Chip kind="warn">{test.warning.replace('_',' ')}</Chip>}
                {!test.ok && <Chip kind="err">{test.error}</Chip>}
                <span className="ff-mono" style={{ color: 'var(--ink-3)', fontSize: 10 }}>{test.msg}</span>
                <span style={{ flex: 1 }}/>
                <span className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)' }}>{new Date(test.ts).toLocaleTimeString()}</span>
              </div>
            )}
          </div>
        </div>
      </div>
    );
  }

  // ─── Form primitives ─────────────────────────────────────────────
  function FormGrid({ children }) {
    return <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '14px 18px' }}>{children}</div>;
  }
  function Field({ label, hint, wide, children }) {
    return (
      <div style={{ gridColumn: wide ? '1 / -1' : 'auto' }}>
        <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', marginBottom: 5 }}>{label}</div>
        {children}
        {hint && <div style={{ fontSize: 10, color: 'var(--ink-4)', marginTop: 4, lineHeight: 1.4 }}>{hint}</div>}
      </div>
    );
  }
  function Input({ value, onChange, placeholder, mono, numeric }) {
    return (
      <input value={value} onChange={e => onChange(numeric ? e.target.value.replace(/[^0-9]/g, '') : e.target.value)}
        placeholder={placeholder} className={mono ? 'ff-mono' : ''}
        style={{
          width: '100%', padding: '8px 10px', fontSize: 12, color: 'var(--ink)',
          background: 'var(--bg)', border: '1px solid var(--rule)', outline: 'none',
          fontFamily: mono ? 'ui-monospace, monospace' : undefined,
          boxSizing: 'border-box',
        }}
        onFocus={e => e.target.style.borderColor = 'var(--ink)'}
        onBlur={e => e.target.style.borderColor = 'var(--rule)'}
      />
    );
  }
  function PasswordField({ plaintext, encrypted, onChange, show, onToggleShow }) {
    return (
      <div style={{ display: 'flex', gap: 6, alignItems: 'stretch' }}>
        <input
          type={show ? 'text' : 'password'}
          value={plaintext}
          placeholder={encrypted ? '•••••••• (encrypted, type to replace)' : 'enter password'}
          onChange={e => onChange(e.target.value)}
          className="ff-mono"
          style={{
            flex: 1, padding: '8px 10px', fontSize: 12, color: 'var(--ink)',
            background: 'var(--bg)', border: '1px solid var(--rule)', outline: 'none',
            fontFamily: 'ui-monospace, monospace', boxSizing: 'border-box',
          }}
          onFocus={e => e.target.style.borderColor = 'var(--ink)'}
          onBlur={e => e.target.style.borderColor = 'var(--rule)'}
        />
        <button onClick={onToggleShow} className="ff-mono upper" type="button" style={{
          padding: '0 12px', background: 'transparent', border: '1px solid var(--rule)',
          fontSize: 9, letterSpacing: '.1em', cursor: 'pointer', color: 'var(--ink-2)',
        }}>{show ? 'HIDE' : 'SHOW'}</button>
      </div>
    );
  }
  function Select({ value, onChange, options }) {
    return (
      <select value={value} onChange={e => onChange(e.target.value)} className="ff-mono"
        style={{
          width: '100%', padding: '8px 10px', fontSize: 12, color: 'var(--ink)',
          background: 'var(--bg)', border: '1px solid var(--rule)', outline: 'none',
          fontFamily: 'ui-monospace, monospace', boxSizing: 'border-box',
        }}>
        {options.map(([v, l]) => <option key={v} value={v}>{l}</option>)}
      </select>
    );
  }
  function Toggle({ on, onChange, onLabel = 'on', offLabel = 'off' }) {
    return (
      <button onClick={() => onChange(!on)} type="button" className="ff-mono upper" style={{
        display: 'flex', alignItems: 'center', gap: 8,
        padding: '8px 10px', fontSize: 10, letterSpacing: '.1em',
        background: 'var(--bg)', border: '1px solid var(--rule)', cursor: 'pointer',
        color: 'var(--ink)', width: '100%', textAlign: 'left',
      }}>
        <span style={{
          width: 28, height: 14, background: on ? '#0a8754' : 'var(--rule)',
          position: 'relative', transition: 'background 0.15s',
        }}>
          <span style={{
            position: 'absolute', top: 1, left: on ? 15 : 1, width: 12, height: 12,
            background: 'white', transition: 'left 0.15s',
          }}/>
        </span>
        <span style={{ color: on ? 'var(--ink)' : 'var(--ink-3)' }}>{on ? onLabel : offLabel}</span>
      </button>
    );
  }

  function formatBytes(n) {
    if (n < 1024) return n + ' B';
    if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
    return (n / (1024 * 1024)).toFixed(1) + ' MB';
  }

  // ─── Security audit panel ──────────────────────────────────────
  function SecurityAudit({ societies, tests }) {
    const findings = useMemo(() => {
      const out = [];
      societies.forEach(({ code, cfg, cred }) => {
        const ftp = cred?.ftp || cred?.ftps;
        if (cfg.primary === 'FTP') {
          out.push({ code, severity: 'high', kind: 'CLEARTEXT_FTP',
            msg: 'Plain FTP transmits credentials and data unencrypted. Recommend migrating to FTPS or SFTP.' });
        }
        if (cfg.primary === 'FTPS' && ftp && ftp.tlsVerify === false) {
          out.push({ code, severity: 'medium', kind: 'TLS_VERIFY_DISABLED',
            msg: 'TLS certificate verification is disabled. Vulnerable to man-in-the-middle attack.' });
        }
        if (ftp && !ftp.passive) {
          out.push({ code, severity: 'low', kind: 'ACTIVE_MODE',
            msg: 'Active mode enabled. Most production NATs and firewalls require passive mode.' });
        }
        if ((cfg.primary === 'FTP' || cfg.primary === 'FTPS') && !cfg.fallback) {
          out.push({ code, severity: 'low', kind: 'NO_FALLBACK',
            msg: 'No fallback transport configured. Connection failures will block delivery.' });
        }
        if (cfg.primary === 'FTPS' && ftp && !ftp.fingerprint) {
          out.push({ code, severity: 'low', kind: 'NO_PINNING',
            msg: 'Server certificate fingerprint not pinned. Trust falls back to system CA store.' });
        }
      });
      return out;
    }, [societies]);

    if (findings.length === 0) return null;

    const sevColor = { high: '#a32a18', medium: '#a35418', low: 'var(--ink-3)' };

    return (
      <div style={{ marginTop: 32, paddingTop: 22, borderTop: '1px solid var(--rule)' }}>
        <Mono size={10} style={{ display: 'block', marginBottom: 4, letterSpacing: '.14em' }}>SECURITY AUDIT · {findings.length} FINDINGS</Mono>
        <div style={{ fontSize: 11, color: 'var(--ink-3)', marginBottom: 14, maxWidth: 720, lineHeight: 1.6 }}>
          Static review of FTP / FTPS configuration. Findings reflect protocol-level risk, not the
          state of any in-flight transfer. Resolve high-severity issues before going live.
        </div>
        <div>
          {findings.map((f, i) => (
            <div key={i} style={{
              display: 'grid', gridTemplateColumns: '90px 110px 200px 1fr',
              gap: 14, padding: '11px 14px', borderBottom: '1px solid var(--rule-soft)', alignItems: 'baseline',
            }}>
              <Chip kind={f.severity === 'high' ? 'err' : f.severity === 'medium' ? 'warn' : 'muted'}>{f.severity}</Chip>
              <span className="ff-mono" style={{ fontSize: 11, fontWeight: 500 }}>{f.code.replace('_',' ')}</span>
              <span className="ff-mono upper" style={{ fontSize: 10, color: sevColor[f.severity], letterSpacing: '.1em' }}>{f.kind}</span>
              <span style={{ fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.4 }}>{f.msg}</span>
            </div>
          ))}
        </div>
      </div>
    );
  }

  // ─── Spec reference ────────────────────────────────────────────
  function SpecReference() {
    return (
      <div style={{ marginTop: 32, paddingTop: 22, borderTop: '1px solid var(--rule)' }}>
        <Mono size={10} style={{ display: 'block', marginBottom: 14, letterSpacing: '.14em' }}>FTP / FTPS REFERENCE</Mono>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 18 }}>
          <RefCard title="FTP" subtitle="RFC 959" lines={[
            ['Port',         '21'],
            ['Encryption',   'none'],
            ['Auth',         'USER + PASS (cleartext)'],
            ['Data channel', 'PORT (active) / PASV (passive)'],
            ['Use case',     'legacy partners only'],
          ]}/>
          <RefCard title="FTPS — explicit" subtitle="RFC 4217 · AUTH TLS" lines={[
            ['Port',         '21 (control)'],
            ['Encryption',   'TLS after AUTH TLS'],
            ['Auth',         'USER + PASS over TLS'],
            ['Data channel', 'PROT P (encrypted)'],
            ['Use case',     'most modern FTPS deployments'],
          ]}/>
          <RefCard title="FTPS — implicit" subtitle="port 990" lines={[
            ['Port',         '990 (control)'],
            ['Encryption',   'TLS from connection start'],
            ['Auth',         'USER + PASS over TLS'],
            ['Data channel', 'always encrypted'],
            ['Use case',     'older FTPS clients / strict envs'],
          ]}/>
        </div>
        <div style={{ marginTop: 16, padding: 14, background: 'var(--bg-2)', borderLeft: '2px solid var(--ink)', fontSize: 11, color: 'var(--ink-2)', lineHeight: 1.6 }}>
          <strong>ASTRO behavior:</strong> all FTP/FTPS submissions wrap into the same submission queue
          as SFTP and PORTAL — events flow through QUEUED → UPLOADING → UPLOADED → ACK_PENDING → ACKED.
          Cleartext FTP transfers add a CLEARTEXT_FTP audit-log entry with severity HIGH on every
          successful upload. Failed TLS handshakes promote the configured fallback transport before
          giving up.
        </div>
      </div>
    );
  }

  function RefCard({ title, subtitle, lines }) {
    return (
      <div style={{ padding: 18, border: '1px solid var(--rule)', background: 'var(--bg)' }}>
        <Mono size={9} style={{ display: 'block', marginBottom: 6 }}>{subtitle}</Mono>
        <div style={{ fontSize: 16, fontWeight: 600, marginBottom: 14 }}>{title}</div>
        <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
          <tbody>
            {lines.map(([k, v]) => (
              <tr key={k} style={{ borderBottom: '1px solid var(--rule-soft)' }}>
                <td className="ff-mono upper" style={{ padding: '5px 0', fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', width: 110 }}>{k}</td>
                <td className="ff-mono" style={{ padding: '5px 0', fontSize: 11 }}>{v}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }

  window.FtpTab = FtpTab;
  console.log('[FtpTab] loaded');
})();
