// Author: Sam Lautenbach — Motor Row (offerlaneauto.com)
/* feeds.jsx — "Inventory feed" panel in Seller mode: connect a dealership's inventory system to Motor Row.
   Upload a vAuto/HomeNet/DMS export with a preview, or set up the automatic SFTP feed, a feed URL we pull, or the push API.
   Loaded after listings.jsx (uses cfsApi) and before app.bundle.jsx. */
const { useState: useStateF, useEffect: useEffectF, useRef: useRefF } = React;

const FEED_TABS = [['upload', 'Upload a file'], ['sftp', 'Automatic feed (SFTP)'], ['url', 'Feed URL'], ['api', 'API'], ['settings', 'Settings'], ['history', 'History']];
const FEED_SOURCE = { upload: 'Upload', sftp: 'SFTP', url: 'Feed URL', api: 'API' };
const feedWhen = (ts) => ts ? new Date(ts).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : '—';
const fdBox = { border: '1px solid var(--line)', borderRadius: 'var(--r-md)', padding: '14px 16px', background: '#fff' };
const fdMono = { fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 13.5, background: 'var(--paper)', border: '1px solid var(--line)', borderRadius: 8, padding: '8px 10px', wordBreak: 'break-all' };
const fdH = { fontSize: 17, fontWeight: 800, color: 'var(--navy)', margin: '0 0 6px' };
const fdP = { color: 'var(--ink-soft)', lineHeight: 1.55, fontSize: 14.5, margin: '0 0 10px' };
const fdStatus = (status) => ({ ok: ['Imported', 'var(--green-600)'], partial: ['Imported with skips', '#8A5A00'], failed: ['Failed', '#B23A12'], running: ['Running…', 'var(--navy)'], queued: ['Queued', 'var(--navy)'] }[status] || [status, 'var(--ink-soft)']);

function FdCopy({ value, label }) {
  const [done, setDone] = useStateF(false);
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
      <button type="button" className="btn btn-ghost btn-sm" onClick={async () => { try { await navigator.clipboard.writeText(value); setDone(true); setTimeout(() => setDone(false), 1800); } catch (e) { window.prompt('Copy this value:', value); } }} aria-label={`Copy ${label}`}>Copy</button>
      <span role="status" aria-live="polite" style={{ fontSize: 12.5, color: 'var(--green-600)', fontWeight: 700, minWidth: 46 }}>{done ? 'Copied' : ''}</span>
    </span>
  );
}
function FdRow({ k, v, copy }) {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '130px 1fr auto', gap: 10, alignItems: 'center', padding: '8px 0', borderBottom: '1px solid var(--line-soft)' }}>
      <div className="field-label" style={{ marginBottom: 0 }}>{k}</div>
      <div style={{ fontWeight: 700, fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 14, wordBreak: 'break-all' }}>{v}</div>
      <div>{copy && v && v !== '—' ? <FdCopy value={v} label={k} /> : null}</div>
    </div>
  );
}
function FdMsg({ kind = 'ok', children }) {
  return <div role="status" aria-live="polite" style={{ borderRadius: 'var(--r-md)', padding: '10px 14px', fontWeight: 600, fontSize: 14, background: kind === 'ok' ? 'var(--green-tint)' : kind === 'warn' ? '#FFF4E0' : '#FCE7DA', color: kind === 'ok' ? 'var(--green-600)' : kind === 'warn' ? '#8A5A00' : '#B23A12' }}>{children}</div>;
}
function FdRunSummary({ run }) {
  if (!run) return null;
  const [label, color] = fdStatus(run.status);
  const live = run.status === 'queued' || run.status === 'running';
  return (
    <div style={{ ...fdBox, borderLeft: `5px solid ${color}` }} role={live ? 'status' : undefined} aria-live={live ? 'polite' : undefined}>
      <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'baseline' }}>
        <strong style={{ color }}>{label}</strong>
        <span style={{ color: 'var(--ink-soft)', fontSize: 13.5 }}>{FEED_SOURCE[run.source] || run.source}{run.file_name ? ` · ${run.file_name}` : ''} · {feedWhen(run.started_at)}</span>
        {live && <span style={{ marginLeft: 'auto', fontSize: 13.5, color: 'var(--ink-soft)' }}>{run.progress || 0} / {run.total || run.valid || 0} cars{run.photos ? ` · ${run.photos} photos` : ''}</span>}
      </div>
      {live ? (
        <div style={{ marginTop: 10, height: 8, background: 'var(--line-soft)', borderRadius: 999, overflow: 'hidden' }} aria-hidden="true"><div style={{ width: `${run.total ? Math.min(100, Math.round(100 * (run.progress || 0) / run.total)) : 5}%`, height: '100%', background: 'var(--green)', transition: 'width .4s' }} /></div>
      ) : (
        <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', marginTop: 8, fontSize: 14 }}>
          <span><strong>{run.added}</strong> added</span><span><strong>{run.updated}</strong> updated</span><span><strong>{run.unchanged}</strong> unchanged</span><span><strong>{run.sold}</strong> marked sold</span><span><strong>{run.skipped}</strong> skipped</span><span><strong>{run.photos}</strong> photos downloaded</span>
        </div>
      )}
      {run.message && <p style={{ marginTop: 8, fontSize: 14, color: 'var(--ink-soft)' }}>{run.message}</p>}
      {run.issues && run.issues.length > 0 && (
        <details style={{ marginTop: 8 }}><summary style={{ cursor: 'pointer', fontWeight: 700, fontSize: 13.5, color: 'var(--navy)' }}>{run.issues.length} note{run.issues.length === 1 ? '' : 's'} on skipped or changed rows</summary>
          <ul style={{ margin: '8px 0 0', paddingLeft: 20, fontSize: 13, color: 'var(--ink-soft)', lineHeight: 1.5, maxHeight: 220, overflow: 'auto' }}>{run.issues.map((s, i) => <li key={i}>{s}</li>)}</ul></details>
      )}
    </div>
  );
}

