/* WP Site Beam — Redirect Manager
   Mirrors the WordPress plugin v1.0 (Rules / 404 Monitor / Import / File Map)
   and adds SaaS cross-site rollup + Scanner handoff.

   Handles: pages, posts, media, documents, renamed files. Preserves
   site URLs and redirections automatically during migrations.
*/

function Redirects() {
  const { useState, useMemo } = React;
  const [activeTab, setActiveTab] = useState('rules');
  const [site, setSite] = useState('acme.co');
  const [populated, setPopulated] = useState(true);
  const [addOpen, setAddOpen] = useState(false);

  // Mock sites (for the per-site filter)
  const sites = ['acme.co', 'northwind.io', 'contoso.org'];

  // Mock redirect rules — exercises pages, posts, media, documents, renames
  const seed = populated ? [
    { id:'R-001', source:'/old-pricing',                target:'/pricing',                     code:301, kind:'page',     hits: 1284, created:'2026-04-02', source_of:'manual' },
    { id:'R-002', source:'/blog/2024/ai-tools',         target:'/blog/ai-tools',                code:301, kind:'post',     hits:  462, created:'2026-04-05', source_of:'yoast-import' },
    { id:'R-003', source:'/wp-content/uploads/2023/brochure-v1.pdf', target:'/wp-content/uploads/2026/brochure.pdf', code:301, kind:'document', hits:  89, created:'2026-04-08', source_of:'file-map' },
    { id:'R-004', source:'/about-us',                   target:'/about',                        code:301, kind:'page',     hits:  218, created:'2026-04-10', source_of:'scanner' },
    { id:'R-005', source:'/team/jane-doe',              target:'/team/jane-smith',              code:301, kind:'post',     hits:   31, created:'2026-04-11', source_of:'manual', note:'name change' },
    { id:'R-006', source:'/media/old-logo.png',         target:'/media/logo-2026.png',          code:301, kind:'media',    hits:  147, created:'2026-04-12', source_of:'file-map' },
    { id:'R-007', source:'/docs/terms-v2.docx',         target:'/docs/terms.pdf',               code:301, kind:'document', hits:   12, created:'2026-04-14', source_of:'file-map' },
    { id:'R-008', source:'/shop/products/widget',       target:'/products/widget',              code:301, kind:'product',  hits:  903, created:'2026-04-15', source_of:'redirection-import' },
    { id:'R-009', source:'/careers/*',                  target:'/jobs/$1',                      code:301, kind:'page',     hits:   74, created:'2026-04-16', source_of:'regex', regex:true },
    { id:'R-010', source:'/go/newsletter',              target:'https://mail.acme.co/subscribe',code:302, kind:'external', hits: 2041, created:'2026-04-16', source_of:'manual' },
    { id:'R-011', source:'/old-contact-form',           target:'/contact',                      code:301, kind:'page',     hits:   58, created:'2026-04-17', source_of:'manual', disabled:true },
    { id:'R-012', source:'/blog/ai-in-2025',            target:'/blog/ai-in-2026',              code:301, kind:'post',     hits:  327, created:'2026-04-17', source_of:'scanner' },
    { id:'R-013', source:'/products.html',              target:'/products',                     code:301, kind:'page',     hits:  612, created:'2026-04-18', source_of:'htaccess-import' },
    { id:'R-014', source:'/wp-content/uploads/2022/report.pdf', target:'/resources/annual-report-2026.pdf', code:301, kind:'document', hits:  44, created:'2026-04-18', source_of:'file-map' },
    { id:'R-015', source:'/partner-portal',             target:'/partners',                     code:301, kind:'page',     hits:  156, created:'2026-04-18', source_of:'scanner' },
  ] : [];

  // Mock 404s — visitors hitting missing pages
  const fouroFours = populated ? [
    { url:'/env',                  hits: 1,   seen:'2026-04-18 23:28:08', referrer:'—',                                ip:'203.0.113.42', ua:'bot/scanner', suggested:null },
    { url:'/old-pricing-2023',     hits: 14,  seen:'2026-04-18 21:11:42', referrer:'https://google.com/search?q=acme',   ip:'198.51.100.3', ua:'Chrome 120',  suggested:'/pricing' },
    { url:'/wp-admin',             hits: 208, seen:'2026-04-18 19:02:15', referrer:'direct',                             ip:'203.0.113.7',  ua:'bot/various', suggested:null, bot:true },
    { url:'/blog/2024',            hits: 9,   seen:'2026-04-18 18:44:03', referrer:'internal',                           ip:'198.51.100.91', ua:'Safari 17',  suggested:'/blog' },
    { url:'/download/whitepaper',  hits: 23,  seen:'2026-04-18 16:30:11', referrer:'twitter.com',                        ip:'198.51.100.44', ua:'iPhone',     suggested:'/resources/whitepaper.pdf' },
    { url:'/team/old-employee',    hits: 3,   seen:'2026-04-17 09:15:52', referrer:'linkedin.com',                       ip:'198.51.100.12', ua:'Chrome',     suggested:null },
  ] : [ { url:'/env', hits: 1, seen:'2026-04-18 23:28:08', referrer:'—', ip:'203.0.113.42', ua:'bot/scanner', suggested:null } ];

  // Mock file mappings (migration — old file URL → new file URL, pre-redirect)
  const fileMappings = populated ? [
    { id:'F-001', oldUrl:'/wp-content/uploads/2023/brochure-v1.pdf', newUrl:'/wp-content/uploads/2026/brochure.pdf', kind:'pdf',  size:'2.4 MB', status:'mapped',  redirect:true },
    { id:'F-002', oldUrl:'/media/old-logo.png',                      newUrl:'/media/logo-2026.png',                   kind:'image', size:'48 KB',  status:'mapped',  redirect:true },
    { id:'F-003', oldUrl:'/docs/terms-v2.docx',                      newUrl:'/docs/terms.pdf',                        kind:'doc',   size:'120 KB', status:'converted', redirect:true, note:'docx → pdf' },
    { id:'F-004', oldUrl:'/wp-content/uploads/2022/report.pdf',      newUrl:'/resources/annual-report-2026.pdf',      kind:'pdf',  size:'5.1 MB', status:'mapped',  redirect:true },
    { id:'F-005', oldUrl:'/media/hero-video.mov',                    newUrl:'/media/hero.mp4',                        kind:'video', size:'24 MB',  status:'converted', redirect:false, note:'needs redirect' },
    { id:'F-006', oldUrl:'/downloads/pricing-2024.xlsx',             newUrl:'',                                       kind:'spreadsheet', size:'84 KB', status:'unmapped', redirect:false },
  ] : [];

  const stats = {
    active:   seed.filter(r => !r.disabled).length,
    total:    seed.length,
    fourohs:  fouroFours.length,
    mapped:   fileMappings.filter(f => f.status !== 'unmapped').length,
    mappings: fileMappings.length,
  };

  const activeSeed = seed.filter(r => !r.disabled);

  // Build CSV payload once
  const payloads = useMemo(() => ({
    csvRules: ['source,target,type', ...seed.map(r => `${r.source},${r.target},${r.code}`)].join('\n'),
    csvMappings: ['old_url,new_url,kind,status', ...fileMappings.map(f => `${f.oldUrl},${f.newUrl},${f.kind},${f.status}`)].join('\n'),
    htaccess: '# Generated by WP Site Beam Redirect Manager\n' + seed.map(r => r.regex
        ? `RewriteRule ^${r.source.replace(/^\//,'').replace('/*','(.*)')}$ ${r.target} [R=${r.code},L]`
        : `Redirect ${r.code} ${r.source} ${r.target}`).join('\n'),
    nginx:    '# Generated by WP Site Beam Redirect Manager\n' + seed.map(r => `location = ${r.source} { return ${r.code} ${r.target}; }`).join('\n'),
  }), [populated]);

  const tabs = [
    { id:'rules',    label:'Rules',       count: stats.active, icon:'redirect' },
    { id:'monitor',  label:'404 Monitor', count: stats.fourohs, icon:'warn' },
    { id:'import',   label:'Import',      count: null, icon:'download' },
    { id:'filemap',  label:'File Map',    count: stats.mappings, icon:'archive' },
  ];

  return (
    <div>
      <PageHead crumb="Operations"
        title={<span style={{ display:'inline-flex', alignItems:'center', gap:10 }}>
          Redirect Manager
          <span style={{ fontFamily:'var(--font-mono)', fontSize:'.58rem', padding:'3px 8px', borderRadius:4, background:'var(--gdim)', border:'1px solid var(--green-dim)', color:'var(--green)', letterSpacing:'.08em' }}>v1.0</span>
        </span>}
        sub="301/302 redirects, 404 monitor, file map & migration URL mapping. Import from Yoast, Redirection, SmartCrawl, Rank Math. Pages · posts · media · documents — all tracked."
        actions={<>
          {/* Site picker */}
          <div style={{ display:'inline-flex', alignItems:'center', gap:6, padding:'4px 10px', background:'var(--surface-2)', border:'1px solid var(--border)', borderRadius:6 }}>
            <Icon name="sites" size={12}/>
            <select value={site} onChange={e => setSite(e.target.value)}
                    style={{ background:'transparent', border:'none', color:'var(--text)', fontSize:'.78rem', fontFamily:'var(--font-mono)', outline:'none', cursor:'pointer' }}>
              {sites.map(x => <option key={x} value={x} style={{ background:'var(--surface)', color:'var(--text)' }}>{x}</option>)}
            </select>
          </div>
          <button className="btn btn-primary btn-sm" onClick={() => setAddOpen(true)}>
            <Icon name="plus" size={13}/>Add Redirect
          </button>
          <button className="btn btn-ghost btn-sm" onClick={() => setActiveTab('import')}>
            <Icon name="download" size={13} style={{ transform:'rotate(180deg)' }}/>Import
          </button>
          <button className="btn btn-ghost btn-sm"
                  onClick={() => window.wpsbDownload(`${site}-redirects.csv`, payloads.csvRules, 'text/csv')}>
            <Icon name="download" size={13}/>Export CSV
          </button>
          <button className="btn btn-ghost btn-sm"
                  onClick={() => window.wpsbDownload('.htaccess', payloads.htaccess, 'text/plain')}>
            .htaccess
          </button>
          {/* 2026-05-19 T2.1 integration: Apply rules directly to the WP site
              via plugin write-back. Builds payload from current rule list,
              uses QueueOperationButton's status display. Site_url derived
              from `site` state — must match the WP site where plugin is
              installed (uses https:// prefix; plugin handler normalizes). */}
          {window.QueueOperationButton && seed.length > 0 && (
            React.createElement(window.QueueOperationButton, {
              operationType: 'redirects_import',
              payload: {
                rules: seed.filter(r => !r.disabled).map(r => ({
                  from: r.source,
                  to:   r.target,
                  code: r.code || 301,
                  match: r.regex ? 'regex' : 'exact',
                })),
                engine: 'auto',
                overwrite_existing: false,
              },
              siteUrl: site.startsWith('http') ? site : `https://${site}`,
              label: `Apply ${seed.filter(r => !r.disabled).length} to WP`,
            })
          )}
        </>}
      />

      {/* Stat strip */}
      <div className="grid grid-4" style={{ marginBottom: 16 }}>
        <div className="stat">
          <div className="stat-lbl">Active Rules</div>
          <div className="stat-val ok">{stats.active}</div>
          <div style={{ fontSize:'.66rem', color:'var(--dim)', fontFamily:'var(--font-mono)', marginTop:4 }}>
            {seed.filter(r=>r.disabled).length} disabled · {seed.filter(r=>r.regex).length} regex
          </div>
        </div>
        <div className="stat">
          <div className="stat-lbl">Unresolved 404s</div>
          <div className={`stat-val ${stats.fourohs ? 'warn' : 'ok'}`}>{stats.fourohs}</div>
          <div style={{ fontSize:'.66rem', color:'var(--dim)', fontFamily:'var(--font-mono)', marginTop:4 }}>
            last 30d · {fouroFours.filter(f=>f.suggested).length} auto-suggested
          </div>
        </div>
        <div className="stat">
          <div className="stat-lbl">File Mappings</div>
          <div className="stat-val">{stats.mapped} / {stats.mappings}</div>
          <div style={{ fontSize:'.66rem', color:'var(--dim)', fontFamily:'var(--font-mono)', marginTop:4 }}>
            pages · posts · media · documents
          </div>
        </div>
        <div className="stat">
          <div className="stat-lbl">Hits (30d)</div>
          <div className="stat-val">{seed.reduce((s,r)=>s+r.hits, 0).toLocaleString()}</div>
          <div style={{ fontSize:'.66rem', color:'var(--dim)', fontFamily:'var(--font-mono)', marginTop:4 }}>
            across all rules
          </div>
        </div>
      </div>

      {/* Tabs */}
      <div className="sub-tabs" role="tablist" aria-label="Redirect manager sections">
        {tabs.map(t => (
          <button key={t.id} role="tab" aria-selected={activeTab === t.id}
                  className={'sub-tab' + (activeTab === t.id ? ' active' : '')}
                  onClick={() => setActiveTab(t.id)}>
            <Icon name={t.icon} size={12}/>
            <span style={{ marginLeft:6 }}>{t.label}</span>
            {t.count != null && (
              <span style={{ marginLeft:8, padding:'1px 7px', borderRadius:10, background:'var(--surface-3)', fontSize:'.66rem', fontFamily:'var(--font-mono)', color:'var(--dim)' }}>
                {t.count}
              </span>
            )}
          </button>
        ))}
      </div>

      <div style={{ marginTop: 16 }}>
        {activeTab === 'rules'   && <RulesTab rules={seed} site={site} populated={populated} setPopulated={setPopulated} onAdd={() => setAddOpen(true)}/>}
        {activeTab === 'monitor' && <MonitorTab items={fouroFours} site={site}/>}
        {activeTab === 'import'  && <ImportTab site={site} payloads={payloads}/>}
        {activeTab === 'filemap' && <FileMapTab items={fileMappings} site={site} populated={populated}/>}
      </div>

      {addOpen && <AddRedirectModal onClose={() => setAddOpen(false)} site={site}/>}
    </div>
  );
}