/* ------------------------------ Upload ------------------------------ */
function FdUpload({ data, onImported, onRun }) {
  const [file, setFile] = useStateF(null);          // { name, text }
  const [full, setFull] = useStateF(true);
  const [preview, setPreview] = useStateF(null);
  const [busy, setBusy] = useStateF('');
  const [err, setErr] = useStateF('');
  const [run, setRun] = useStateF(null);
  const inputRef = useRefF(null);
  const pollRef = useRefF(null);
  useEffectF(() => () => clearTimeout(pollRef.current), []);

  const readFile = (f) => new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => resolve(String(r.result || '')); r.onerror = () => reject(new Error('Could not read that file.')); r.readAsText(f); });
  const doPreview = async (fl, isFull) => {
    setBusy('Reading the file…'); setErr(''); setPreview(null); setRun(null);
    try {
      const p = await cfsApi('/api/feeds/preview', { method: 'POST', body: { name: fl.name, content: fl.text, mode: isFull ? 'full' : 'partial' } });
      setPreview(p);
    } catch (e) { setErr(e.message); }
    setBusy('');
  };
  const pick = async (f) => {
    if (!f) return;
    if (f.size > 14 * 1024 * 1024) { setErr('That file is over 14 MB. Use the SFTP feed or API for files this large.'); return; }
    try { const text = await readFile(f); const fl = { name: f.name, text }; setFile(fl); await doPreview(fl, full); } catch (e) { setErr(e.message); }
  };
  const poll = (id) => {
    cfsApi('/api/feeds/runs/' + id).then(({ run: r }) => {
      setRun(r);
      if (r.status === 'queued' || r.status === 'running') pollRef.current = setTimeout(() => poll(id), 1500);
      else { onImported && onImported(); onRun && onRun(); }
    }).catch(() => { pollRef.current = setTimeout(() => poll(id), 3000); });
  };
  const doImport = async () => {
    setBusy('Starting import…'); setErr('');
    try {
      const { run: r } = await cfsApi('/api/feeds/import', { method: 'POST', body: { token: preview.token, mode: full ? 'full' : 'partial' } });
      setPreview(null); setFile(null); setRun(r); poll(r.id);
    } catch (e) { setErr(e.message); }
    setBusy('');
  };
  const planLine = (label, p, color) => p && p.count > 0 && (
    <li style={{ marginBottom: 6 }}><strong style={{ color }}>{p.count}</strong> {label}{p.sample.length ? <span style={{ color: 'var(--muted)' }}> — {p.sample.map(s => s.title).filter(Boolean).slice(0, 4).join(', ')}{p.count > 4 ? ', …' : ''}</span> : null}</li>
  );

  return (
    <div>
      <h3 style={fdH}>Upload an inventory export</h3>
      <p style={fdP}>Export your inventory from vAuto, HomeNet, Dealer Specialties, your DMS or a spreadsheet as CSV, TSV, XML or JSON and drop it here. You will see exactly what changes before anything is imported. <a href={data.template} style={{ color: 'var(--navy)', textDecoration: 'underline', fontWeight: 700 }}>Download the CSV template</a> · <a href={data.docs} target="_blank" rel="noopener" style={{ color: 'var(--navy)', textDecoration: 'underline', fontWeight: 700 }}>Accepted columns</a></p>
      <div onDragOver={e => { e.preventDefault(); }} onDrop={e => { e.preventDefault(); pick(e.dataTransfer.files && e.dataTransfer.files[0]); }}
        style={{ border: '2px dashed var(--line)', borderRadius: 'var(--r-md)', padding: '22px 18px', textAlign: 'center', background: 'var(--paper)' }}>
        <input ref={inputRef} id="fd-file" type="file" accept=".csv,.tsv,.txt,.xml,.json,text/csv,text/plain,application/xml,text/xml,application/json" className="sr-only" onChange={e => pick(e.target.files && e.target.files[0])} />
        <label htmlFor="fd-file" className="btn btn-green btn-md" style={{ cursor: 'pointer' }}>Choose a file</label>
        <p style={{ color: 'var(--muted)', fontSize: 13.5, marginTop: 10 }}>or drag it here · CSV, TSV, XML or JSON up to 14 MB{file ? ` · selected: ${file.name}` : ''}</p>
      </div>
      <label style={{ display: 'flex', alignItems: 'flex-start', gap: 10, marginTop: 14, fontSize: 14.5, lineHeight: 1.5, cursor: 'pointer' }}>
        <input type="checkbox" checked={full} onChange={e => { setFull(e.target.checked); if (file) doPreview(file, e.target.checked); }} style={{ width: 18, height: 18, marginTop: 3, accentColor: 'var(--green)' }} />
        <span>This file is my <strong>complete</strong> inventory — mark my feed-imported cars that are missing from it as sold. <span style={{ color: 'var(--muted)' }}>(Cars posted by hand are never auto-sold.)</span></span>
      </label>
      {busy && <p role="status" aria-live="polite" style={{ marginTop: 12, display: 'flex', gap: 10, alignItems: 'center', color: 'var(--ink-soft)' }}><MiniSpin size={18} /> {busy}</p>}
      {err && <div style={{ marginTop: 12 }}><FdMsg kind="err">{err}</FdMsg></div>}

      {preview && (
        <div style={{ marginTop: 16, display: 'grid', gap: 12 }}>
          <div style={fdBox}>
            <div style={{ fontWeight: 800, color: 'var(--navy)', marginBottom: 6 }}>{preview.file} · {preview.format.toUpperCase()} · {preview.rows} row{preview.rows === 1 ? '' : 's'}, {preview.valid} usable, {preview.photos} photo link{preview.photos === 1 ? '' : 's'}</div>
            <ul style={{ margin: 0, paddingLeft: 18, lineHeight: 1.5, fontSize: 14.5 }}>
              {planLine('new car' + (preview.plan.add.count === 1 ? '' : 's') + ' will be added', preview.plan.add, 'var(--green-600)')}
              {planLine('existing car' + (preview.plan.update.count === 1 ? '' : 's') + ' will be updated (same listing URL)', preview.plan.update, 'var(--navy)')}
              {planLine('sold car' + (preview.plan.relist.count === 1 ? '' : 's') + ' will be relisted', preview.plan.relist, 'var(--navy)')}
              {planLine('car' + (preview.plan.sold.count === 1 ? '' : 's') + ' not in this file will be marked sold', preview.plan.sold, '#8A5A00')}
              {planLine('row' + (preview.plan.skip.count === 1 ? '' : 's') + ' will be skipped', preview.plan.skip, '#B23A12')}
              {preview.valid === 0 && <li style={{ color: '#B23A12', fontWeight: 700 }}>No usable rows. Check the column list below.</li>}
            </ul>
          </div>
          <details style={fdBox}>
            <summary style={{ cursor: 'pointer', fontWeight: 700, color: 'var(--navy)' }}>How columns were matched ({Object.keys(preview.mapping).length + (preview.photo_columns.length ? 1 : 0)} recognized{preview.unmapped.length ? `, ${preview.unmapped.length} ignored` : ''})</summary>
            <table style={{ width: '100%', borderCollapse: 'collapse', marginTop: 10, fontSize: 13.5 }}>
              <thead><tr><th style={{ textAlign: 'left', padding: '4px 8px 4px 0', color: 'var(--muted)' }}>Motor Row field</th><th style={{ textAlign: 'left', padding: '4px 0', color: 'var(--muted)' }}>Your column</th></tr></thead>
              <tbody>
                {Object.entries(preview.mapping).map(([f, h]) => <tr key={f}><td style={{ padding: '3px 8px 3px 0', fontWeight: 700 }}>{f.replace(/_/g, ' ')}</td><td style={{ padding: '3px 0', fontFamily: 'ui-monospace, Menlo, monospace' }}>{h}</td></tr>)}
                {preview.photo_columns.length > 0 && <tr><td style={{ padding: '3px 8px 3px 0', fontWeight: 700 }}>photos</td><td style={{ padding: '3px 0', fontFamily: 'ui-monospace, Menlo, monospace' }}>{preview.photo_columns.join(', ')}</td></tr>}
                {preview.unmapped.length > 0 && <tr><td style={{ padding: '3px 8px 3px 0', color: 'var(--muted)' }}>ignored</td><td style={{ padding: '3px 0', color: 'var(--muted)', fontFamily: 'ui-monospace, Menlo, monospace' }}>{preview.unmapped.join(', ')}</td></tr>}
              </tbody>
            </table>
          </details>
          {preview.issues.length > 0 && (
            <details style={fdBox}><summary style={{ cursor: 'pointer', fontWeight: 700, color: 'var(--navy)' }}>{preview.issue_count} row note{preview.issue_count === 1 ? '' : 's'}</summary>
              <ul style={{ margin: '8px 0 0', paddingLeft: 20, fontSize: 13, color: 'var(--ink-soft)', lineHeight: 1.5, maxHeight: 200, overflow: 'auto' }}>{preview.issues.map((s, i) => <li key={i}>{s}</li>)}</ul></details>
          )}
          <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center' }}>
            <button type="button" className="btn btn-green btn-md" disabled={!!busy || (preview.valid === 0 && preview.plan.sold.count === 0)} onClick={doImport}>
              Import {preview.valid} car{preview.valid === 1 ? '' : 's'}{preview.plan.sold.count ? ` and mark ${preview.plan.sold.count} sold` : ''}
            </button>
            <button type="button" className="btn btn-ghost btn-md" onClick={() => { setPreview(null); setFile(null); if (inputRef.current) inputRef.current.value = ''; }}>Cancel</button>
          </div>
        </div>
      )}
      {run && <div style={{ marginTop: 16 }}><FdRunSummary run={run} /></div>}
    </div>
  );
}

/* ------------------------------ SFTP ------------------------------ */
function FdSftp({ data, reload }) {
  const s = data.feed.sftp;
  const [pw, setPw] = useStateF(null);
  const [busy, setBusy] = useStateF(false);
  const [err, setErr] = useStateF('');
  const lastSftp = (data.runs || []).find(r => r.source === 'sftp');
  const makePw = async () => {
    if (s.password_set && !window.confirm('Create a new password? The old one stops working immediately, so update it in your inventory tool too.')) return;
    setBusy(true); setErr('');
    try { setPw(await cfsApi('/api/feeds/sftp/password', { method: 'POST', body: {} })); reload(); } catch (e) { setErr(e.message); }
    setBusy(false);
  };
  const host = s.host || 'pending setup';
  return (
    <div>
      <h3 style={fdH}>Automatic feed from vAuto, HomeNet and other inventory tools</h3>
      <p style={fdP}>Your inventory system sends your cars to Motor Row on its own schedule, usually every night, exactly the way it already sends them to Autotrader or Cars.com. Set it up once and forget it: new cars appear, prices and photos update, and sold cars drop off.</p>
      <div style={{ marginBottom: 12 }}>
        {s.ready ? <FdMsg kind="ok">Ready. The SFTP drop is online and reachable at {s.host}.</FdMsg>
          : s.listening ? <FdMsg kind="warn">Almost ready: the SFTP receiver is running, but it is not reachable from the internet yet, so your inventory tool can't connect until the public address is set up. Uploads, the feed URL and the API all work now.</FdMsg>
          : <FdMsg kind="err">The SFTP receiver is not running right now. Uploads, the feed URL and the API still work.</FdMsg>}
      </div>
      <div style={fdBox}>
        <FdRow k="Protocol" v="SFTP (SSH File Transfer)" />
        <FdRow k="Host" v={host} copy={!!s.host} />
        <FdRow k="Port" v={String(s.port)} copy />
        <FdRow k="Username" v={s.username} copy />
        <FdRow k="Password" v={pw ? pw.password : s.password_set ? `set ${feedWhen(s.password_at)} (hidden)` : 'not set yet'} copy={!!pw} />
        <FdRow k="Folder" v="/ (any folder name works; every file lands in your inbox)" />
        <FdRow k="File name" v="anything, e.g. inventory.csv — CSV, TSV, XML or JSON" />
        {pw && <div style={{ marginTop: 10 }}><FdMsg kind="warn">Copy the password now. For your security it is not stored in a readable form and cannot be shown again; you can create a new one at any time.</FdMsg></div>}
        <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center', marginTop: 12 }}>
          <button type="button" className="btn btn-green btn-sm" disabled={busy} onClick={makePw}>{s.password_set ? 'Create a new password' : 'Create password'}</button>
          {err && <span style={{ color: '#B23A12', fontWeight: 600, fontSize: 13.5 }}>{err}</span>}
        </div>
      </div>
      {s.fingerprints && s.fingerprints.length > 0 && (
        <details style={{ marginTop: 10, fontSize: 13, color: 'var(--ink-soft)' }}><summary style={{ cursor: 'pointer', fontWeight: 700, color: 'var(--navy)' }}>Host key fingerprints (if your tool asks you to verify the server)</summary>
          <ul style={{ margin: '6px 0 0', paddingLeft: 18, fontFamily: 'ui-monospace, Menlo, monospace', lineHeight: 1.6 }}>{s.fingerprints.map(f => <li key={f}>{f}</li>)}</ul></details>
      )}
      <h4 style={{ fontSize: 15, fontWeight: 800, color: 'var(--navy)', margin: '18px 0 6px' }}>Setting it up</h4>
      <ol style={{ paddingLeft: 20, lineHeight: 1.6, fontSize: 14.5, color: 'var(--ink)' }}>
        <li><strong>vAuto:</strong> Settings → Exports (or "Third-party feeds") → add a destination. If Motor Row is not in the list, ask vAuto support to add a custom SFTP export to the host, port, username and password above, with photos included. Choose daily or more often.</li>
        <li><strong>HomeNet / Dealer Specialties / DealerSocket / Dealer.com:</strong> same idea — add an export destination for Motor Row with the details above. Any of their standard CSV or XML formats works.</li>
        <li><strong>Other systems:</strong> any SFTP client works: upload any CSV, TSV, XML or JSON file to the folder. We recognize the usual column names automatically.</li>
        <li>After the first delivery, check the <strong>History</strong> tab. Each run shows what was added, updated, marked sold or skipped and why.</li>
      </ol>
      <p style={{ ...fdP, marginTop: 10 }}>Last file received by SFTP: {lastSftp ? `${lastSftp.file_name || 'file'} on ${feedWhen(lastSftp.started_at)} (${fdStatus(lastSftp.status)[0].toLowerCase()})` : 'none yet'}.</p>
    </div>
  );
}