/* ── Rules tab ─────────────────────────────────────────────── */
function RulesTab({ rules, site, populated, setPopulated, onAdd }) {
  const { useState } = React;
  const [q, setQ] = useState('');
  const [kindFilter, setKindFilter] = useState('all');
  const [sel, setSel] = useState(new Set());

  const kinds = ['all', 'page', 'post', 'media', 'document', 'product', 'external'];
  const filtered = rules.filter(r => {
    const matchQ = !q || r.source.toLowerCase().includes(q.toLowerCase()) || r.target.toLowerCase().includes(q.toLowerCase());
    const matchK = kindFilter === 'all' || r.kind === kindFilter;
    return matchQ && matchK;
  });

  const toggleSel = id => {
    const next = new Set(sel);
    if (next.has(id)) next.delete(id); else next.add(id);
    setSel(next);
  };
  const toggleAll = () => {
    if (sel.size === filtered.length) setSel(new Set());
    else setSel(new Set(filtered.map(r => r.id)));
  };

  const kindTone = k => ({ page:'beam', post:'', media:'warn', document:'ok', product:'beam', external:'' })[k] || '';
  const srcTagLabel = s => ({
    manual:'MANUAL', 'yoast-import':'YOAST', 'redirection-import':'REDIRECTION', 'htaccess-import':'.HTACCESS',
    'file-map':'FILE MAP', scanner:'SCANNER', regex:'REGEX'
  })[s] || s.toUpperCase();

  return (
    <div className="card">
      <div className="card-head">
        <h2 className="card-title">Rules · {site}</h2>
        <div style={{ display:'flex', gap:6, alignItems:'center' }}>
          <span className="tag">{filtered.length} of {rules.length}</span>
          <label className="opt-check" style={{ fontSize:'.72rem' }}>
            <input type="checkbox" checked={populated} onChange={e => setPopulated(e.target.checked)}/>
            <span>Populated demo</span>
          </label>
        </div>
      </div>
      <div className="card-body">
        {/* Toolbar */}
        <div style={{ display:'flex', gap:8, flexWrap:'wrap', alignItems:'center', marginBottom:12 }}>
          <div style={{ flex:'1 1 260px', display:'flex', alignItems:'center', gap:6, padding:'6px 10px', background:'var(--surface-3)', border:'1px solid var(--border)', borderRadius:6 }}>
            <Icon name="search" size={12}/>
            <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search rules…"
                   style={{ flex:1, background:'transparent', border:'none', color:'var(--text)', fontSize:'.82rem', outline:'none', fontFamily:'var(--font-mono)' }}/>
          </div>
          <div style={{ display:'flex', gap:4, flexWrap:'wrap' }} role="group" aria-label="Kind filter">
            {kinds.map(k => (
              <button key={k}
                      className={'btn btn-ghost btn-sm' + (kindFilter===k?' active':'')}
                      aria-pressed={kindFilter===k}
                      style={kindFilter===k ? { background:'var(--beam-dim)', color:'var(--beam)', borderColor:'var(--beam-dim)' } : null}
                      onClick={() => setKindFilter(k)}>
                {k === 'all' ? 'All' : k.charAt(0).toUpperCase()+k.slice(1)}
              </button>
            ))}
          </div>
          {sel.size > 0 && (
            <div style={{ display:'flex', gap:6, alignItems:'center', padding:'4px 10px', background:'var(--beam-dim)', border:'1px solid var(--beam-dim)', borderRadius:6 }}>
              <span style={{ fontSize:'.72rem', color:'var(--beam)', fontFamily:'var(--font-mono)' }}>{sel.size} selected</span>
              <button className="btn btn-ghost btn-sm" onClick={() => { window.wpsbToast(`${sel.size} rules disabled`, 'ok'); setSel(new Set()); }}>
                Disable
              </button>
              <button className="btn btn-ghost btn-sm" onClick={() => { window.wpsbToast(`${sel.size} rules deleted`, 'ok'); setSel(new Set()); }}>
                <Icon name="trash" size={11}/>Delete
              </button>
            </div>
          )}
          <button className="btn btn-primary btn-sm" onClick={onAdd}>
            <Icon name="plus" size={12}/>Add Rule
          </button>
        </div>

        {rules.length === 0 ? (
          <EmptyState
            icon="redirect"
            title="No redirect rules yet."
            hint="Add rules manually, import from a file, or generate them from a Site Scanner migration."
            primary={{ label:'Add First Rule', onClick:onAdd }}
            secondary={{ label:'Import', onClick:() => window.wpsbToast('Switch to Import tab', 'info') }}
          />
        ) : (
          <div style={{ overflowX:'auto' }}>
            <table className="table" style={{ minWidth:780 }}>
              <thead>
                <tr>
                  <th style={{ width:28 }}>
                    <input type="checkbox"
                           checked={sel.size>0 && sel.size===filtered.length}
                           onChange={toggleAll}/>
                  </th>
                  <th>Source</th>
                  <th style={{ width:28 }}></th>
                  <th>Target</th>
                  <th style={{ width:70 }}>Code</th>
                  <th style={{ width:90 }}>Kind</th>
                  <th style={{ width:90 }}>Source</th>
                  <th style={{ width:80, textAlign:'right' }}>Hits</th>
                  <th style={{ width:80 }}></th>
                </tr>
              </thead>
              <tbody>
                {filtered.map(r => (
                  <tr key={r.id} style={{ opacity: r.disabled ? 0.45 : 1 }}>
                    <td>
                      <input type="checkbox"
                             checked={sel.has(r.id)}
                             onChange={() => toggleSel(r.id)}/>
                    </td>
                    <td className="mono" style={{ fontSize:'.74rem', color: r.regex ? 'var(--purple, #9b6dff)' : 'var(--text)' }}>
                      {r.source}
                      {r.note && <div style={{ fontSize:'.64rem', color:'var(--dim)', fontFamily:'var(--font-mono)' }}>{r.note}</div>}
                    </td>
                    <td style={{ color:'var(--dim)', textAlign:'center' }}>→</td>
                    <td className="mono" style={{ fontSize:'.74rem', color:'var(--beam)' }}>{r.target}</td>
                    <td>
                      <span className={`tag ${r.code===301?'ok':'warn'}`}>{r.code}</span>
                    </td>
                    <td><span className={`tag ${kindTone(r.kind)}`}>{r.kind.toUpperCase()}</span></td>
                    <td>
                      <span style={{ display:'inline-block', fontFamily:'var(--font-mono)', fontSize:'.58rem', padding:'2px 6px', borderRadius:3, background:'var(--surface-3)', border:'1px solid var(--border)', color:'var(--dim)', letterSpacing:'.05em', whiteSpace:'nowrap' }}>
                        {srcTagLabel(r.regex ? 'regex' : r.source_of)}
                      </span>
                    </td>
                    <td className="mono" style={{ textAlign:'right', fontSize:'.76rem', color: r.hits > 500 ? 'var(--green)' : 'var(--text-2)' }}>
                      {r.hits.toLocaleString()}
                    </td>
                    <td style={{ textAlign:'right' }}>
                      <button className="btn btn-ghost btn-sm" onClick={() => window.wpsbToast(`Edit ${r.id}`, 'info')} aria-label={`Edit ${r.id}`}>
                        <Icon name="edit" size={11}/>
                      </button>
                      <button className="btn btn-ghost btn-sm" onClick={() => window.wpsbToast(`${r.id} deleted`, 'ok')} aria-label={`Delete ${r.id}`}>
                        <Icon name="trash" size={11}/>
                      </button>
                    </td>
                  </tr>
                ))}
                {filtered.length === 0 && (
                  <tr><td colSpan={9} style={{ textAlign:'center', color:'var(--dim)', padding:'24px 0' }}>
                    No rules match your filter.
                  </td></tr>
                )}
              </tbody>
            </table>
          </div>
        )}
      </div>
    </div>
  );
}