/* ------------------------------ Feed URL ------------------------------ */
function FdUrl({ data, reload, onImported }) {
  const p = data.feed.pull;
  const [url, setUrl] = useStateF(p.url);
  const [user, setUser] = useStateF(p.user);
  const [pass, setPass] = useStateF('');
  const [every, setEvery] = useStateF(String(p.every || 60));
  const [busy, setBusy] = useStateF('');
  const [msg, setMsg] = useStateF(null);
  const [run, setRun] = useStateF(null);
  const pollRef = useRefF(null);
  useEffectF(() => () => clearTimeout(pollRef.current), []);
  const save = async (e) => {
    e.preventDefault(); setBusy('Saving…'); setMsg(null);
    try { await cfsApi('/api/feeds/pull', { method: 'PUT', body: { url: url.trim(), user: user.trim(), pass, every: Number(every) } }); setPass(''); setMsg(['ok', url.trim() ? 'Saved. We will fetch this address on the schedule you chose.' : 'Feed URL removed.']); reload(); }
    catch (e2) { setMsg(['err', e2.message]); }
    setBusy('');
  };
  const poll = (id) => cfsApi('/api/feeds/runs/' + id).then(({ run: r }) => { setRun(r); if (r.status === 'queued' || r.status === 'running') pollRef.current = setTimeout(() => poll(id), 1500); else { onImported && onImported(); reload(); } }).catch(() => {});
  const check = async () => {
    setBusy('Fetching the feed…'); setMsg(null); setRun(null);
    try { const r = await cfsApi('/api/feeds/pull/check', { method: 'POST', body: {} }); if (r.run) { setRun(r.run); poll(r.run.id); } else setMsg(['ok', 'Fetched; the file has not changed since last time.']); }
    catch (e) { setMsg(['err', e.message]); }
    setBusy('');
  };
  return (
    <div>
      <h3 style={fdH}>We fetch your feed from a URL</h3>
      <p style={fdP}>If your inventory tool or website publishes a feed address (https://, ftp:// or sftp://), paste it here. Motor Row downloads it on a schedule and imports anything that changed. Good for dealer websites that expose an inventory export and for tools that can host a file but not push one.</p>
      <form onSubmit={save} style={{ display: 'grid', gap: 12 }}>
        <label className="field"><span className="field-label">Feed address</span><input className="field-input" value={url} onChange={e => setUrl(e.target.value)} placeholder="https://example.com/exports/inventory.csv" inputMode="url" autoComplete="off" /></label>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
          <label className="field"><span className="field-label">Username (if the feed needs one)</span><input className="field-input" value={user} onChange={e => setUser(e.target.value)} autoComplete="off" /></label>
          <label className="field"><span className="field-label">Password</span><input className="field-input" type="password" value={pass} onChange={e => setPass(e.target.value)} placeholder={p.has_pass ? 'saved — type to replace' : ''} autoComplete="new-password" /></label>
        </div>
        <label className="field"><span className="field-label">Check for changes</span>
          <select className="field-select" value={every} onChange={e => setEvery(e.target.value)}>{[['30', 'Every 30 minutes'], ['60', 'Every hour'], ['180', 'Every 3 hours'], ['360', 'Every 6 hours'], ['720', 'Twice a day'], ['1440', 'Once a day']].map(([v, l]) => <option key={v} value={v}>{l}</option>)}</select></label>
        <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center' }}>
          <button type="submit" className="btn btn-green btn-md" disabled={!!busy}>Save</button>
          <button type="button" className="btn btn-ghost btn-md" disabled={!!busy || !p.url} onClick={check}>Fetch now</button>
          {busy && <span role="status" style={{ display: 'flex', gap: 8, alignItems: 'center', color: 'var(--ink-soft)', fontSize: 14 }}><MiniSpin size={16} /> {busy}</span>}
        </div>
      </form>
      {msg && <div style={{ marginTop: 12 }}><FdMsg kind={msg[0]}>{msg[1]}</FdMsg></div>}
      {p.url && <p style={{ ...fdP, marginTop: 12 }}>Last checked: {feedWhen(p.last_at)}{p.last_error ? <span style={{ color: '#B23A12' }}> · last error: {p.last_error}</span> : ''}</p>}
      {run && <div style={{ marginTop: 12 }}><FdRunSummary run={run} /></div>}
    </div>
  );
}

/* ------------------------------ API ------------------------------ */
function FdApi({ data, reload }) {
  const [show, setShow] = useStateF(false);
  const [busy, setBusy] = useStateF(false);
  const key = data.feed.api_key || '';
  const masked = key ? key.slice(0, 8) + '•'.repeat(Math.max(6, key.length - 12)) + key.slice(-4) : '—';
  const rotate = async () => {
    if (!window.confirm('Create a new key? Anything using the current key stops working immediately.')) return;
    setBusy(true); try { await cfsApi('/api/feeds/api-key', { method: 'POST', body: {} }); setShow(true); reload(); } catch (e) { alert(e.message); } setBusy(false);
  };
  const curl = `curl -X POST "${data.push_url}?mode=full" \\\n  -H "Authorization: Bearer ${show ? key : 'YOUR_FEED_KEY'}" \\\n  -H "Content-Type: text/csv" \\\n  --data-binary @inventory.csv`;
  return (
    <div>
      <h3 style={fdH}>Push inventory with the API</h3>
      <p style={fdP}>For middleware, a script on your server, or a tool like Zapier or Make: send your inventory file to the address below with your feed key. Same formats and matching rules as the upload. The response is <code>202 Accepted</code> with a status link; add <code>&amp;dry_run=1</code> to preview without changing anything, or <code>mode=partial</code> to add/update without marking anything sold.</p>
      <div style={fdBox}>
        <FdRow k="Endpoint" v={`POST ${data.push_url}`} copy />
        <FdRow k="Feed key" v={show ? key : masked} copy={show} />
        <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginTop: 12 }}>
          <button type="button" className="btn btn-ghost btn-sm" onClick={() => setShow(s => !s)} aria-pressed={show}>{show ? 'Hide key' : 'Reveal key'}</button>
          <button type="button" className="btn btn-ghost btn-sm" disabled={busy} onClick={rotate}>Create a new key</button>
        </div>
      </div>
      <h4 style={{ fontSize: 15, fontWeight: 800, color: 'var(--navy)', margin: '16px 0 6px' }}>Example</h4>
      <pre style={{ ...fdMono, whiteSpace: 'pre-wrap', margin: 0 }}><code>{curl}</code></pre>
      <p style={{ ...fdP, marginTop: 10 }}>Then poll the <code>status_url</code> from the response with the same header. Full details, including the response fields, are in the <a href="/openapi.json" style={{ color: 'var(--navy)', textDecoration: 'underline', fontWeight: 700 }}>OpenAPI spec</a> and on <a href={data.docs} target="_blank" rel="noopener" style={{ color: 'var(--navy)', textDecoration: 'underline', fontWeight: 700 }}>the connector page</a>.</p>
    </div>
  );
}