/* ── 404 Monitor tab ───────────────────────────────────────── */
function MonitorTab({ items, site }) {
  return (
    <div className="card">
      <div className="card-head">
        <h2 className="card-title">404 Monitor · {site}</h2>
        <div style={{ display:'flex', gap:6 }}>
          <button className="btn btn-ghost btn-sm" onClick={() => window.wpsbToast('Auto-fix all — would create 4 suggested redirects', 'ok')}>
            <Icon name="spark" size={12}/>Auto-fix all ({items.filter(i=>i.suggested).length})
          </button>
          <button className="btn btn-ghost btn-sm" onClick={() => window.wpsbToast('404 log cleared', 'ok')}>
            <Icon name="trash" size={12}/>Clear all
          </button>
        </div>
      </div>
      <div className="card-body">
        <div style={{ fontSize:'.74rem', color:'var(--dim)', marginBottom:12, display:'flex', alignItems:'center', gap:6 }}>
          <Icon name="info" size={12}/>
          404s are captured automatically when visitors hit missing pages. Click <strong style={{ color:'var(--warn)' }}>Create Redirect</strong> to fix any row.
        </div>
        {items.length === 0 ? (
          <EmptyState
            icon="warn"
            title="No 404s detected."
            hint="When visitors hit a missing page, it'll appear here for you to redirect."
          />
        ) : (
          <table className="table">
            <thead>
              <tr>
                <th>URL</th>
                <th style={{ textAlign:'right', width:70 }}>Hits</th>
                <th style={{ width:160 }}>Last Seen</th>
                <th>Referrer</th>
                <th>Suggested fix</th>
                <th style={{ width:170, textAlign:'right' }}>Action</th>
              </tr>
            </thead>
            <tbody>
              {items.map(i => (
                <tr key={i.url}>
                  <td className="mono" style={{ fontSize:'.76rem', color:'var(--rose)' }}>
                    {i.url}
                    {i.bot && <span style={{ marginLeft:6, fontFamily:'var(--font-mono)', fontSize:'.56rem', padding:'1px 5px', borderRadius:3, background:'var(--surface-3)', border:'1px solid var(--border)', color:'var(--dim)' }}>BOT</span>}
                  </td>
                  <td className="mono" style={{ textAlign:'right', fontSize:'.76rem', color: i.hits > 100 ? 'var(--warn)' : 'var(--text-2)' }}>
                    {i.hits.toLocaleString()}
                  </td>
                  <td className="mono" style={{ fontSize:'.7rem', color:'var(--dim)' }}>{i.seen}</td>
                  <td className="mono" style={{ fontSize:'.72rem', color:'var(--dim)', maxWidth:220, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }} title={i.referrer}>
                    {i.referrer}
                  </td>
                  <td>
                    {i.suggested ? (
                      <span style={{ display:'inline-flex', alignItems:'center', gap:6, fontSize:'.74rem' }}>
                        <Icon name="spark" size={11}/>
                        <span className="mono" style={{ color:'var(--beam)' }}>{i.suggested}</span>
                      </span>
                    ) : <span style={{ color:'var(--dimmer, var(--dim))', fontSize:'.72rem', fontStyle:'italic' }}>no match</span>}
                  </td>
                  <td style={{ textAlign:'right' }}>
                    <button className="btn btn-ghost btn-sm" onClick={() => window.wpsbToast(`New URL input for ${i.url}`, 'info')}>
                      New URL
                    </button>
                    <button className="btn btn-primary btn-sm" style={{ marginLeft:4, background:'var(--warn)', borderColor:'var(--warn)', color:'#1a1200' }}
                            onClick={() => window.wpsbToast(i.suggested ? `Created redirect → ${i.suggested}` : `Create redirect for ${i.url}`, 'ok')}>
                      Fix
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>
    </div>
  );
}

/* ── Import tab ─────────────────────────────────────────────── */
function ImportTab({ site, payloads }) {
  const { useState } = React;
  const [csv, setCsv] = useState('/old-page,/new-page,301\n/old-file.pdf,/new-file.pdf,301');
  const [source, setSource] = useState('csv');

  const sources = [
    { id:'csv',         label:'CSV',         desc:'source,target,type — Redirection plugin compatible', tag:'UNIVERSAL' },
    { id:'yoast',       label:'Yoast SEO',   desc:'Import /wp-content redirects from Yoast Premium',     tag:'PLUGIN' },
    { id:'redirection', label:'Redirection', desc:'Export/import from the Redirection plugin',           tag:'PLUGIN' },
    { id:'smartcrawl',  label:'SmartCrawl',  desc:'Import from WPMU DEV SmartCrawl Pro',                 tag:'PLUGIN' },
    { id:'rankmath',    label:'Rank Math',   desc:'Import from Rank Math SEO redirect module',           tag:'PLUGIN' },
    { id:'htaccess',    label:'.htaccess',   desc:'Paste an Apache .htaccess file',                      tag:'SERVER' },
    { id:'wxr',         label:'WXR export',  desc:'Import from WordPress WXR (paired with Scanner)',     tag:'MIGRATION' },
  ];

  return (
    <div className="grid" style={{ gridTemplateColumns:'minmax(0,1fr) 320px', alignItems:'start', gap:16 }}>
      {/* Left — picker + input */}
      <div className="card">
        <div className="card-head">
          <h2 className="card-title" style={{ display:'flex', alignItems:'center', gap:6 }}>
            <Icon name="download" size={14} style={{ transform:'rotate(180deg)' }}/>
            Import sources
          </h2>
        </div>
        <div className="card-body">
          <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(220px, 1fr))', gap:8, marginBottom:16 }}>
            {sources.map(s => (
              <label key={s.id}
                     style={{ display:'flex', gap:10, alignItems:'flex-start', padding:'10px 12px', borderRadius:6,
                              background: source === s.id ? 'var(--beam-dim)' : 'var(--surface-3)',
                              border:'1px solid', borderColor: source === s.id ? 'var(--beam-dim)' : 'var(--border)',
                              cursor:'pointer' }}>
                <input type="radio" checked={source === s.id} onChange={() => setSource(s.id)}
                       style={{ marginTop:3, accentColor:'var(--beam)' }}/>
                <div style={{ flex:1, minWidth:0 }}>
                  <div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:2 }}>
                    <strong style={{ fontSize:'.8rem' }}>{s.label}</strong>
                    <span style={{ fontFamily:'var(--font-mono)', fontSize:'.54rem', padding:'1px 5px', borderRadius:3, background:'var(--surface-2)', color:'var(--dim)', border:'1px solid var(--border)' }}>{s.tag}</span>
                  </div>
                  <div style={{ fontSize:'.7rem', color:'var(--dim)', lineHeight:1.4 }}>{s.desc}</div>
                </div>
              </label>
            ))}
          </div>

          {/* CSV input */}
          <div>
            <div style={{ fontFamily:'var(--font-mono)', fontSize:'.66rem', color:'var(--warn)', letterSpacing:'.1em', marginBottom:6 }}>
              <Icon name="docs" size={11}/> {source === 'htaccess' ? 'PASTE .HTACCESS' : source === 'csv' ? 'IMPORT FROM CSV' : `IMPORT FROM ${source.toUpperCase()}`}
            </div>
            {(source === 'csv' || source === 'htaccess') ? (
              <>
                {source === 'csv' && (
                  <div style={{ fontSize:'.72rem', color:'var(--dim)', marginBottom:6 }}>
                    CSV format: <span className="mono" style={{ color:'var(--text-2)' }}>source,target,type</span> — compatible with Redirection plugin export.
                  </div>
                )}
                <textarea value={csv} onChange={e => setCsv(e.target.value)} rows={10}
                          style={{ width:'100%', background:'var(--surface-3)', border:'1px solid var(--border)', borderRadius:6, color:'var(--text)', fontFamily:'var(--font-mono)', fontSize:'.76rem', padding:'10px 12px', resize:'vertical' }}/>
                <button className="btn btn-primary btn-sm" style={{ marginTop:10 }}
                        onClick={() => {
                          const n = csv.split('\n').filter(l => l.trim()).length;
                          window.wpsbToast(`Imported ${n} rules from ${source === 'csv' ? 'CSV' : '.htaccess'}`, 'ok');
                        }}>
                  <Icon name="download" size={12} style={{ transform:'rotate(180deg)' }}/>
                  Import {source === 'csv' ? 'CSV' : '.htaccess'}
                </button>
              </>
            ) : (
              <div style={{ padding:'16px 14px', background:'var(--surface-3)', border:'1px dashed var(--border)', borderRadius:6, textAlign:'center' }}>
                <div style={{ fontSize:'.82rem', marginBottom:4 }}>Connect to <strong>{sources.find(x=>x.id===source).label}</strong></div>
                <div style={{ fontSize:'.7rem', color:'var(--dim)', marginBottom:12 }}>
                  We'll read redirects from the plugin's tables via the WPSB WordPress plugin connection.
                </div>
                <button className="btn btn-primary btn-sm"
                        onClick={() => window.wpsbToast(`Importing from ${sources.find(x=>x.id===source).label}… 42 rules found`, 'ok')}>
                  <Icon name="spark" size={12}/>Connect & Import
                </button>
              </div>
            )}
          </div>
        </div>
      </div>

      {/* Right — Export sidebar */}
      <div className="card">
        <div className="card-head">
          <h2 className="card-title" style={{ display:'flex', alignItems:'center', gap:6 }}>
            <Icon name="download" size={14}/>Export
          </h2>
        </div>
        <div className="card-body" style={{ display:'flex', flexDirection:'column', gap:8 }}>
          <button className="btn btn-ghost" style={{ justifyContent:'flex-start' }}
                  onClick={() => window.wpsbDownload(`${site}-redirects.csv`, payloads.csvRules, 'text/csv')}>
            <Icon name="download" size={13}/>CSV (Redirection compatible)
          </button>
          <button className="btn btn-ghost" style={{ justifyContent:'flex-start' }}
                  onClick={() => window.wpsbDownload('.htaccess', payloads.htaccess, 'text/plain')}>
            <Icon name="download" size={13}/>.htaccess
          </button>
          <button className="btn btn-ghost" style={{ justifyContent:'flex-start' }}
                  onClick={() => window.wpsbDownload('nginx.conf', payloads.nginx, 'text/plain')}>
            <Icon name="download" size={13}/>Nginx Config
          </button>
          <button className="btn btn-ghost" style={{ justifyContent:'flex-start' }}
                  onClick={() => window.wpsbDownload(`${site}-redirects.json`, JSON.stringify({ site, generated: new Date().toISOString() }, null, 2), 'application/json')}>
            <Icon name="download" size={13}/>JSON (WPSB)
          </button>

          <div style={{ marginTop:8, paddingTop:10, borderTop:'1px solid var(--border)', fontSize:'.68rem', color:'var(--dim)', lineHeight:1.5 }}>
            Exports include only enabled rules. Use CSV to round-trip between sites. Use .htaccess / Nginx when deploying to a new server before the WPSB plugin is installed.
          </div>
        </div>
      </div>
    </div>
  );
}

/* ── File Map tab ───────────────────────────────────────────── */
/* Three-panel view:
   ┌──────────┬───────────────────────────┬─────────────┐
   │ Rules    │ File list (bulk editable) │ Split view  │
   │ Panel    │ - checkbox + kind icon    │ (details    │
   │ (toggle) │ - current → proposed      │  drawer)    │
   │          │ - ADA flags               │             │
   └──────────┴───────────────────────────┴─────────────┘
   Feeds into 301 creation on "Create All Redirects" or per-row. */
function FileMapTab({ items: seedItems, site, populated }) {
  const { useState, useMemo, useEffect } = React;

  // Expanded seed with real filenames worth renaming (demo data).
  // Falls back to parent's mappings if the extra seed is disabled.
  const EXTENDED_SEED = populated ? [
    { id:'FM01', oldUrl:'/wp-content/uploads/2023/Commission Minutes 1-15-25.PDF', newUrl:'', kind:'pdf', size:'342 KB', status:'unmapped', redirect:false, page:'/commissioners/minutes', flags:['case','spaces','date'] },
    { id:'FM02', oldUrl:'/wp-content/uploads/2023/Feb 12 2023 Meeting Minutes.pdf', newUrl:'', kind:'pdf', size:'287 KB', status:'unmapped', redirect:false, page:'/commissioners/minutes', flags:['spaces','date','no-ocr'] },
    { id:'FM03', oldUrl:'/wp-content/uploads/2024/annual-report-FINAL-v2.pdf', newUrl:'', kind:'pdf', size:'4.8 MB', status:'unmapped', redirect:false, page:'/about/reports', flags:['revision'] },
    { id:'FM04', oldUrl:'/wp-content/uploads/2022/IMG_4821.jpg', newUrl:'', kind:'image', size:'3.2 MB', status:'unmapped', redirect:false, page:null, flags:['generic','no-alt','oversized'] },
    { id:'FM05', oldUrl:'/wp-content/uploads/2023/Résumé_(final).pdf', newUrl:'', kind:'pdf', size:'86 KB', status:'unmapped', redirect:false, page:'/team', flags:['diacritics','revision','parens'] },
    { id:'FM06', oldUrl:'/wp-content/uploads/2023/brochure-v1.pdf', newUrl:'/wp-content/uploads/2026/brochure.pdf', kind:'pdf', size:'2.4 MB', status:'mapped', redirect:true, page:'/about', flags:[] },
    { id:'FM07', oldUrl:'/media/old-logo.png', newUrl:'/media/logo-2026.png', kind:'image', size:'48 KB', status:'mapped', redirect:true, page:'/', flags:[] },
    { id:'FM08', oldUrl:'/docs/terms-v2.docx', newUrl:'/docs/terms.pdf', kind:'doc', size:'120 KB', status:'converted', redirect:true, page:'/legal', flags:[], note:'docx → pdf' },
  ] : seedItems;

  // Rename rules catalog (presets the user can toggle)
  const DEFAULT_RULES = {
    kebab:      true,   // Slug-case (lowercase, dashes)
    iso_date:   true,   // Date normalization → YYYY-MM-DD
    parent:     false,  // Prefix with parent-page context
    lowercase:  true,   // .PDF → .pdf
    diacritics: true,   // Résumé → resume
    strip_parens: true, // remove (final), _final, _v2, _v3 etc
    max_len:    true,   // cap at 80 chars
  };
  const [rules, setRules] = useState(DEFAULT_RULES);
  const [rulesOpen, setRulesOpen] = useState(true);
  const [items, setItems] = useState(EXTENDED_SEED);
  const [kind, setKind] = useState('all');
  const [selected, setSelected] = useState(new Set());
  const [detail, setDetail] = useState(null); // id of opened row
  const [viewMode, setViewMode] = useState('split'); // 'list' | 'split'

  // Apply rename rules to produce a proposed new filename
  const proposeName = (oldPath, flags, page) => {
    const parts = oldPath.split('/');
    let name = parts.pop();
    const dir = parts.join('/');
    const extMatch = name.match(/\.([a-z0-9]+)$/i);
    let ext = extMatch ? extMatch[1] : '';
    let base = extMatch ? name.slice(0, name.length - ext.length - 1) : name;

    // Ext lowercasing
    if (rules.lowercase) ext = ext.toLowerCase();

    // Diacritics / special char strip
    if (rules.diacritics) {
      base = base.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
    }

    // Strip (final), _v2, FINAL, etc
    if (rules.strip_parens) {
      base = base
        .replace(/[\s_-]*\(([^)]*)\)[\s_-]*/gi, '-')     // (final) → -
        .replace(/[\s_-]+(final|FINAL|v\d+|V\d+|draft|DRAFT)[\s_-]*$/gi, '')
        .replace(/[\s_-]+(final|FINAL|v\d+|V\d+|draft|DRAFT)[\s_-]+/gi, '-');
    }

    // Date normalization M-D-YY or Mon D YYYY → YYYY-MM-DD
    if (rules.iso_date) {
      base = base
        .replace(/(\d{1,2})-(\d{1,2})-(\d{2,4})/g, (_, m, d, y) => {
          const yr = y.length === 2 ? '20' + y : y;
          return `${yr}-${m.padStart(2,'0')}-${d.padStart(2,'0')}`;
        })
        .replace(/\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*[\s-]+(\d{1,2})[\s,-]+(\d{4})/gi,
          (_, mon, d, y) => {
            const months = { Jan:'01',Feb:'02',Mar:'03',Apr:'04',May:'05',Jun:'06',Jul:'07',Aug:'08',Sep:'09',Oct:'10',Nov:'11',Dec:'12' };
            const m = months[mon.slice(0,1).toUpperCase() + mon.slice(1,3).toLowerCase()] || '01';
            return `${y}-${m}-${String(d).padStart(2,'0')}`;
          });
    }

    // Kebab-case
    if (rules.kebab) {
      base = base
        .toLowerCase()
        .replace(/[^a-z0-9-]+/g, '-')
        .replace(/-+/g, '-')
        .replace(/^-|-$/g, '');
    }

    // Parent-page prefix
    if (rules.parent && page) {
      const segment = page.split('/').filter(Boolean).slice(-1)[0];
      if (segment && !base.startsWith(segment)) {
        base = `${segment}-${base}`;
      }
    }

    // Length cap at word boundary
    if (rules.max_len && base.length > 60) {
      base = base.slice(0, 60).replace(/-[^-]*$/, '');
    }

    return `${dir}/${base}${ext ? '.' + ext : ''}`;
  };

  // Recompute proposed names when rules or items change
  const proposed = useMemo(() => {
    return items.map(it => ({
      ...it,
      proposed: it.newUrl || proposeName(it.oldUrl, it.flags || [], it.page),
    }));
  }, [items, rules]);

  const kinds = ['all', 'pdf', 'image', 'doc', 'video', 'spreadsheet'];
  const filtered = proposed.filter(i => kind === 'all' || i.kind === kind);

  const kindIcon = k => ({ pdf:'docs', image:'image', doc:'docs', video:'activity', spreadsheet:'chart' })[k] || 'docs';
  const statusTone = s => ({ mapped:'ok', converted:'beam', unmapped:'warn' })[s] || '';
  const flagLabel = {
    'case':'Uppercase',
    'spaces':'Spaces',
    'date':'Non-ISO date',
    'revision':'"final" / "v2"',
    'generic':'Generic name',
    'no-alt':'No alt-text',
    'oversized':'Oversized',
    'no-ocr':'No OCR',
    'diacritics':'Diacritics',
    'parens':'Parentheses',
  };
  const flagTone = f => ({ 'no-alt':'warn','no-ocr':'warn','oversized':'warn' })[f] || '';

  const toggleAll = () => {
    if (selected.size === filtered.length) setSelected(new Set());
    else setSelected(new Set(filtered.map(f => f.id)));
  };
  const toggleOne = (id) => {
    const n = new Set(selected);
    if (n.has(id)) n.delete(id); else n.add(id);
    setSelected(n);
  };

  const acceptProposed = (id) => {
    setItems(items.map(it => it.id === id
      ? { ...it, newUrl: proposed.find(p => p.id === id).proposed, status:'mapped' }
      : it));
    window.wpsbToast('Rename accepted', 'ok');
  };
  const acceptAllSelected = () => {
    const ids = selected.size ? selected : new Set(filtered.map(f => f.id));
    setItems(items.map(it => ids.has(it.id) && it.status === 'unmapped'
      ? { ...it, newUrl: proposed.find(p => p.id === it.id).proposed, status:'mapped' }
      : it));
    window.wpsbToast(`Accepted ${ids.size} proposed renames`, 'ok');
    setSelected(new Set());
  };
  const createRedirects = () => {
    const ids = selected.size ? selected : new Set(filtered.filter(f => f.newUrl && !f.redirect).map(f => f.id));
    setItems(items.map(it => ids.has(it.id) && it.newUrl
      ? { ...it, redirect:true }
      : it));
    window.wpsbToast(`Created ${ids.size} 301 redirects`, 'ok');
    setSelected(new Set());
  };

  const selDetail = filtered.find(f => f.id === detail);
  const splitActive = viewMode === 'split' && !!selDetail;

  return (
    <div style={{ display:'flex', flexDirection:'column', gap:12 }}>
      <VolatilityNotice variant="compact" scope="fileMap"/>
      {/* Rules Panel */}
      <div className="card">
        <div className="card-head" style={{ cursor:'pointer' }} onClick={() => setRulesOpen(o => !o)}>
          <div style={{ display:'flex', alignItems:'center', gap:10 }}>
            <Icon name="sliders" size={14}/>
            <h2 className="card-title" style={{ margin:0 }}>Rename Rules</h2>
            <span style={{ fontSize:'.68rem', color:'var(--dim)', fontFamily:'var(--font-mono)' }}>
              {Object.values(rules).filter(Boolean).length}/{Object.keys(rules).length} active
            </span>
          </div>
          <Icon name="chevron-down" size={12} style={{ transform: rulesOpen ? 'rotate(180deg)' : 'none', transition:'transform .2s' }}/>
        </div>
        {rulesOpen && (
          <div className="card-body" style={{ display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(260px, 1fr))', gap:10 }}>
            <RuleToggle label="Slug-case" hint="County Minutes.PDF → county-minutes.pdf" on={rules.kebab} onChange={v => setRules({ ...rules, kebab:v })}/>
            <RuleToggle label="ISO date format" hint="1-15-25, Jan 15 2025 → 2025-01-15" on={rules.iso_date} onChange={v => setRules({ ...rules, iso_date:v })}/>
            <RuleToggle label="Parent-page prefix" hint="Minutes → commission-minutes" on={rules.parent} onChange={v => setRules({ ...rules, parent:v })}/>
            <RuleToggle label="Lowercase extension" hint=".PDF → .pdf" on={rules.lowercase} onChange={v => setRules({ ...rules, lowercase:v })}/>
            <RuleToggle label="Strip diacritics" hint="Résumé → resume" on={rules.diacritics} onChange={v => setRules({ ...rules, diacritics:v })}/>
            <RuleToggle label='Strip "final" / "v2"' hint="report-FINAL-v2.pdf → report.pdf" on={rules.strip_parens} onChange={v => setRules({ ...rules, strip_parens:v })}/>
            <RuleToggle label="Length cap (60 chars)" hint="Truncate at word boundary" on={rules.max_len} onChange={v => setRules({ ...rules, max_len:v })}/>
          </div>
        )}
      </div>

      {/* File Map table */}
      <div className="card">
        <div className="card-head" style={{ flexWrap:'wrap', gap:10 }}>
          <h2 className="card-title">File &amp; Document URL Mapping</h2>
          <div style={{ display:'flex', gap:6, flexWrap:'wrap' }}>
            <div className="tw-seg" role="group" aria-label="View mode">
              <button type="button" className={viewMode==='clusters'?'active':''} onClick={() => setViewMode('clusters')}>🧩 Clusters</button>
              <button type="button" className={viewMode==='list'?'active':''} onClick={() => setViewMode('list')}>List</button>
              <button type="button" className={viewMode==='split'?'active':''} onClick={() => setViewMode('split')}>Split</button>
            </div>
            <button className="btn btn-primary btn-sm" onClick={() => window.wpsbToast('Add mapping dialog', 'info')}>
              <Icon name="plus" size={12}/>Add
            </button>
            <button className="btn btn-ghost btn-sm" onClick={() => window.wpsbToast('CSV template ready', 'ok')}>
              <Icon name="download" size={12} style={{ transform:'rotate(180deg)' }}/>Import CSV
            </button>
            <button className="btn btn-ghost btn-sm" onClick={createRedirects}>
              <Icon name="redirect" size={12}/>Create All Redirects
            </button>
          </div>
        </div>
        <div className="card-body">
          <div style={{ fontSize:'.74rem', color:'var(--dim)', marginBottom:12, display:'flex', alignItems:'center', gap:6, flexWrap:'wrap' }}>
            <Icon name="info" size={12}/>
            Map old file/document URLs to new locations. Rules engine auto-proposes normalized filenames; click a row for split-view edit, or select multiple to bulk-accept.
          </div>

          {items.length > 0 && (
            <div style={{ display:'flex', gap:4, marginBottom:12, flexWrap:'wrap' }} role="group" aria-label="Filter by kind">
              {kinds.map(k => (
                <button key={k}
                        className={'btn btn-ghost btn-sm' + (kind===k?' active':'')}
                        aria-pressed={kind===k}
                        style={kind===k ? { background:'var(--beam-dim)', color:'var(--beam)', borderColor:'var(--beam-dim)' } : null}
                        onClick={() => setKind(k)}>
                  {k === 'all' ? 'All' : k.charAt(0).toUpperCase()+k.slice(1)}
                </button>
              ))}
            </div>
          )}

          {items.length === 0 ? (
            <EmptyState
              icon="archive"
              title="No file mappings yet."
              hint="Import from the scanner WXR export, add mappings manually, or send files from the File Library."
              primary={{ label:'Add Mapping', onClick:() => window.wpsbToast('Add mapping dialog', 'info') }}
              secondary={{ label:'Import CSV', onClick:() => window.wpsbToast('CSV uploader opened', 'info') }}
            />
          ) : viewMode === 'clusters' && window.FileMapClusters ? (
            <window.FileMapClusters.ClustersView
              items={items}
              presetId={'municipal'}
              onPresetChange={() => {}}
              onApplyCluster={(clusterId, rewrittenItems) => {
                setItems(rewrittenItems);
                window.wpsbToast(`Applied cluster "${clusterId}" — ${rewrittenItems.filter(r => r.newUrl).length} files proposed`, 'ok');
              }}
              onOpenRow={(id) => { setDetail(id); setViewMode('split'); }}
            />
          ) : (
            <div style={{ display:'grid', gridTemplateColumns: splitActive ? '1fr 380px' : '1fr', gap:12, alignItems:'start' }}>
              {/* Table */}
              <div style={{ overflowX:'auto' }}>
                <table className="table">
                  <thead>
                    <tr>
                      <th style={{ width:28 }}>
                        <input type="checkbox"
                               checked={selected.size > 0 && selected.size === filtered.length}
                               ref={el => { if (el) el.indeterminate = selected.size > 0 && selected.size < filtered.length; }}
                               onChange={toggleAll}
                               aria-label="Select all mappings"/>
                      </th>
                      <th style={{ width:32 }}></th>
                      <th>Current → Proposed</th>
                      <th style={{ width:80 }}>Size</th>
                      <th style={{ width:110 }}>Flags</th>
                      <th style={{ width:100 }}>Status</th>
                      <th style={{ width:72, textAlign:'center' }}>301</th>
                    </tr>
                  </thead>
                  <tbody>
                    {filtered.map(f => {
                      const curName = f.oldUrl.split('/').pop();
                      const newName = (f.newUrl || f.proposed || '').split('/').pop();
                      const isOpen = detail === f.id;
                      const isSel = selected.has(f.id);
                      return (
                        <tr key={f.id}
                            style={{
                              cursor:'pointer',
                              background: isOpen ? 'var(--beam-dim)' : isSel ? 'var(--beam-dim)' : undefined,
                              outline: isOpen ? '1px solid var(--beam-dim)' : undefined,
                            }}
                            onClick={() => setDetail(isOpen ? null : f.id)}>
                          <td onClick={e => e.stopPropagation()}>
                            <input type="checkbox" checked={isSel} onChange={() => toggleOne(f.id)}
                                   aria-label={`Select ${curName}`}/>
                          </td>
                          <td>
                            <div style={{ width:28, height:28, background:'var(--surface-3)', border:'1px solid var(--border)', borderRadius:4, display:'flex', alignItems:'center', justifyContent:'center' }}>
                              <Icon name={kindIcon(f.kind)} size={13}/>
                            </div>
                          </td>
                          <td className="mono" style={{ fontSize:'.72rem' }}>
                            <div style={{ color:'var(--text)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', maxWidth:280 }}>{curName}</div>
                            <div style={{ display:'flex', alignItems:'center', gap:4, fontSize:'.66rem', color: f.newUrl ? 'var(--green)' : 'var(--beam)', marginTop:2 }}>
                              <span style={{ color:'var(--dim)' }}>→</span>
                              <span style={{ overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', maxWidth:260 }}>{newName || <em style={{ color:'var(--dim)' }}>— none —</em>}</span>
                            </div>
                            {f.page && <div style={{ fontSize:'.6rem', color:'var(--dim)', marginTop:1 }}>on {f.page}</div>}
                          </td>
                          <td className="mono" style={{ fontSize:'.7rem', color:'var(--dim)' }}>{f.size}</td>
                          <td>
                            <div style={{ display:'flex', flexWrap:'wrap', gap:2 }}>
                              {(f.flags || []).slice(0,2).map(fl => (
                                <span key={fl} className={`tag ${flagTone(fl)}`} style={{ fontSize:'.54rem', padding:'1px 4px' }}>
                                  {flagLabel[fl] || fl}
                                </span>
                              ))}
                              {(f.flags || []).length > 2 && (
                                <span style={{ fontSize:'.6rem', color:'var(--dim)' }}>+{f.flags.length - 2}</span>
                              )}
                            </div>
                          </td>
                          <td><span className={`tag ${statusTone(f.status)}`} style={{ whiteSpace:'nowrap' }}>{f.status.toUpperCase()}</span></td>
                          <td style={{ textAlign:'center' }} onClick={e => e.stopPropagation()}>
                            {f.redirect ? (
                              <Icon name="check" size={14}/>
                            ) : f.newUrl || f.proposed ? (
                              <button className="btn btn-ghost btn-sm"
                                      onClick={() => { acceptProposed(f.id); }}>
                                Accept
                              </button>
                            ) : (
                              <span style={{ color:'var(--dim)' }}>—</span>
                            )}
                          </td>
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
              </div>

              {/* Split-view detail drawer */}
              {splitActive && selDetail && (
                <FileMapDetail
                  f={selDetail}
                  onClose={() => setDetail(null)}
                  onAccept={() => acceptProposed(selDetail.id)}
                  onUpdate={(patch) => setItems(items.map(it => it.id === selDetail.id ? { ...it, ...patch } : it))}
                  flagLabel={flagLabel}
                  flagTone={flagTone}
                />
              )}
            </div>
          )}
        </div>
      </div>

      {/* Sticky bulk-action bar */}
      {selected.size > 0 && (
        <div style={{
          position:'sticky', bottom:12, zIndex:30,
          background:'var(--surface-2)', border:'1px solid var(--beam)',
          borderRadius:10, padding:'10px 14px',
          display:'flex', alignItems:'center', gap:10, flexWrap:'wrap',
          boxShadow:'0 8px 30px var(--beam-dim)',
        }} role="region" aria-label="Bulk actions">
          <div style={{ fontSize:'.82rem', fontWeight:600, color:'var(--text)' }}>{selected.size} selected</div>
          <div style={{ flex:1, minWidth:80 }}/>
          <button className="btn btn-ghost btn-sm" onClick={() => setSelected(new Set())}>Clear</button>
          <button className="btn btn-ghost btn-sm" onClick={createRedirects}>
            <Icon name="redirect" size={12}/>Create redirects
          </button>
          <button className="btn btn-primary btn-sm" onClick={acceptAllSelected}>
            <Icon name="check" size={12}/>Accept proposed
          </button>
        </div>
      )}
    </div>
  );
}

/* Simple rule toggle — checkbox + label + hint text */
function RuleToggle({ label, hint, on, onChange }) {
  return (
    <label style={{
      display:'flex', gap:10, alignItems:'flex-start', cursor:'pointer',
      padding:'8px 10px', background: on ? 'var(--beam-dim)' : 'var(--surface-3)',
      border:'1px solid ' + (on ? 'var(--beam-dim)' : 'var(--border)'),
      borderRadius:6, transition:'all .15s',
    }}>
      <input type="checkbox" checked={on} onChange={e => onChange(e.target.checked)} style={{ marginTop:2 }}/>
      <div style={{ flex:1 }}>
        <div style={{ fontSize:'.78rem', fontWeight:500, color:'var(--text)' }}>{label}</div>
        <div style={{ fontSize:'.66rem', color:'var(--dim)', fontFamily:'var(--font-mono)', marginTop:2, lineHeight:1.4 }}>{hint}</div>
      </div>
    </label>
  );
}

/* File Map split-view detail drawer */
function FileMapDetail({ f, onClose, onAccept, onUpdate, flagLabel, flagTone }) {
  const { useState } = React;
  const [editing, setEditing] = useState(f.newUrl || f.proposed || '');

  return (
    <div style={{
      position:'sticky', top:8,
      background:'var(--surface-2)', border:'1px solid var(--border)',
      borderRadius:10, padding:14, display:'flex', flexDirection:'column', gap:12,
    }}>
      <div style={{ display:'flex', alignItems:'center', gap:8 }}>
        <Icon name="edit" size={14}/>
        <strong style={{ fontSize:'.86rem', flex:1 }}>File details</strong>
        <button className="btn btn-ghost btn-sm" onClick={onClose} aria-label="Close details" style={{ padding:'2px 6px' }}>
          <Icon name="x" size={12}/>
        </button>
      </div>

      <div>
        <div className="mono" style={{ fontSize:'.58rem', color:'var(--dim)', letterSpacing:1, marginBottom:3, textTransform:'uppercase' }}>Current URL</div>
        <div className="mono" style={{ fontSize:'.72rem', color:'var(--text)', background:'var(--surface-3)', padding:'6px 8px', borderRadius:4, border:'1px solid var(--border)', wordBreak:'break-all' }}>
          {f.oldUrl}
        </div>
      </div>

      <div>
        <div className="mono" style={{ fontSize:'.58rem', color:'var(--dim)', letterSpacing:1, marginBottom:3, textTransform:'uppercase' }}>Proposed URL</div>
        <textarea
          value={editing}
          onChange={e => setEditing(e.target.value)}
          className="mono"
          rows={2}
          style={{
            width:'100%', fontSize:'.72rem', color:'var(--beam)',
            background:'var(--surface-3)', padding:'6px 8px', borderRadius:4,
            border:'1px solid var(--border)', resize:'vertical',
            outline:'none', fontFamily:'var(--font-mono)',
          }}/>
        <button
          className="btn btn-ghost btn-sm"
          onClick={() => { onUpdate({ newUrl: editing, status:'mapped' }); window.wpsbToast('Mapping saved', 'ok'); }}
          style={{ marginTop:4, fontSize:'.68rem' }}>
          Save custom path
        </button>
      </div>

      <div>
        <div className="mono" style={{ fontSize:'.58rem', color:'var(--dim)', letterSpacing:1, marginBottom:4, textTransform:'uppercase' }}>Context</div>
        <div style={{ fontSize:'.72rem', color:'var(--muted)', display:'flex', flexDirection:'column', gap:3 }}>
          <div>Kind: <span style={{ color:'var(--text)' }}>{f.kind}</span></div>
          <div>Size: <span style={{ color:'var(--text)' }}>{f.size}</span></div>
          {f.page && <div>Linked from: <span className="mono" style={{ color:'var(--beam)' }}>{f.page}</span></div>}
          {f.note && <div>Note: <span style={{ color:'var(--text)' }}>{f.note}</span></div>}
        </div>
      </div>

      {(f.flags || []).length > 0 && (
        <div>
          <div className="mono" style={{ fontSize:'.58rem', color:'var(--dim)', letterSpacing:1, marginBottom:4, textTransform:'uppercase' }}>ADA / Naming flags</div>
          <div style={{ display:'flex', flexWrap:'wrap', gap:4 }}>
            {f.flags.map(fl => (
              <span key={fl} className={`tag ${flagTone(fl)}`} style={{ fontSize:'.6rem', padding:'2px 6px' }}>
                {flagLabel[fl] || fl}
              </span>
            ))}
          </div>
        </div>
      )}

      <div style={{ display:'flex', gap:6, marginTop:4, paddingTop:10, borderTop:'1px solid var(--border)' }}>
        <button className="btn btn-ghost btn-sm" style={{ flex:1 }}
                onClick={() => window.wpsbToast('Preview opened', 'info')}>
          Preview file
        </button>
        <button className="btn btn-primary btn-sm" style={{ flex:1 }}
                onClick={onAccept} disabled={f.status !== 'unmapped'}>
          Accept rename
        </button>
      </div>
    </div>
  );
}

/* ── Add Redirect modal ─────────────────────────────────────── */
function AddRedirectModal({ onClose, site }) {
  const { useState } = React;
  const [source, setSource] = useState('');
  const [target, setTarget] = useState('');
  const [code, setCode]     = useState(301);
  const [kind, setKind]     = useState('page');
  const [regex, setRegex]   = useState(false);

  return (
    <div onClick={onClose}
         style={{ position:'fixed', inset:0, background:'rgba(0,0,0,.6)', backdropFilter:'blur(4px)', zIndex:1000, display:'flex', alignItems:'center', justifyContent:'center', padding:20 }}>
      <div onClick={e => e.stopPropagation()}
           style={{ background:'var(--surface)', border:'1px solid var(--border2, var(--border))', borderRadius:10, maxWidth:560, width:'100%' }}>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', padding:'14px 18px', borderBottom:'1px solid var(--border)' }}>
          <strong style={{ fontSize:'.92rem' }}>Add Redirect · {site}</strong>
          <button onClick={onClose} style={{ background:'none', border:'none', color:'var(--dim)', fontSize:'1.2rem', cursor:'pointer' }}>✕</button>
        </div>
        <div style={{ padding:18, display:'flex', flexDirection:'column', gap:14 }}>
          <div className="field">
            <label>Source URL (old)</label>
            <input value={source} onChange={e => setSource(e.target.value)} placeholder="/old-page or /old-page/* for regex"/>
          </div>
          <div className="field">
            <label>Target URL (new)</label>
            <input value={target} onChange={e => setTarget(e.target.value)} placeholder="/new-page"/>
          </div>
          <div style={{ display:'flex', gap:12, flexWrap:'wrap' }}>
            <div className="field" style={{ flex:'1 1 160px' }}>
              <label>HTTP code</label>
              <div className="tw-seg">
                {[301, 302, 307, 410].map(c => (
                  <button key={c} type="button" className={code===c?'active':''} onClick={() => setCode(c)}>{c}</button>
                ))}
              </div>
            </div>
            <div className="field" style={{ flex:'1 1 180px' }}>
              <label>Kind</label>
              <select value={kind} onChange={e => setKind(e.target.value)}
                      style={{ width:'100%', padding:'8px 10px', background:'var(--surface-3)', border:'1px solid var(--border)', borderRadius:6, color:'var(--text)', fontSize:'.82rem' }}>
                {['page','post','media','document','product','external'].map(k => <option key={k} value={k}>{k}</option>)}
              </select>
            </div>
          </div>
          <label className="opt-check" style={{ marginTop:4 }}>
            <input type="checkbox" checked={regex} onChange={e => setRegex(e.target.checked)}/>
            <span>Treat source as regex / wildcard (e.g. <code style={{ background:'var(--surface-3)', padding:'1px 4px', borderRadius:3 }}>/old/*</code> → <code style={{ background:'var(--surface-3)', padding:'1px 4px', borderRadius:3 }}>/new/$1</code>)</span>
          </label>

          <div style={{ display:'flex', gap:8, justifyContent:'flex-end', marginTop:6, paddingTop:14, borderTop:'1px solid var(--border)' }}>
            <button className="btn btn-ghost btn-sm" onClick={onClose}>Cancel</button>
            <button className="btn btn-primary btn-sm"
                    onClick={() => {
                      if (!source || !target) { window.wpsbToast('Source and target required', 'warn'); return; }
                      window.wpsbToast(`Redirect created: ${source} → ${target}`, 'ok');
                      onClose();
                    }}>
              <Icon name="plus" size={12}/>Create Redirect
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ── Shared empty state ─────────────────────────────────────── */
function EmptyState({ icon, title, hint, primary, secondary }) {
  return (
    <div style={{ textAlign:'center', padding:'40px 20px' }}>
      <div style={{ width:48, height:48, margin:'0 auto 14px', borderRadius:24, background:'var(--surface-3)', border:'1px solid var(--border)', display:'flex', alignItems:'center', justifyContent:'center', color:'var(--dim)' }}>
        <Icon name={icon || 'info'} size={22}/>
      </div>
      <div style={{ fontSize:'.92rem', fontWeight:600, marginBottom:6 }}>{title}</div>
      {hint && <div style={{ fontSize:'.76rem', color:'var(--dim)', maxWidth:380, margin:'0 auto 14px', lineHeight:1.5 }}>{hint}</div>}
      <div style={{ display:'flex', gap:6, justifyContent:'center' }}>
        {primary && (
          <button className="btn btn-primary btn-sm" onClick={primary.onClick}>
            {primary.label}
          </button>
        )}
        {secondary && (
          <button className="btn btn-ghost btn-sm" onClick={secondary.onClick}>
            {secondary.label}
          </button>
        )}
      </div>
    </div>
  );
}

window.Redirects = Redirects;