/* ------------------------------ Settings ------------------------------ */
function FdSettings({ data, reload }) {
  const f = data.feed;
  const [soldSync, setSoldSync] = useStateF(f.sold_sync);
  const [includeNew, setIncludeNew] = useStateF(f.include_new);
  const [phone, setPhone] = useStateF(f.default_phone || '');
  const [msg, setMsg] = useStateF(null);
  const save = async (e) => {
    e.preventDefault(); setMsg(null);
    try { await cfsApi('/api/feeds/settings', { method: 'PUT', body: { sold_sync: soldSync, include_new: includeNew, default_phone: phone } }); setMsg(['ok', 'Saved.']); reload(); } catch (e2) { setMsg(['err', e2.message]); }
  };
  return (
    <form onSubmit={save} style={{ display: 'grid', gap: 14 }}>
      <h3 style={{ ...fdH, marginBottom: 0 }}>Feed settings</h3>
      <label style={{ display: 'flex', gap: 10, alignItems: 'flex-start', fontSize: 14.5, lineHeight: 1.5, cursor: 'pointer' }}>
        <input type="checkbox" checked={soldSync} onChange={e => setSoldSync(e.target.checked)} style={{ width: 18, height: 18, marginTop: 3, accentColor: 'var(--green)' }} />
        <span><strong>Mark missing cars as sold.</strong> When a complete feed arrives (SFTP, URL, API in full mode, or an upload marked complete), feed-imported cars that are not in it are marked sold. Cars posted by hand are never touched. Turn off if your tool sends partial files.</span>
      </label>
      <label style={{ display: 'flex', gap: 10, alignItems: 'flex-start', fontSize: 14.5, lineHeight: 1.5, cursor: 'pointer' }}>
        <input type="checkbox" checked={includeNew} onChange={e => setIncludeNew(e.target.checked)} style={{ width: 18, height: 18, marginTop: 3, accentColor: 'var(--green)' }} />
        <span><strong>Include new vehicles.</strong> By default rows marked "New" are skipped so only used cars are listed.</span>
      </label>
      <label className="field" style={{ maxWidth: 360 }}><span className="field-label">Phone shown on imported cars</span><input className="field-input" value={phone} onChange={e => setPhone(e.target.value)} inputMode="tel" placeholder="(317) 555-0100" /></label>
      <p style={{ ...fdP, margin: 0 }}>Used when the feed itself has no phone column. Imported cars show your dealership name, <strong>{f.dealer}</strong>, as the seller (change it in the Seller bar).</p>
      <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}><button type="submit" className="btn btn-green btn-md">Save settings</button>{msg && <FdMsg kind={msg[0]}>{msg[1]}</FdMsg>}</div>
    </form>
  );
}

/* ------------------------------ History ------------------------------ */
function FdHistory({ data }) {
  const runs = data.runs || [];
  return (
    <div>
      <h3 style={fdH}>Import history</h3>
      <p style={fdP}>Every import, from any source, with what changed and why rows were skipped. {data.counts.feed} of your active cars came from a feed; {data.counts.manual} were posted by hand.</p>
      {runs.length === 0 ? <p style={{ color: 'var(--ink-soft)' }}>No imports yet.</p> : <div style={{ display: 'grid', gap: 10 }}>{runs.map(r => <FdRunSummary key={r.id} run={r} />)}</div>}
    </div>
  );
}

/* ------------------------------ panel ------------------------------ */
function CfsFeedPanel({ onImported }) {
  const [data, setData] = useStateF(null);
  const [err, setErr] = useStateF('');
  const [tab, setTab] = useStateF('upload');
  const tabsRef = useRefF(null);
  const reload = () => cfsApi('/api/feeds/me').then(setData).catch(e => setErr(e.message));
  useEffectF(() => { reload(); }, []);
  useEffectF(() => { if (!data) return; const live = (data.runs || []).some(r => r.status === 'queued' || r.status === 'running'); if (!live) return; const t = setTimeout(reload, 2000); return () => clearTimeout(t); }, [data]);
  const onKey = (e) => {
    const i = FEED_TABS.findIndex(([k]) => k === tab);
    if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft' && e.key !== 'Home' && e.key !== 'End') return;
    e.preventDefault();
    const n = e.key === 'Home' ? 0 : e.key === 'End' ? FEED_TABS.length - 1 : (i + (e.key === 'ArrowRight' ? 1 : FEED_TABS.length - 1)) % FEED_TABS.length;
    setTab(FEED_TABS[n][0]);
    const btn = tabsRef.current && tabsRef.current.querySelector(`[data-tab="${FEED_TABS[n][0]}"]`); btn && btn.focus();
  };
  if (err) return <FdMsg kind="err">{err}</FdMsg>;
  if (!data) return <div style={{ display: 'grid', placeItems: 'center', padding: 30 }}><MiniSpin size={26} /></div>;
  const s = data.feed.sftp;
  const connected = [s.password_set && 'SFTP', data.feed.pull.url && 'feed URL'].filter(Boolean);
  return (
    <div>
      <p style={{ ...fdP, marginBottom: 14 }}>Connect <strong>{data.feed.dealer}</strong>'s inventory system so cars post, update and sell off automatically. {connected.length ? `Connected: ${connected.join(' and ')}.` : 'Nothing connected yet: start with a one-time upload, or set up the automatic feed.'} <a href={data.docs} target="_blank" rel="noopener" style={{ color: 'var(--navy)', textDecoration: 'underline', fontWeight: 700 }}>How it works</a></p>
      <div ref={tabsRef} role="tablist" aria-label="Inventory feed sections" onKeyDown={onKey} style={{ display: 'flex', gap: 6, flexWrap: 'wrap', borderBottom: '2px solid var(--line-soft)', marginBottom: 18 }}>
        {FEED_TABS.map(([k, label]) => (
          <button key={k} type="button" role="tab" data-tab={k} id={`fd-tab-${k}`} aria-selected={tab === k} aria-controls={`fd-panel-${k}`} tabIndex={tab === k ? 0 : -1} onClick={() => setTab(k)}
            style={{ padding: '10px 14px', fontWeight: 800, fontSize: 14, color: tab === k ? 'var(--navy)' : 'var(--ink-soft)', borderBottom: `3px solid ${tab === k ? 'var(--green)' : 'transparent'}`, marginBottom: -2, borderRadius: '6px 6px 0 0', background: 'transparent' }}>
            {label}
          </button>
        ))}
      </div>
      <div role="tabpanel" id={`fd-panel-${tab}`} aria-labelledby={`fd-tab-${tab}`}>
        {tab === 'upload' && <FdUpload data={data} onImported={onImported} onRun={reload} />}
        {tab === 'sftp' && <FdSftp data={data} reload={reload} />}
        {tab === 'url' && <FdUrl data={data} reload={reload} onImported={onImported} />}
        {tab === 'api' && <FdApi data={data} reload={reload} />}
        {tab === 'settings' && <FdSettings data={data} reload={reload} />}
        {tab === 'history' && <FdHistory data={data} />}
      </div>
    </div>
  );
}

Object.assign(window, { CfsFeedPanel });
