/* global React, Icon, Link, mgConfirm, mgAlert, mgToast */
// ══════════════════════════════════════════════════════════════════
// AdminBot.jsx — Gestion centralisée du bot Discord
// Accessible uniquement aux officiers (requireMongolieAdmin côté API).
// ══════════════════════════════════════════════════════════════════
const { useState: useAB, useEffect: useEAB } = React;

const fmtAB = n => (n ?? 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');

const fmtRelTime = (iso) => {
  if (!iso) return 'jamais';
  let ts;
  try {
    // SQLite datetime() format = 'YYYY-MM-DD HH:MM:SS' (UTC), pas ISO
    ts = new Date(iso.replace(' ', 'T') + (iso.includes('Z') ? '' : 'Z')).getTime();
  } catch { return iso; }
  const diff = (Date.now() - ts) / 1000;
  if (diff < 60)   return Math.floor(diff) + 's';
  if (diff < 3600) return Math.floor(diff / 60) + ' min';
  if (diff < 86400) return Math.floor(diff / 3600) + ' h';
  return Math.floor(diff / 86400) + ' j';
};

function AdminBot() {
  const [status, setStatus] = useAB(undefined);
  const [tab, setTab]       = useAB('status');  // status | outbox | triggers | announce | config

  const reloadStatus = () => {
    fetch('/api/admin/bot/status', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null).then(setStatus)
      .catch(() => setStatus(null));
  };
  useEAB(() => {
    reloadStatus();
    const t = setInterval(reloadStatus, 30000);
    return () => clearInterval(t);
  }, []);

  if (status === undefined) return <div style={{padding:60, textAlign:'center', color:'var(--ink-mute)'}}>Chargement…</div>;
  if (!status) return <div style={{padding:60, textAlign:'center', color:'var(--mg-danger)'}}>Erreur (auth ?)</div>;

  return (
    <div className="stack gap-6" style={{padding:'24px 20px'}}>
      <header>
        <div className="eyebrow" style={{color:'var(--mg-gold-700)'}}>🤖 Officier · Gestion</div>
        <h1 style={{fontSize:38, marginTop:8}}>Bot <em style={{color:'var(--accent)',fontStyle:'normal'}}>Discord</em></h1>
        <p className="soft" style={{maxWidth:'64ch', marginTop:8}}>
          Contrôle complet du bot Mongolie : outbox, scheduler, annonces custom, configuration channels & rôles.
        </p>
      </header>

      {/* KPI bar */}
      <div style={{display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(140px, 1fr))', gap:12}}>
        <Kpi label="Outbox pending"  value={status.outbox_counts.pending || 0} color={status.outbox_counts.pending > 5 ? 'var(--mg-gold-500)' : 'var(--accent)'}/>
        <Kpi label="Outbox failed"   value={status.outbox_counts.failed || 0}  color={status.outbox_counts.failed > 0 ? 'var(--mg-danger)' : 'var(--mg-success)'}/>
        <Kpi label="Outbox sent"     value={status.outbox_counts.sent || 0}/>
        <Kpi label="Tâches scheduler" value={Object.keys(status.scheduler).length}/>
      </div>

      {/* Tabs */}
      <div className="row gap-2" style={{flexWrap:'wrap'}}>
        <button className={'btn btn-sm ' + (tab === 'status' ? 'btn-gold' : 'btn-ghost')} onClick={() => setTab('status')}>📊 Status</button>
        <button className={'btn btn-sm ' + (tab === 'outbox' ? 'btn-gold' : 'btn-ghost')} onClick={() => setTab('outbox')}>📤 Outbox</button>
        <button className={'btn btn-sm ' + (tab === 'triggers' ? 'btn-gold' : 'btn-ghost')} onClick={() => setTab('triggers')}>⚡ Triggers</button>
        <button className={'btn btn-sm ' + (tab === 'announce' ? 'btn-gold' : 'btn-ghost')} onClick={() => setTab('announce')}>📣 Annonce</button>
        <button className={'btn btn-sm ' + (tab === 'features' ? 'btn-gold' : 'btn-ghost')} onClick={() => setTab('features')}>🎚️ Features</button>
        <button className={'btn btn-sm ' + (tab === 'thresholds' ? 'btn-gold' : 'btn-ghost')} onClick={() => setTab('thresholds')}>🔢 Seuils</button>
        <button className={'btn btn-sm ' + (tab === 'templates' ? 'btn-gold' : 'btn-ghost')} onClick={() => setTab('templates')}>📝 Templates</button>
        <button className={'btn btn-sm ' + (tab === 'config' ? 'btn-gold' : 'btn-ghost')} onClick={() => setTab('config')}>⚙️ Channels & Rôles</button>
        <button className={'btn btn-sm ' + (tab === 'tree' ? 'btn-gold' : 'btn-ghost')} onClick={() => setTab('tree')}>🌳 Discord complet</button>
        <button className={'btn btn-sm ' + (tab === 'welcome' ? 'btn-gold' : 'btn-ghost')} onClick={() => setTab('welcome')}>👋 Welcome</button>
        <button className={'btn btn-sm ' + (tab === 'setup' ? 'btn-gold' : 'btn-ghost')} onClick={() => setTab('setup')}>🔧 Setup</button>
      </div>

      {tab === 'status'     && <TabStatus   status={status} onChange={reloadStatus}/>}
      {tab === 'outbox'     && <TabOutbox   onChange={reloadStatus}/>}
      {tab === 'triggers'   && <TabTriggers status={status} onChange={reloadStatus}/>}
      {tab === 'announce'   && <TabAnnounce/>}
      {tab === 'features'   && <TabFeatures/>}
      {tab === 'thresholds' && <TabThresholds/>}
      {tab === 'templates'  && <TabTemplates/>}
      {tab === 'config'     && <TabConfigEditable/>}
      {tab === 'tree'       && <TabDiscordTree/>}
      {tab === 'welcome'    && <TabWelcome/>}
      {tab === 'setup'      && <TabSetup/>}
    </div>
  );
}

function Kpi({ label, value, color }) {
  return (
    <div style={{padding:'12px 14px', background:'var(--surface-2)', borderRadius:'var(--r-sm)', borderLeft:'3px solid ' + (color || 'var(--accent)')}}>
      <div className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)'}}>{label}</div>
      <div style={{fontFamily:'var(--font-display)', fontWeight:700, fontSize:26, color: color || 'var(--accent)', marginTop:2}}>{value}</div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────────
function TabStatus({ status }) {
  return (
    <section className="card" style={{padding:20}}>
      <h2 style={{fontSize:20, marginBottom:14}}>📊 État des tâches scheduler</h2>
      <table style={{width:'100%', fontSize:14}}>
        <thead>
          <tr style={{borderBottom:'1px solid var(--border)'}}>
            <th style={{textAlign:'left', padding:'8px 6px', fontSize:11, color:'var(--ink-mute)', textTransform:'uppercase'}}>Tâche</th>
            <th style={{textAlign:'left', padding:'8px 6px', fontSize:11, color:'var(--ink-mute)', textTransform:'uppercase'}}>Description</th>
            <th style={{textAlign:'right', padding:'8px 6px', fontSize:11, color:'var(--ink-mute)', textTransform:'uppercase'}}>Dernier run</th>
          </tr>
        </thead>
        <tbody>
          {Object.entries(status.scheduler).map(([key, info]) => (
            <tr key={key} style={{borderBottom:'1px solid var(--border)'}}>
              <td style={{padding:'10px 6px'}} className="mono">{key}</td>
              <td style={{padding:'10px 6px', color:'var(--ink-soft)', fontSize:13}}>{info.label}</td>
              <td style={{padding:'10px 6px', textAlign:'right'}} className="mono">
                {info.last_run ? (
                  <span style={{color: 'var(--mg-success)'}}>il y a {fmtRelTime(info.last_run)}</span>
                ) : (
                  <span style={{color: 'var(--ink-mute)'}}>jamais</span>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      <div className="soft" style={{fontSize:11, marginTop:14}}>
        Le serveur a tiqué à <code>{new Date(status.server_time).toLocaleTimeString('fr-FR')}</code>.
        Page auto-refresh toutes les 30s.
      </div>
    </section>
  );
}

// ───────────────────────────────────────────────────────────────
function TabOutbox({ onChange }) {
  const [items, setItems]   = useAB(undefined);
  const [filter, setFilter] = useAB('pending');

  const reload = () => {
    fetch('/api/admin/bot/outbox?status=' + filter + '&limit=100', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null).then(d => setItems(d?.items || []))
      .catch(() => setItems([]));
  };
  useEAB(() => { reload(); }, [filter]);

  const retry = async (id) => {
    if (!(await mgConfirm('Retry cette entrée ? (Reviens en status pending et le bot va re-essayer)', 'Retry outbox'))) return;
    await fetch('/api/admin/bot/outbox/' + id + '/retry', { method: 'POST', credentials: 'same-origin' });
    mgToast('Remis en pending', { type: 'success' });
    reload(); onChange?.();
  };
  const del = async (id) => {
    if (!(await mgConfirm('Supprimer définitivement cette entrée ?', 'Delete outbox'))) return;
    await fetch('/api/admin/bot/outbox/' + id, { method: 'DELETE', credentials: 'same-origin' });
    mgToast('Supprimé', { type: 'success' });
    reload(); onChange?.();
  };

  return (
    <section className="card" style={{padding:20}}>
      <div className="row gap-3" style={{justifyContent:'space-between', alignItems:'center', marginBottom:14, flexWrap:'wrap'}}>
        <h2 style={{fontSize:20, margin:0}}>📤 Outbox · {items?.length || 0} entrée(s)</h2>
        <div className="row gap-2">
          <button className={'btn btn-sm ' + (filter === 'pending' ? 'btn-gold' : 'btn-ghost')} onClick={() => setFilter('pending')}>⏳ Pending</button>
          <button className={'btn btn-sm ' + (filter === 'sent' ? 'btn-gold' : 'btn-ghost')}    onClick={() => setFilter('sent')}>✅ Sent</button>
          <button className={'btn btn-sm ' + (filter === 'failed' ? 'btn-gold' : 'btn-ghost')}  onClick={() => setFilter('failed')}>❌ Failed</button>
          <button className="btn btn-ghost btn-sm" onClick={reload}>🔄 Refresh</button>
        </div>
      </div>

      {!items ? (
        <div style={{padding:20, textAlign:'center', color:'var(--ink-mute)'}}>Chargement…</div>
      ) : items.length === 0 ? (
        <div style={{padding:30, textAlign:'center', color:'var(--ink-mute)'}}>Aucune entrée en {filter}.</div>
      ) : (
        <div className="stack gap-2">
          {items.map(it => (
            <article key={it.id} style={{padding:'12px 14px', background:'var(--surface-2)', borderRadius:'var(--r-sm)', borderLeft:'3px solid ' + (it.status === 'failed' ? 'var(--mg-danger)' : it.status === 'pending' ? 'var(--mg-gold-500)' : 'var(--mg-success)')}}>
              <div className="row gap-2" style={{justifyContent:'space-between', alignItems:'baseline', flexWrap:'wrap'}}>
                <div className="row gap-2" style={{alignItems:'baseline'}}>
                  <span className="mono" style={{fontSize:11, color:'var(--ink-mute)'}}>#{it.id}</span>
                  <span style={{fontWeight:700, color:'var(--accent)'}}>{it.type}</span>
                </div>
                <div className="row gap-2" style={{alignItems:'baseline'}}>
                  <span className="mono" style={{fontSize:11, color:'var(--ink-mute)'}}>
                    {fmtRelTime(it.sent_at || it.created_at)}
                  </span>
                  {it.status !== 'sent' && (
                    <button className="btn btn-ghost btn-sm" onClick={() => retry(it.id)}>↻ Retry</button>
                  )}
                  <button className="btn btn-ghost btn-sm" onClick={() => del(it.id)} style={{color:'var(--mg-danger)'}}>🗑</button>
                </div>
              </div>
              <pre style={{margin:'8px 0 0', padding:8, background:'var(--bg)', borderRadius:4, fontSize:11, overflow:'auto', maxHeight:120, color:'var(--ink-soft)'}}>{JSON.stringify(it.payload, null, 2)}</pre>
              {it.error && (
                <div style={{marginTop:6, fontSize:12, color:'var(--mg-danger)'}}>⚠️ {it.error}</div>
              )}
            </article>
          ))}
        </div>
      )}
    </section>
  );
}

// ───────────────────────────────────────────────────────────────
function TabTriggers({ status, onChange }) {
  const trigger = async (taskName, label) => {
    if (!(await mgConfirm(`Lancer la tâche "${label}" immédiatement ?`, 'Trigger manuel'))) return;
    try {
      const r = await fetch('/api/admin/bot/trigger/' + taskName, { method: 'POST', credentials: 'same-origin' });
      if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.error || 'Erreur'); }
      mgToast(`Tâche "${label}" enqueue. Va voir le résultat dans Outbox dans 5-10s.`, { type: 'success', duration: 5000 });
      onChange?.();
    } catch (e) { mgToast(e.message, { type: 'error' }); }
  };

  return (
    <section className="card" style={{padding:20}}>
      <h2 style={{fontSize:20, marginBottom:14}}>⚡ Triggers manuels</h2>
      <p className="soft" style={{fontSize:13, marginBottom:18}}>
        Force l'exécution immédiate d'une tâche scheduler (ne change pas son cron normal — c'est juste un run supplémentaire).
        Le bot va la consommer dans les 5 secondes via l'outbox.
      </p>
      <div style={{display:'grid', gridTemplateColumns:'repeat(auto-fit, minmax(280px, 1fr))', gap:12}}>
        {Object.entries(status.scheduler).map(([key, info]) => (
          <div key={key} style={{padding:14, background:'var(--surface-2)', borderRadius:'var(--r-sm)'}}>
            <div style={{fontWeight:700, fontSize:14, color:'var(--accent)'}}>{info.label}</div>
            <div className="soft" style={{fontSize:11, marginTop:2}}>
              Dernier run : {info.last_run ? 'il y a ' + fmtRelTime(info.last_run) : 'jamais'}
            </div>
            <button className="btn btn-gold btn-sm" style={{marginTop:10, width:'100%'}} onClick={() => trigger(key, info.label)}>
              ⚡ Lancer maintenant
            </button>
          </div>
        ))}
      </div>
    </section>
  );
}

// ───────────────────────────────────────────────────────────────
// TabAnnounce — Embed builder : compose un embed riche + preview live
function TabAnnounce() {
  const [title, setTitle]     = useAB('');
  const [description, setDesc] = useAB('');
  const [channel, setChannel] = useAB('announce');
  const [color, setColor]     = useAB('#d39b36');
  const [imageUrl, setImageUrl] = useAB('');
  const [thumbUrl, setThumbUrl] = useAB('');
  const [footer, setFooter]   = useAB('');
  const [stamp, setStamp]     = useAB(false);
  const [posting, setPosting] = useAB(false);

  const CHANNELS = [
    ['announce', '#annonces-pays'], ['bazar', '#commerce (Bazar)'],
    ['lignee', '#lignée-publique'], ['accueil_recrue', '#accueil-recrue'],
    ['chantiers', '#chantiers-actifs'], ['ticket_log', '#logs-tickets'],
    ['bienvenue_recrues', '#bienvenue-recrues'], ['classement_mensuel', '#classement-mensuel'],
    ['rapport_mensuel', '#rapport-mensuel'], ['log_fondations', '#log-fondations'],
    ['log_paliers_t5', '#log-paliers-t5'], ['log_recrues_actives', '#log-recrues-actives'],
    ['log_departs', '#log-departs'], ['log_audit_site', '#log-audit-site'],
  ];

  const send = async () => {
    if (!title.trim() && !description.trim()) {
      mgToast('Titre ou description requis', { type: 'error' }); return;
    }
    if (!(await mgConfirm(`Poster cet embed dans #${channel} ?`, 'Confirmer'))) return;
    setPosting(true);
    try {
      const r = await fetch('/api/admin/bot/announce', {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title, description, channel,
          color, image_url: imageUrl, thumbnail_url: thumbUrl, footer, timestamp: stamp,
        }),
      });
      if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.error || 'Erreur'); }
      mgToast('Embed enqueue, le bot le poste dans ~5s.', { type: 'success' });
      setTitle(''); setDesc(''); setImageUrl(''); setThumbUrl(''); setFooter('');
    } catch (e) { mgToast(e.message, { type: 'error' }); }
    finally { setPosting(false); }
  };

  const lbl = { fontSize:11, color:'var(--ink-mute)', display:'block', marginBottom:4 };

  return (
    <section className="card" style={{padding:20}}>
      <h2 style={{fontSize:20, marginBottom:14}}>📣 Embed builder</h2>
      <div style={{display:'grid', gridTemplateColumns:'minmax(0,1fr) minmax(0,340px)', gap:20, alignItems:'start'}}>
        {/* Form */}
        <div className="stack gap-3">
          <div>
            <label className="eyebrow" style={lbl}>Canal cible</label>
            <select value={channel} onChange={e => setChannel(e.target.value)} style={inputStyle}>
              {CHANNELS.map(([v, l]) => <option key={v} value={v}>{l}</option>)}
            </select>
          </div>
          <div>
            <label className="eyebrow" style={lbl}>Titre (max 256)</label>
            <input value={title} onChange={e => setTitle(e.target.value)} maxLength={256} placeholder="Ex: 📢 Réunion ce soir 21h" style={inputStyle}/>
          </div>
          <div>
            <label className="eyebrow" style={lbl}>Description (Markdown OK, max 4000)</label>
            <textarea value={description} onChange={e => setDesc(e.target.value)} maxLength={4000} rows={7}
              placeholder="Contenu. **gras**, *italique*, [lien](url)…"
              style={{...inputStyle, resize:'vertical', fontFamily:'inherit'}}/>
            <div className="soft" style={{fontSize:11, textAlign:'right', marginTop:2}}>{description.length}/4000</div>
          </div>
          <div style={{display:'grid', gridTemplateColumns:'120px 1fr', gap:10, alignItems:'center'}}>
            <div>
              <label className="eyebrow" style={lbl}>Couleur</label>
              <input type="color" value={color} onChange={e => setColor(e.target.value)} style={{width:'100%', height:38, border:'none', background:'none', cursor:'pointer'}}/>
            </div>
            <div>
              <label className="eyebrow" style={lbl}>Pied de page (footer)</label>
              <input value={footer} onChange={e => setFooter(e.target.value)} maxLength={256} placeholder="Ex: Mongolie · Staff" style={inputStyle}/>
            </div>
          </div>
          <div>
            <label className="eyebrow" style={lbl}>Image (URL, grande, en bas)</label>
            <input value={imageUrl} onChange={e => setImageUrl(e.target.value)} placeholder="https://i.imgur.com/…" style={inputStyle}/>
          </div>
          <div>
            <label className="eyebrow" style={lbl}>Vignette (URL, petite, en haut à droite)</label>
            <input value={thumbUrl} onChange={e => setThumbUrl(e.target.value)} placeholder="https://i.imgur.com/…" style={inputStyle}/>
          </div>
          <label className="row gap-2" style={{alignItems:'center', fontSize:13, cursor:'pointer'}}>
            <input type="checkbox" checked={stamp} onChange={e => setStamp(e.target.checked)}/>
            Afficher l'horodatage
          </label>
          <button className="btn btn-gold" disabled={posting} onClick={send}>
            {posting ? '…' : '📨 Envoyer dans #' + channel}
          </button>
        </div>

        {/* Preview Discord-like */}
        <div>
          <label className="eyebrow" style={lbl}>Aperçu</label>
          <div style={{background:'#313338', borderRadius:8, padding:14}}>
            <div style={{display:'flex', borderRadius:4, overflow:'hidden', background:'#2b2d31'}}>
              <div style={{width:4, background:color, flexShrink:0}}/>
              <div style={{padding:'12px 14px', flex:1, minWidth:0}}>
                <div style={{display:'flex', gap:10}}>
                  <div style={{flex:1, minWidth:0}}>
                    {title && <div style={{color:'#f2f3f5', fontWeight:600, fontSize:15, marginBottom:6, wordBreak:'break-word'}}>{title}</div>}
                    {description && <div style={{color:'#dbdee1', fontSize:13.5, whiteSpace:'pre-wrap', wordBreak:'break-word', lineHeight:1.4}}>{description}</div>}
                  </div>
                  {thumbUrl && <img src={thumbUrl} alt="" style={{width:64, height:64, borderRadius:6, objectFit:'cover', flexShrink:0}} onError={e => e.target.style.display='none'}/>}
                </div>
                {imageUrl && <img src={imageUrl} alt="" style={{marginTop:10, maxWidth:'100%', borderRadius:6, display:'block'}} onError={e => e.target.style.display='none'}/>}
                {(footer || stamp) && (
                  <div style={{color:'#949ba4', fontSize:11.5, marginTop:8}}>
                    {footer}{footer && stamp ? ' • ' : ''}{stamp ? "aujourd'hui" : ''}
                  </div>
                )}
                {!title && !description && <div style={{color:'#949ba4', fontSize:13}}>L'aperçu s'affiche ici…</div>}
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

// ───────────────────────────────────────────────────────────────
// TabConfigEditable — édite channels + rôles avec dropdowns Discord
function TabConfigEditable() {
  const [config, setConfig] = useAB(undefined);
  const [discord, setDiscord] = useAB(undefined);

  const reload = () => {
    fetch('/api/admin/bot/config-editable', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null).then(d => setConfig(d?.keys || []))
      .catch(() => setConfig([]));
    fetch('/api/admin/bot/discord-state', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null).then(setDiscord)
      .catch(() => setDiscord({ available: false, channels: [], roles: [] }));
  };
  useEAB(() => { reload(); }, []);

  if (config === undefined || discord === undefined) {
    return <div style={{padding:30, textAlign:'center', color:'var(--ink-mute)'}}>Chargement…</div>;
  }

  const channels = (discord?.channels || []).filter(c =>
    c.type === 0 /* GuildText */ || c.type === 15 /* GuildForum */ || c.type === 5 /* GuildAnnouncement */
  );
  const roles = discord?.roles || [];

  return (
    <div className="stack gap-5">
      {!discord?.available && (
        <div className="card" style={{padding:14, borderColor:'var(--mg-gold-500)', background:'rgba(240,182,87,0.06)'}}>
          ⚠️ Le bot n'a pas encore sync l'état du serveur Discord. Va sur l'onglet <b>Triggers</b> et lance <code>sync_guild_state</code> ou attends que le bot démarre (sync auto au boot).
        </div>
      )}

      <section className="card" style={{padding:20}}>
        <h2 style={{fontSize:20, marginBottom:8}}>⚙️ Canaux Discord</h2>
        <p className="soft" style={{fontSize:12, marginBottom:14}}>
          Override le .env. Une fois sauvé, le bot recharge sa config dans les 5s.
        </p>
        <div className="stack gap-3">
          {config.filter(c => c.kind === 'channel').map(c => (
            <ConfigEditableRow key={c.key} cfg={c} options={channels} optionLabel={ch => `#${ch.name}`} onChange={reload}/>
          ))}
        </div>
      </section>

      <section className="card" style={{padding:20}}>
        <h2 style={{fontSize:20, marginBottom:8}}>🎭 Rôles Discord</h2>
        <div className="stack gap-3">
          {config.filter(c => c.kind === 'role').map(c => (
            <ConfigEditableRow key={c.key} cfg={c} options={roles} optionLabel={r => r.name} onChange={reload}/>
          ))}
        </div>
      </section>
    </div>
  );
}

function ConfigEditableRow({ cfg, options, optionLabel, onChange }) {
  const [editing, setEditing] = useAB(false);
  const [value, setValue]     = useAB(cfg.override_value || cfg.env_value || '');
  const [saving, setSaving]   = useAB(false);

  const effective = cfg.override_value || cfg.env_value || '';
  const hasOverride = !!cfg.override_value;
  const matchingOption = options.find(o => o.id === effective);

  const save = async () => {
    setSaving(true);
    try {
      const r = await fetch('/api/admin/bot/config/' + encodeURIComponent(cfg.key), {
        method: 'PUT', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ value }),
      });
      if (!r.ok) throw new Error('Save failed');
      mgToast('Sauvegardé · bot reload dans 5s', { type: 'success' });
      setEditing(false);
      onChange?.();
    } catch (e) { mgToast(e.message, { type: 'error' }); }
    finally { setSaving(false); }
  };

  const resetToEnv = async () => {
    if (!(await mgConfirm('Supprimer l\'override et revenir au .env ?'))) return;
    await fetch('/api/admin/bot/config/' + encodeURIComponent(cfg.key), {
      method: 'DELETE', credentials: 'same-origin',
    });
    mgToast('Override retiré', { type: 'success' });
    setEditing(false);
    onChange?.();
  };

  return (
    <div style={{padding:'12px 14px', background:'var(--surface-2)', borderRadius:'var(--r-sm)', borderLeft: '3px solid ' + (hasOverride ? 'var(--accent)' : 'var(--border)')}}>
      <div className="row gap-2" style={{justifyContent:'space-between', alignItems:'baseline', marginBottom:6}}>
        <div>
          <span style={{fontWeight:600, fontSize:14}}>{cfg.label}</span>
          <span className="mono" style={{fontSize:10, color:'var(--ink-mute)', marginLeft:8}}>{cfg.key}</span>
        </div>
        {!editing && (
          <div className="row gap-2">
            <button className="btn btn-ghost btn-sm" onClick={() => setEditing(true)}>✏️ Modifier</button>
            {hasOverride && <button className="btn btn-ghost btn-sm" onClick={resetToEnv} style={{color:'var(--mg-danger)'}}>↻ Reset</button>}
          </div>
        )}
      </div>

      {!editing ? (
        <div className="row gap-2" style={{alignItems:'baseline'}}>
          <span className="mono" style={{fontSize:12, color: effective ? 'var(--ink)' : 'var(--ink-mute)'}}>
            {matchingOption ? optionLabel(matchingOption) : effective || '— (non défini)'}
          </span>
          {hasOverride && <span className="badge badge-gold" style={{fontSize:10}}>override</span>}
          {!hasOverride && effective && <span style={{fontSize:10, color:'var(--ink-mute)'}}>(via .env)</span>}
          {matchingOption && <span className="mono" style={{fontSize:10, color:'var(--ink-mute)'}}>id={effective}</span>}
        </div>
      ) : (
        <div className="stack gap-2">
          <select value={value} onChange={e => setValue(e.target.value)} style={inputStyle}>
            <option value="">— Vide (ou tape un ID en dessous) —</option>
            {options.map(o => (
              <option key={o.id} value={o.id}>{optionLabel(o)} ({o.id})</option>
            ))}
          </select>
          <input value={value} onChange={e => setValue(e.target.value)} placeholder="Ou tape un ID Discord (snowflake)" style={{...inputStyle, fontSize:12}}/>
          <div className="row gap-2">
            <button className="btn btn-gold btn-sm" disabled={saving} onClick={save}>{saving ? '…' : '💾 Sauver'}</button>
            <button className="btn btn-ghost btn-sm" onClick={() => { setEditing(false); setValue(cfg.override_value || cfg.env_value || ''); }}>Annuler</button>
          </div>
        </div>
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────────────
// TabWelcome — édite les 2 templates avec preview
function TabWelcome() {
  const [data, setData] = useAB(undefined);
  const [arrival, setArrival] = useAB('');
  const [recrue, setRecrue]   = useAB('');
  const [saving, setSaving]   = useAB(false);

  useEAB(() => {
    fetch('/api/admin/bot/welcome', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null).then(d => {
        setData(d);
        if (d) {
          setArrival(d.arrival || d.arrival_default || '');
          setRecrue(d.recrue || d.recrue_default || '');
        }
      });
  }, []);

  if (data === undefined) return <div style={{padding:30, textAlign:'center', color:'var(--ink-mute)'}}>Chargement…</div>;
  if (!data) return <div style={{padding:30, textAlign:'center', color:'var(--mg-danger)'}}>Erreur</div>;

  const save = async () => {
    setSaving(true);
    try {
      await fetch('/api/admin/bot/welcome', {
        method: 'PUT', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ arrival, recrue }),
      });
      mgToast('Templates sauvés', { type: 'success' });
    } catch (e) { mgToast(e.message, { type: 'error' }); }
    finally { setSaving(false); }
  };

  return (
    <div className="stack gap-5">
      <section className="card" style={{padding:20}}>
        <h2 style={{fontSize:20, marginBottom:8}}>👋 Message de bienvenue Discord (#bienvenue)</h2>
        <p className="soft" style={{fontSize:12, marginBottom:8}}>
          Posté à chaque nouvelle arrivée sur le serveur. Placeholders : <code>{'{name}'}</code> (pseudo Discord) · <code>{'{ticket_channel}'}</code> (mention de #ticket).
        </p>
        <textarea value={arrival} onChange={e => setArrival(e.target.value)} rows={14}
          style={{...inputStyle, fontFamily:'JetBrains Mono, monospace', fontSize:12, resize:'vertical'}}/>
        <div className="row gap-2" style={{marginTop:8, justifyContent:'space-between'}}>
          <button className="btn btn-ghost btn-sm" onClick={() => setArrival(data.arrival_default)}>↻ Reset au défaut</button>
          <span className="soft" style={{fontSize:11}}>{arrival.length} chars</span>
        </div>
      </section>

      <section className="card" style={{padding:20}}>
        <h2 style={{fontSize:20, marginBottom:8}}>🎖️ Message Recrue Mongolie (#accueil-recrue)</h2>
        <p className="soft" style={{fontSize:12, marginBottom:8}}>
          Posté quand quelqu'un est officiellement passé Recrue via /lier. Placeholders : <code>{'{name}'}</code> · <code>{'{pseudo}'}</code> (pseudo NG) · <code>{'{recruiter}'}</code> (parrain).
        </p>
        <textarea value={recrue} onChange={e => setRecrue(e.target.value)} rows={10}
          style={{...inputStyle, fontFamily:'JetBrains Mono, monospace', fontSize:12, resize:'vertical'}}/>
        <div className="row gap-2" style={{marginTop:8, justifyContent:'space-between'}}>
          <button className="btn btn-ghost btn-sm" onClick={() => setRecrue(data.recrue_default)}>↻ Reset au défaut</button>
          <span className="soft" style={{fontSize:11}}>{recrue.length} chars</span>
        </div>
      </section>

      <div>
        <button className="btn btn-gold" disabled={saving} onClick={save} style={{width:'100%', padding:'14px'}}>
          {saving ? '…' : '💾 Sauvegarder les 2 templates'}
        </button>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────────
// TabSetup — bouton re-run perfect-discord.js + diagnostic
function TabSetup() {
  const sync = async () => {
    if (!(await mgConfirm('Sync l\'état du serveur Discord (channels + rôles) ? Utile pour rafraîchir les dropdowns de l\'onglet Channels & Rôles.'))) return;
    try {
      await fetch('/api/admin/bot/trigger/sync_guild_state', { method: 'POST', credentials: 'same-origin' });
      mgToast('Sync lancé · check dans 10s', { type: 'success' });
    } catch (e) { mgToast(e.message, { type: 'error' }); }
  };

  const reload = async () => {
    try {
      await fetch('/api/admin/bot/trigger/reload_config', { method: 'POST', credentials: 'same-origin' });
      mgToast('Bot va reload sa config dans 5s', { type: 'success' });
    } catch (e) { mgToast(e.message, { type: 'error' }); }
  };

  return (
    <div className="stack gap-5">
      <section className="card" style={{padding:20}}>
        <h2 style={{fontSize:20, marginBottom:14}}>🔧 Setup & maintenance</h2>

        <div className="stack gap-3">
          <SetupAction
            title="Resync état Discord"
            desc="Re-fetch tous les channels + rôles du serveur Discord pour rafraîchir les dropdowns. À faire si tu viens d'ajouter/renommer un canal IG sur Discord."
            label="🔄 Resync"
            onClick={sync}
          />
          <SetupAction
            title="Reload config bot"
            desc="Force le bot à relire sa config depuis la DB (override .env). Normalement fait automatiquement après chaque sauvegarde de Channels/Rôles."
            label="♻️ Reload"
            onClick={reload}
          />
          <SetupAction
            title="Re-run perfect-discord.js"
            desc="Relance le setup complet du serveur Discord : création/rename catégories, canaux, rôles, perms intelligentes. Idempotent — peut être lancé sans risque."
            label="🏗 Lancer le setup (depuis SSH)"
            disabled
            help="Pour l'instant à lancer en SSH : docker compose run --rm mongolie-bot node bot/perfect-discord.js"
          />
        </div>
      </section>

      <section className="card" style={{padding:20}}>
        <h2 style={{fontSize:20, marginBottom:14}}>📋 Slash commands déployées</h2>
        <p className="soft" style={{fontSize:13}}>
          Les commandes slash sont déployées via <code>docker compose run --rm mongolie-bot node bot/deploy-commands.js</code>.
          À relancer après chaque ajout/modification de commande.
        </p>
        <div className="soft" style={{fontSize:12, marginTop:10}}>
          Commandes connues : <code>/lier</code> <code>/profil</code> <code>/parcours</code> <code>/lignee</code> <code>/khanat</code> <code>/online</code> <code>/joueur</code> <code>/pays</code> <code>/item</code> <code>/armure</code> <code>/skill</code> <code>/classement</code> <code>/ambassadeurs</code> <code>/pretendants</code> <code>/filleuls</code> <code>/plan</code> <code>/compagnons</code> <code>/mes-togrogs</code> <code>/mes-quetes</code> <code>/quetes</code> <code>/quete</code> <code>/abandonner</code> <code>/bazar</code> <code>/defi</code> <code>/ping</code> <code>/intel</code> <code>/guerre</code> + officier : <code>/valider-quete</code> <code>/inactifs</code> <code>/nouveaux</code> <code>/rapport</code> <code>/chantiers-refresh</code> <code>/ticket-panel</code> <code>/ticket-*</code>
        </div>
      </section>
    </div>
  );
}

function SetupAction({ title, desc, label, onClick, disabled, help }) {
  return (
    <div style={{padding:14, background:'var(--surface-2)', borderRadius:'var(--r-sm)'}}>
      <div className="row gap-2" style={{justifyContent:'space-between', alignItems:'flex-start', flexWrap:'wrap'}}>
        <div style={{flex:1, minWidth:200}}>
          <div style={{fontWeight:700, fontSize:14}}>{title}</div>
          <div className="soft" style={{fontSize:12, marginTop:4}}>{desc}</div>
          {help && <div className="mono" style={{fontSize:11, marginTop:6, color:'var(--ink-mute)', background:'var(--bg)', padding:'6px 10px', borderRadius:4}}>{help}</div>}
        </div>
        <button className={'btn btn-sm ' + (disabled ? 'btn-ghost' : 'btn-gold')} disabled={disabled} onClick={onClick}>
          {label}
        </button>
      </div>
    </div>
  );
}

const inputStyle = {
  width:'100%', padding:'10px 12px', borderRadius:'var(--r-sm)',
  border:'1px solid var(--border)', background:'var(--surface-1)', color:'var(--ink)', fontSize:14,
};

// ───────────────────────────────────────────────────────────────
// TabFeatures — enable/disable toutes les tâches scheduler + auto-annonces
function TabFeatures() {
  const [data, setData] = useAB(undefined);

  const reload = () => {
    fetch('/api/admin/bot/features', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null).then(d => setData(d?.features || []));
  };
  useEAB(() => { reload(); }, []);

  const toggle = async (key, enabled) => {
    await fetch('/api/admin/bot/feature/' + encodeURIComponent(key), {
      method: 'PUT', credentials: 'same-origin',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ enabled }),
    });
    mgToast(enabled ? '✅ Activé' : '⏸ Désactivé', { type: 'success' });
    reload();
  };

  if (!data) return <div style={{padding:30, textAlign:'center', color:'var(--ink-mute)'}}>Chargement…</div>;

  const scheduler = data.filter(f => f.scope === 'scheduler');
  const autoann   = data.filter(f => f.scope === 'autoann');

  return (
    <div className="stack gap-5">
      <section className="card" style={{padding:20}}>
        <h2 style={{fontSize:20, marginBottom:8}}>🎚️ Tâches scheduler</h2>
        <p className="soft" style={{fontSize:12, marginBottom:14}}>
          Désactiver une task = elle ne tournera plus. Le bouton "Lancer maintenant" du Triggers ne marchera pas non plus pour une task désactivée.
        </p>
        <div className="stack gap-2">
          {scheduler.map(f => <FeatureRow key={f.key} f={f} onToggle={toggle}/>)}
        </div>
      </section>

      <section className="card" style={{padding:20}}>
        <h2 style={{fontSize:20, marginBottom:8}}>📢 Auto-annonces Discord</h2>
        <p className="soft" style={{fontSize:12, marginBottom:14}}>
          Désactiver une annonce = elle ne sera plus postée sur Discord, mais l'event sera quand même enregistré (notifs site continuent).
        </p>
        <div className="stack gap-2">
          {autoann.map(f => <FeatureRow key={f.key} f={f} onToggle={toggle}/>)}
        </div>
      </section>
    </div>
  );
}

function FeatureRow({ f, onToggle }) {
  return (
    <div className="row gap-3" style={{
      justifyContent:'space-between', alignItems:'center',
      padding:'10px 14px', background:'var(--surface-2)', borderRadius:'var(--r-sm)',
      borderLeft: '3px solid ' + (f.enabled ? 'var(--mg-success)' : 'var(--ink-mute)'),
    }}>
      <div>
        <div style={{fontWeight:600, fontSize:13}}>{f.label}</div>
        <div className="mono" style={{fontSize:10, color:'var(--ink-mute)'}}>{f.key}</div>
      </div>
      <button
        onClick={() => onToggle(f.key, !f.enabled)}
        className={'btn btn-sm ' + (f.enabled ? '' : 'btn-ghost')}
        style={f.enabled ? {background:'var(--mg-success)', color:'#fff', minWidth:100} : {minWidth:100}}
      >
        {f.enabled ? '✅ Activé' : '⏸ Désactivé'}
      </button>
    </div>
  );
}

// ───────────────────────────────────────────────────────────────
// TabThresholds — édite les valeurs numériques tunables
function TabThresholds() {
  const [data, setData] = useAB(undefined);

  const reload = () => {
    fetch('/api/admin/bot/thresholds', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null).then(d => setData(d?.thresholds || []));
  };
  useEAB(() => { reload(); }, []);

  if (!data) return <div style={{padding:30, textAlign:'center', color:'var(--ink-mute)'}}>Chargement…</div>;

  return (
    <section className="card" style={{padding:20}}>
      <h2 style={{fontSize:20, marginBottom:8}}>🔢 Seuils & limites</h2>
      <p className="soft" style={{fontSize:12, marginBottom:14}}>
        Override la valeur par défaut. Les helpers DB lisent ces seuils en live, pas besoin de restart.
      </p>
      <div className="stack gap-3">
        {data.map(t => <ThresholdRow key={t.key} t={t} onChange={reload}/>)}
      </div>
    </section>
  );
}

function ThresholdRow({ t, onChange }) {
  const [editing, setEditing] = useAB(false);
  const [value, setValue]     = useAB(t.value);
  const [saving, setSaving]   = useAB(false);

  const save = async () => {
    setSaving(true);
    try {
      await fetch('/api/admin/bot/threshold/' + encodeURIComponent(t.key), {
        method: 'PUT', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ value: Number(value) }),
      });
      mgToast('Sauvegardé', { type: 'success' });
      setEditing(false);
      onChange?.();
    } catch (e) { mgToast(e.message, { type: 'error' }); }
    finally { setSaving(false); }
  };

  const reset = async () => {
    if (!(await mgConfirm('Revenir à la valeur par défaut (' + t.default + ') ?'))) return;
    await fetch('/api/admin/bot/threshold/' + encodeURIComponent(t.key), {
      method: 'DELETE', credentials: 'same-origin',
    });
    mgToast('Reset', { type: 'success' });
    setEditing(false);
    onChange?.();
  };

  return (
    <div style={{padding:'12px 14px', background:'var(--surface-2)', borderRadius:'var(--r-sm)', borderLeft: '3px solid ' + (t.is_override ? 'var(--accent)' : 'var(--border)')}}>
      <div className="row gap-2" style={{justifyContent:'space-between', alignItems:'baseline', marginBottom:6}}>
        <div>
          <span style={{fontWeight:600, fontSize:14}}>{t.label}</span>
          {t.hint && <span className="soft" style={{fontSize:11, marginLeft:8}}>{t.hint}</span>}
          {t.is_override && <span className="badge badge-gold" style={{fontSize:10, marginLeft:8}}>override</span>}
        </div>
        {!editing && (
          <div className="row gap-2">
            <span className="mono" style={{fontSize:13, fontWeight:700, color:'var(--accent)'}}>{t.value}</span>
            <span className="soft" style={{fontSize:11}}>(défaut {t.default})</span>
            <button className="btn btn-ghost btn-sm" onClick={() => setEditing(true)}>✏️</button>
            {t.is_override && <button className="btn btn-ghost btn-sm" onClick={reset} style={{color:'var(--mg-danger)'}}>↻</button>}
          </div>
        )}
      </div>
      {editing && (
        <div className="row gap-2">
          <input type="number" value={value} onChange={e => setValue(e.target.value)} style={{...inputStyle, width:120}}/>
          <button className="btn btn-gold btn-sm" disabled={saving} onClick={save}>{saving ? '…' : '💾'}</button>
          <button className="btn btn-ghost btn-sm" onClick={() => { setEditing(false); setValue(t.value); }}>Annuler</button>
        </div>
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────────────
// TabTemplates — override le titre + body des auto-annonces par type
function TabTemplates() {
  const [data, setData] = useAB(undefined);

  const reload = () => {
    fetch('/api/admin/bot/templates', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null).then(d => setData(d?.types || []));
  };
  useEAB(() => { reload(); }, []);

  if (!data) return <div style={{padding:30, textAlign:'center', color:'var(--ink-mute)'}}>Chargement…</div>;

  return (
    <section className="card" style={{padding:20}}>
      <h2 style={{fontSize:20, marginBottom:8}}>📝 Templates auto-annonces</h2>
      <p className="soft" style={{fontSize:12, marginBottom:14}}>
        Override le <b>titre</b> et le <b>body</b> d'une auto-annonce. Si pas d'override, le texte par défaut codé en dur est utilisé.
        Placeholders : utilise <code>{'{key}'}</code> où key vient du payload de l'event
        (ex: <code>{'{player}'}</code>, <code>{'{country}'}</code>, <code>{'{togrog}'}</code>, etc.).
      </p>
      <div className="stack gap-2">
        {data.map(t => <TemplateRow key={t.type} t={t} onChange={reload}/>)}
      </div>
    </section>
  );
}

function TemplateRow({ t, onChange }) {
  const [editing, setEditing] = useAB(false);
  // Pré-fill avec override OU default. Pas vide.
  const [title, setTitle] = useAB(t.override?.title || t.default_title || '');
  const [body, setBody]   = useAB(t.override?.body  || t.default_body  || '');
  const [saving, setSaving] = useAB(false);

  const save = async () => {
    setSaving(true);
    try {
      // Si la valeur est identique au default, on l'envoie quand même (l'admin
      // veut explicitement "verrouiller" ce texte). Pour reset au default,
      // utiliser le bouton ↻ Reset.
      await fetch('/api/admin/bot/template/' + encodeURIComponent(t.type), {
        method: 'PUT', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title, body }),
      });
      mgToast('Template sauvé', { type: 'success' });
      setEditing(false);
      onChange?.();
    } catch (e) { mgToast(e.message, { type: 'error' }); }
    finally { setSaving(false); }
  };

  const reset = async () => {
    if (!(await mgConfirm('Supprimer l\'override et revenir au texte par défaut ?'))) return;
    await fetch('/api/admin/bot/template/' + encodeURIComponent(t.type), {
      method: 'DELETE', credentials: 'same-origin',
    });
    mgToast('Override retiré · défaut restauré', { type: 'success' });
    setEditing(false);
    setTitle(t.default_title || '');
    setBody(t.default_body || '');
    onChange?.();
  };

  // Preview rendu avec sample payload
  const samplePayload = t.sample_payload || {};
  const renderedTitle = String(title || '').replace(/\{(\w+)\}/g, (_, k) => samplePayload[k] ?? '?');
  const renderedBody  = String(body || '').replace(/\{(\w+)\}/g, (_, k) => samplePayload[k] ?? '?');

  const hasOverride = !!t.override;
  return (
    <div style={{padding:'12px 14px', background:'var(--surface-2)', borderRadius:'var(--r-sm)', borderLeft: '3px solid ' + (hasOverride ? 'var(--accent)' : 'var(--border)')}}>
      <div className="row gap-2" style={{justifyContent:'space-between', alignItems:'baseline', marginBottom:6}}>
        <div>
          <span style={{fontWeight:600, fontSize:13}}>{t.label}</span>
          <span className="mono" style={{fontSize:10, color:'var(--ink-mute)', marginLeft:8}}>{t.type}</span>
          {hasOverride && <span className="badge badge-gold" style={{fontSize:10, marginLeft:8}}>override</span>}
        </div>
        <div className="row gap-2">
          {!editing && <button className="btn btn-ghost btn-sm" onClick={() => setEditing(true)}>{hasOverride ? '✏️ Modifier' : '✏️ Customiser'}</button>}
          {hasOverride && <button className="btn btn-ghost btn-sm" onClick={reset} style={{color:'var(--mg-danger)'}}>↻ Reset</button>}
        </div>
      </div>

      {!editing && (
        <div style={{padding:'8px 12px', background:'var(--bg)', borderRadius:'var(--r-sm)', borderLeft:'3px solid var(--mg-gold-700)'}}>
          <div className="eyebrow" style={{fontSize:9, color:'var(--ink-mute)', marginBottom:4}}>Preview (avec sample)</div>
          <div style={{fontWeight:700, fontSize:13, color:'var(--accent)'}}>{renderedTitle}</div>
          <div style={{fontSize:12, color:'var(--ink-soft)', whiteSpace:'pre-wrap', marginTop:4}}>{renderedBody}</div>
        </div>
      )}

      {editing && (
        <div className="stack gap-2" style={{marginTop:8}}>
          <div>
            <label className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)', display:'block', marginBottom:4}}>Titre</label>
            <input value={title} onChange={e => setTitle(e.target.value)} placeholder={t.default_title || 'Titre…'} style={inputStyle}/>
          </div>
          <div>
            <label className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)', display:'block', marginBottom:4}}>Body Markdown</label>
            <textarea value={body} onChange={e => setBody(e.target.value)} rows={5} placeholder={t.default_body || 'Body…'}
              style={{...inputStyle, fontFamily:'inherit', resize:'vertical'}}/>
          </div>

          <div style={{padding:'8px 12px', background:'var(--bg)', borderRadius:'var(--r-sm)', borderLeft:'3px solid var(--mg-gold-700)'}}>
            <div className="eyebrow" style={{fontSize:9, color:'var(--ink-mute)', marginBottom:4}}>Preview live (avec sample {Object.keys(samplePayload).join(', ')})</div>
            <div style={{fontWeight:700, fontSize:13, color:'var(--accent)'}}>{renderedTitle}</div>
            <div style={{fontSize:12, color:'var(--ink-soft)', whiteSpace:'pre-wrap', marginTop:4}}>{renderedBody}</div>
          </div>

          <div className="row gap-2">
            <button className="btn btn-gold btn-sm" disabled={saving} onClick={save}>{saving ? '…' : '💾 Sauver'}</button>
            <button className="btn btn-ghost btn-sm" onClick={() => { setEditing(false); setTitle(t.override?.title || t.default_title || ''); setBody(t.override?.body || t.default_body || ''); }}>Annuler</button>
          </div>
        </div>
      )}
    </div>
  );
}

// ═══════════════════════════════════════════════════════════════
// TabDiscordTree — explorateur complet read-only des channels + rôles
// ═══════════════════════════════════════════════════════════════

const CHANNEL_TYPE = {
  0:  { label: 'Text',         icon: '💬' },
  2:  { label: 'Voice',        icon: '🔊' },
  4:  { label: 'Category',     icon: '📁' },
  5:  { label: 'Announcement', icon: '📣' },
  13: { label: 'Stage',        icon: '🎤' },
  15: { label: 'Forum',        icon: '🗂️' },
};

// Permission Discord → label FR + emoji catégorie
const PERM_META = {
  // General
  ViewChannel:        { fr: 'Voir le canal',         cat: 'general' },
  ManageChannels:     { fr: 'Gérer les canaux',      cat: 'general' },
  ManageRoles:        { fr: 'Gérer les rôles',       cat: 'general' },
  ManageGuild:        { fr: 'Gérer le serveur',      cat: 'general' },
  CreateInstantInvite:{ fr: 'Créer une invitation',  cat: 'general' },
  ChangeNickname:     { fr: 'Changer son pseudo',    cat: 'general' },
  ManageNicknames:    { fr: 'Gérer les pseudos',     cat: 'general' },
  ManageWebhooks:     { fr: 'Gérer webhooks',        cat: 'general' },
  ViewAuditLog:       { fr: 'Voir audit log',        cat: 'general' },
  // Text
  SendMessages:       { fr: 'Envoyer messages',      cat: 'text' },
  SendMessagesInThreads: { fr: 'Envoyer dans threads', cat: 'text' },
  CreatePublicThreads:{ fr: 'Créer thread public',   cat: 'text' },
  CreatePrivateThreads:{ fr:'Créer thread privé',    cat: 'text' },
  EmbedLinks:         { fr: 'Liens enrichis',        cat: 'text' },
  AttachFiles:        { fr: 'Joindre fichiers',      cat: 'text' },
  AddReactions:       { fr: 'Ajouter réactions',     cat: 'text' },
  UseExternalEmojis:  { fr: 'Emojis externes',       cat: 'text' },
  MentionEveryone:    { fr: 'Mention @everyone',     cat: 'text' },
  ManageMessages:     { fr: 'Gérer messages',        cat: 'text' },
  ManageThreads:      { fr: 'Gérer threads',         cat: 'text' },
  ReadMessageHistory: { fr: 'Voir historique',       cat: 'text' },
  UseApplicationCommands: { fr: 'Slash commands',    cat: 'text' },
  // Voice
  Connect:            { fr: 'Rejoindre vocal',       cat: 'voice' },
  Speak:              { fr: 'Parler',                cat: 'voice' },
  Stream:             { fr: 'Vidéo / partage',       cat: 'voice' },
  UseVAD:             { fr: 'Voice activity',        cat: 'voice' },
  PrioritySpeaker:    { fr: 'Prioritaire',           cat: 'voice' },
  MuteMembers:        { fr: 'Mute membres',          cat: 'voice' },
  DeafenMembers:      { fr: 'Sourdine membres',      cat: 'voice' },
  MoveMembers:        { fr: 'Déplacer membres',      cat: 'voice' },
  // Moderation
  KickMembers:        { fr: 'Kick',                  cat: 'mod' },
  BanMembers:         { fr: 'Ban',                   cat: 'mod' },
  ModerateMembers:    { fr: 'Time-out',              cat: 'mod' },
  Administrator:      { fr: '⚡ Administrateur',     cat: 'mod' },
};

const CAT_LABEL = { general: 'Général', text: 'Textuel', voice: 'Vocal', mod: 'Modération' };

function TabDiscordTree() {
  const [state, setState] = useAB(undefined);
  const [view, setView]   = useAB('channels');  // 'channels' | 'roles'
  const [selected, setSelected] = useAB(null);  // { kind: 'channel'|'role', id }

  const reload = () => {
    fetch('/api/admin/bot/discord-state', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null).then(setState);
  };
  useEAB(() => { reload(); }, []);

  if (state === undefined) return <div style={{padding:30, textAlign:'center', color:'var(--ink-mute)'}}>Chargement…</div>;
  if (!state || !state.available) return (
    <section className="card" style={{padding:30, textAlign:'center'}}>
      <div style={{fontSize:36, opacity:0.4}}>🤖</div>
      <div style={{marginTop:8, fontWeight:600}}>État Discord non disponible</div>
      <p className="soft" style={{fontSize:13, marginTop:6}}>
        Va sur l'onglet <b>Setup</b> et clique sur "Resync état Discord", puis reviens ici.
      </p>
    </section>
  );

  const channels = state.channels || [];
  const roles    = state.roles    || [];

  return (
    <div className="stack gap-4">
      <section className="card" style={{padding:'12px 16px'}}>
        <div className="row gap-3" style={{justifyContent:'space-between', alignItems:'center', flexWrap:'wrap'}}>
          <div>
            <span style={{fontWeight:700}}>🏰 {state.guild_name}</span>
            <span className="soft" style={{fontSize:12, marginLeft:8}}>· {state.member_count} membres · {channels.length} canaux · {roles.length} rôles</span>
          </div>
          <div className="row gap-2">
            <span className="soft" style={{fontSize:11}}>Sync : {fmtRelTime(state.synced_at)} ago</span>
            <button className="btn btn-ghost btn-sm" onClick={reload}>🔄</button>
          </div>
        </div>
      </section>

      <div className="row gap-2">
        <button className={'btn btn-sm ' + (view === 'channels' ? 'btn-gold' : 'btn-ghost')} onClick={() => { setView('channels'); setSelected(null); }}>
          💬 Canaux ({channels.length})
        </button>
        <button className={'btn btn-sm ' + (view === 'roles' ? 'btn-gold' : 'btn-ghost')} onClick={() => { setView('roles'); setSelected(null); }}>
          🎭 Rôles ({roles.length})
        </button>
      </div>

      <div style={{display:'grid', gridTemplateColumns:'minmax(280px, 1fr) 2fr', gap:14}}>
        {/* Liste à gauche */}
        <div style={{maxHeight:'70vh', overflowY:'auto', paddingRight:6}}>
          {view === 'channels'
            ? <ChannelTree channels={channels} selected={selected} onSelect={setSelected}/>
            : <RoleList roles={roles} selected={selected} onSelect={setSelected}/>
          }
        </div>
        {/* Détails à droite */}
        <div style={{maxHeight:'70vh', overflowY:'auto', paddingLeft:6}}>
          {!selected ? (
            <div className="soft" style={{padding:40, textAlign:'center', fontSize:13}}>
              ← Sélectionne un {view === 'channels' ? 'canal' : 'rôle'} pour voir les détails
            </div>
          ) : selected.kind === 'channel' ? (
            <ChannelDetail channel={channels.find(c => c.id === selected.id)} roles={roles} allChannels={channels}/>
          ) : (
            <RoleDetail role={roles.find(r => r.id === selected.id)} channels={channels}/>
          )}
        </div>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────
function ChannelTree({ channels, selected, onSelect }) {
  // Group by category. Catégories = type=4, channels = type≠4
  const cats = channels.filter(c => c.type === 4).sort((a,b) => a.position - b.position);
  const noCats = channels.filter(c => c.type !== 4 && !c.parent_id);
  const byParent = channels.filter(c => c.type !== 4 && c.parent_id).reduce((a, c) => {
    (a[c.parent_id] = a[c.parent_id] || []).push(c);
    return a;
  }, {});
  Object.values(byParent).forEach(arr => arr.sort((a,b) => a.position - b.position));

  return (
    <div className="stack gap-1">
      {noCats.length > 0 && (
        <div style={{marginBottom:8}}>
          <div className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)', padding:'4px 8px'}}>Hors catégorie</div>
          {noCats.map(c => <ChannelRow key={c.id} c={c} selected={selected} onSelect={onSelect}/>)}
        </div>
      )}
      {cats.map(cat => (
        <div key={cat.id} style={{marginBottom:8}}>
          <div className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)', padding:'4px 8px', display:'flex', alignItems:'center', gap:4}}>
            📁 {cat.name}
            <button onClick={() => onSelect({ kind: 'channel', id: cat.id })} style={{
              marginLeft:'auto', background:'transparent', border:'none', color:'var(--accent)', cursor:'pointer', fontSize:10,
            }}>détails</button>
          </div>
          {(byParent[cat.id] || []).map(c => <ChannelRow key={c.id} c={c} selected={selected} onSelect={onSelect}/>)}
        </div>
      ))}
    </div>
  );
}

function ChannelRow({ c, selected, onSelect }) {
  const isSel = selected?.kind === 'channel' && selected.id === c.id;
  const meta = CHANNEL_TYPE[c.type] || { label: 'Type ' + c.type, icon: '?' };
  return (
    <div
      onClick={() => onSelect({ kind: 'channel', id: c.id })}
      style={{
        padding:'6px 10px', borderRadius:6, cursor:'pointer', marginBottom:1,
        background: isSel ? 'rgba(227,177,90,0.15)' : 'transparent',
        borderLeft: '3px solid ' + (isSel ? 'var(--accent)' : 'transparent'),
        display:'flex', alignItems:'center', gap:8,
      }}
      onMouseEnter={e => { if (!isSel) e.currentTarget.style.background = 'var(--surface-2)'; }}
      onMouseLeave={e => { if (!isSel) e.currentTarget.style.background = 'transparent'; }}
    >
      <span style={{fontSize:14}}>{meta.icon}</span>
      <span style={{fontSize:13, color: isSel ? 'var(--accent)' : 'var(--ink)', fontWeight: isSel ? 600 : 400}}>{c.name}</span>
      {c.overwrites?.length > 0 && (
        <span style={{marginLeft:'auto', fontSize:10, color:'var(--ink-mute)'}}>{c.overwrites.length}</span>
      )}
    </div>
  );
}

function RoleList({ roles, selected, onSelect }) {
  return (
    <div className="stack gap-1">
      {roles.map(r => {
        const isSel = selected?.kind === 'role' && selected.id === r.id;
        const color = r.color === '#000000' ? 'var(--ink-mute)' : r.color;
        return (
          <div
            key={r.id}
            onClick={() => onSelect({ kind: 'role', id: r.id })}
            style={{
              padding:'8px 10px', borderRadius:6, cursor:'pointer', marginBottom:1,
              background: isSel ? 'rgba(227,177,90,0.15)' : 'transparent',
              borderLeft: '3px solid ' + (isSel ? 'var(--accent)' : (r.color !== '#000000' ? color : 'transparent')),
              display:'flex', alignItems:'center', gap:8,
            }}
            onMouseEnter={e => { if (!isSel) e.currentTarget.style.background = 'var(--surface-2)'; }}
            onMouseLeave={e => { if (!isSel) e.currentTarget.style.background = 'transparent'; }}
          >
            <span style={{
              width:10, height:10, borderRadius:'50%',
              background: r.color !== '#000000' ? color : 'var(--ink-mute)',
            }}/>
            <span style={{fontSize:13, color: isSel ? 'var(--accent)' : 'var(--ink)', fontWeight: isSel ? 600 : 400}}>
              {r.is_everyone ? '@everyone' : r.name}
            </span>
            <span style={{marginLeft:'auto', fontSize:10, color:'var(--ink-mute)'}}>
              {r.member_count} {r.hoist ? '· hoist' : ''} {r.is_managed ? '· bot' : ''}
            </span>
          </div>
        );
      })}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────
function ChannelDetail({ channel, roles, allChannels }) {
  if (!channel) return null;
  const meta = CHANNEL_TYPE[channel.type] || { label: 'Type ' + channel.type, icon: '?' };
  const rolesById = new Map(roles.map(r => [r.id, r]));
  const [editing, setEditing] = useAB(false);
  const [draft, setDraft] = useAB({
    name: channel.name, topic: channel.topic || '',
    nsfw: !!channel.nsfw, parent_id: channel.parent_id || '',
    position: channel.position,
  });
  const [saving, setSaving] = useAB(false);

  // Reset draft quand on change de channel
  useEAB(() => {
    setDraft({
      name: channel.name, topic: channel.topic || '',
      nsfw: !!channel.nsfw, parent_id: channel.parent_id || '',
      position: channel.position,
    });
    setEditing(false);
  }, [channel.id]);

  const isCategory = channel.type === 4;
  const categories = (allChannels || []).filter(c => c.type === 4).sort((a,b) => a.position - b.position);

  const save = async () => {
    setSaving(true);
    try {
      const r = await fetch('/api/admin/bot/channel/' + channel.id, {
        method: 'PUT', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: draft.name,
          topic: isCategory ? undefined : draft.topic,
          nsfw: isCategory ? undefined : draft.nsfw,
          parent_id: isCategory ? undefined : (draft.parent_id || null),
          position: Number(draft.position),
        }),
      });
      if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.error || 'Erreur'); }
      mgToast('Édition enqueue · bot applique dans 5s puis resync auto', { type: 'success', duration: 5000 });
      setEditing(false);
    } catch (e) { mgToast(e.message, { type: 'error' }); }
    finally { setSaving(false); }
  };

  return (
    <div className="stack gap-4">
      <section className="card" style={{padding:18}}>
        <div className="row gap-3" style={{justifyContent:'space-between', alignItems:'flex-start', flexWrap:'wrap'}}>
          <div className="row gap-3" style={{alignItems:'baseline'}}>
            <span style={{fontSize:24}}>{meta.icon}</span>
            <div>
              <h3 style={{margin:0, fontSize:20}}>{channel.name}</h3>
              <div className="soft" style={{fontSize:12}}>{meta.label} · position {channel.position}</div>
            </div>
          </div>
          {!editing ? (
            <button className="btn btn-gold btn-sm" onClick={() => setEditing(true)}>✏️ Éditer</button>
          ) : (
            <div className="row gap-2">
              <button className="btn btn-gold btn-sm" disabled={saving} onClick={save}>{saving ? '…' : '💾 Sauver'}</button>
              <button className="btn btn-ghost btn-sm" onClick={() => setEditing(false)}>Annuler</button>
            </div>
          )}
        </div>

        {!editing ? (
          <>
            {channel.topic && (
              <div style={{marginTop:10, padding:'8px 12px', background:'var(--surface-2)', borderRadius:'var(--r-sm)', fontSize:13, color:'var(--ink-soft)'}}>
                {channel.topic}
              </div>
            )}
            <div className="row gap-3" style={{marginTop:10, fontSize:12, color:'var(--ink-mute)', flexWrap:'wrap'}}>
              <span className="mono">ID: {channel.id}</span>
              {channel.parent_id && <span>Parent: <code>{channel.parent_id}</code></span>}
              {channel.nsfw && <span style={{color:'var(--mg-danger)'}}>🔞 NSFW</span>}
            </div>
          </>
        ) : (
          <div className="stack gap-2" style={{marginTop:14}}>
            <div>
              <label className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)', display:'block', marginBottom:4}}>Nom du canal</label>
              <input value={draft.name} onChange={e => setDraft({...draft, name: e.target.value})} maxLength={100} style={inputStyle}/>
            </div>
            {!isCategory && (
              <div>
                <label className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)', display:'block', marginBottom:4}}>Topic (description)</label>
                <textarea value={draft.topic} onChange={e => setDraft({...draft, topic: e.target.value})} rows={2} maxLength={1024} style={{...inputStyle, fontFamily:'inherit'}}/>
              </div>
            )}
            <div className="row gap-2">
              {!isCategory && (
                <div style={{flex:2}}>
                  <label className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)', display:'block', marginBottom:4}}>Catégorie parente</label>
                  <select value={draft.parent_id} onChange={e => setDraft({...draft, parent_id: e.target.value})} style={inputStyle}>
                    <option value="">— Hors catégorie —</option>
                    {categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
                  </select>
                </div>
              )}
              <div style={{flex:1}}>
                <label className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)', display:'block', marginBottom:4}}>Position</label>
                <input type="number" value={draft.position} onChange={e => setDraft({...draft, position: e.target.value})} style={inputStyle}/>
              </div>
            </div>
            {!isCategory && (
              <label className="row gap-2" style={{alignItems:'center', cursor:'pointer'}}>
                <input type="checkbox" checked={draft.nsfw} onChange={e => setDraft({...draft, nsfw: e.target.checked})}/>
                <span style={{fontSize:13}}>🔞 NSFW</span>
              </label>
            )}
            <div className="soft" style={{fontSize:11, marginTop:4}}>
              ⚠️ L'édition se fait via le bot (5s). Pour modifier les <b>permissions</b>, utilise <code>perfect-discord.js</code> en SSH (policies cohérentes).
            </div>
          </div>
        )}
      </section>

      <section className="card" style={{padding:18}}>
        <h3 style={{margin:'0 0 14px', fontSize:16}}>🔐 Permissions overrides ({channel.overwrites.length})</h3>
        {channel.overwrites.length === 0 ? (
          <div className="soft" style={{fontSize:13, textAlign:'center', padding:14}}>
            Pas d'override — hérite des perms de la catégorie.
          </div>
        ) : (
          <div className="stack gap-2">
            {channel.overwrites.map(o => {
              const role = rolesById.get(o.id);
              const isEveryone = role?.is_everyone;
              const isMember = o.type === 1;
              return (
                <div key={o.id} style={{padding:'10px 12px', background:'var(--surface-2)', borderRadius:'var(--r-sm)'}}>
                  <div className="row gap-2" style={{alignItems:'baseline', marginBottom:6}}>
                    <span style={{
                      width:10, height:10, borderRadius:'50%',
                      background: role && role.color !== '#000000' ? role.color : 'var(--ink-mute)',
                    }}/>
                    <span style={{fontWeight:600, fontSize:13}}>
                      {isMember ? `👤 Membre` : isEveryone ? '@everyone' : (role?.name || 'Rôle inconnu')}
                    </span>
                    <span className="mono" style={{fontSize:10, color:'var(--ink-mute)', marginLeft:'auto'}}>{o.id}</span>
                  </div>
                  {o.allow.length > 0 && (
                    <div style={{marginBottom:4}}>
                      <span className="eyebrow" style={{fontSize:10, color:'var(--mg-success)'}}>✓ Autorisé ({o.allow.length})</span>
                      <PermBadges perms={o.allow} color="var(--mg-success)"/>
                    </div>
                  )}
                  {o.deny.length > 0 && (
                    <div>
                      <span className="eyebrow" style={{fontSize:10, color:'var(--mg-danger)'}}>✗ Refusé ({o.deny.length})</span>
                      <PermBadges perms={o.deny} color="var(--mg-danger)"/>
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </section>
    </div>
  );
}

function PermBadges({ perms, color }) {
  return (
    <div style={{display:'flex', flexWrap:'wrap', gap:4, marginTop:4}}>
      {perms.map(p => {
        const meta = PERM_META[p];
        return (
          <span key={p} title={p} style={{
            padding:'2px 8px', borderRadius:10, fontSize:10, fontWeight:600,
            background: color + '22', color,
          }}>{meta?.fr || p}</span>
        );
      })}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────
function RoleDetail({ role, channels }) {
  if (!role) return null;
  const color = role.color === '#000000' ? 'var(--ink-mute)' : role.color;
  const [editing, setEditing] = useAB(false);
  const [draft, setDraft] = useAB({
    name: role.name, color: role.color, hoist: !!role.hoist,
    mentionable: !!role.mentionable, position: role.position,
  });
  const [saving, setSaving] = useAB(false);
  useEAB(() => {
    setDraft({ name: role.name, color: role.color, hoist: !!role.hoist, mentionable: !!role.mentionable, position: role.position });
    setEditing(false);
  }, [role.id]);

  // Channels où ce rôle a un override
  const channelOverrides = channels
    .filter(c => c.overwrites.some(o => o.id === role.id))
    .map(c => ({
      channel: c,
      overwrite: c.overwrites.find(o => o.id === role.id),
    }));

  // Group perms par catégorie pour l'affichage
  const permsByCat = role.permissions.reduce((a, p) => {
    const cat = PERM_META[p]?.cat || 'other';
    (a[cat] = a[cat] || []).push(p);
    return a;
  }, {});

  const isProtected = role.is_everyone || role.is_managed || role.name === '*';

  const save = async () => {
    setSaving(true);
    try {
      const r = await fetch('/api/admin/bot/role/' + role.id, {
        method: 'PUT', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: draft.name,
          color: draft.color,
          hoist: draft.hoist,
          mentionable: draft.mentionable,
          position: Number(draft.position),
        }),
      });
      if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.error || 'Erreur'); }
      mgToast('Édition enqueue · bot applique dans 5s', { type: 'success' });
      setEditing(false);
    } catch (e) { mgToast(e.message, { type: 'error' }); }
    finally { setSaving(false); }
  };

  return (
    <div className="stack gap-4">
      <section className="card" style={{padding:18, borderLeft: '4px solid ' + color}}>
        <div className="row gap-3" style={{justifyContent:'space-between', alignItems:'flex-start', flexWrap:'wrap'}}>
          <div className="row gap-3" style={{alignItems:'baseline'}}>
            <span style={{width:14, height:14, borderRadius:'50%', background: color}}/>
            <h3 style={{margin:0, fontSize:20, color}}>{role.is_everyone ? '@everyone' : role.name}</h3>
          </div>
          {!isProtected && (
            !editing ? (
              <button className="btn btn-gold btn-sm" onClick={() => setEditing(true)}>✏️ Éditer</button>
            ) : (
              <div className="row gap-2">
                <button className="btn btn-gold btn-sm" disabled={saving} onClick={save}>{saving ? '…' : '💾 Sauver'}</button>
                <button className="btn btn-ghost btn-sm" onClick={() => setEditing(false)}>Annuler</button>
              </div>
            )
          )}
        </div>

        {!editing ? (
          <div className="row gap-3" style={{marginTop:8, fontSize:12, color:'var(--ink-mute)', flexWrap:'wrap'}}>
            <span className="mono">ID: {role.id}</span>
            <span>{role.member_count} membre{role.member_count > 1 ? 's' : ''}</span>
            <span>position {role.position}</span>
            {role.hoist && <span>📌 hoist</span>}
            {role.mentionable && <span>@ mentionable</span>}
            {role.is_managed && <span>🤖 bot/intégration</span>}
            {role.is_everyone && <span style={{color:'var(--mg-gold-500)'}}>🛡️ @everyone (protégé)</span>}
            {role.name === '*' && <span style={{color:'var(--mg-gold-500)'}}>🛡️ * (protégé)</span>}
          </div>
        ) : (
          <div className="stack gap-2" style={{marginTop:14}}>
            <div>
              <label className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)', display:'block', marginBottom:4}}>Nom du rôle</label>
              <input value={draft.name} onChange={e => setDraft({...draft, name: e.target.value})} maxLength={100} style={inputStyle}/>
            </div>
            <div className="row gap-2">
              <div style={{flex:1}}>
                <label className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)', display:'block', marginBottom:4}}>Couleur (hex)</label>
                <div className="row gap-2">
                  <input type="color" value={draft.color === '#000000' ? '#888888' : draft.color} onChange={e => setDraft({...draft, color: e.target.value})} style={{width:50, height:38, border:'1px solid var(--border)', borderRadius:6, padding:2, background:'var(--surface-1)'}}/>
                  <input value={draft.color} onChange={e => setDraft({...draft, color: e.target.value})} placeholder="#ff0000" style={{...inputStyle, flex:1, fontFamily:'monospace'}}/>
                </div>
              </div>
              <div style={{flex:1}}>
                <label className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)', display:'block', marginBottom:4}}>Position</label>
                <input type="number" value={draft.position} onChange={e => setDraft({...draft, position: e.target.value})} style={inputStyle}/>
              </div>
            </div>
            <label className="row gap-2" style={{alignItems:'center', cursor:'pointer'}}>
              <input type="checkbox" checked={draft.hoist} onChange={e => setDraft({...draft, hoist: e.target.checked})}/>
              <span style={{fontSize:13}}>📌 <b>Hoist</b> (affiche le rôle séparément dans la liste des membres)</span>
            </label>
            <label className="row gap-2" style={{alignItems:'center', cursor:'pointer'}}>
              <input type="checkbox" checked={draft.mentionable} onChange={e => setDraft({...draft, mentionable: e.target.checked})}/>
              <span style={{fontSize:13}}>@ <b>Mentionable</b> (n'importe qui peut faire @{role.name})</span>
            </label>
            <div className="soft" style={{fontSize:11, marginTop:4}}>
              ⚠️ Les <b>permissions</b> du rôle ne s'éditent pas ici — utilise <code>perfect-discord.js</code> en SSH.
            </div>
          </div>
        )}
      </section>

      <section className="card" style={{padding:18}}>
        <h3 style={{margin:'0 0 14px', fontSize:16}}>🛡️ Permissions globales ({role.permissions.length})</h3>
        {role.permissions.length === 0 ? (
          <div className="soft" style={{fontSize:13, textAlign:'center', padding:14}}>
            Aucune permission spéciale (rôle purement cosmétique/marqueur)
          </div>
        ) : (
          <div className="stack gap-3">
            {Object.entries(permsByCat).map(([cat, perms]) => (
              <div key={cat}>
                <div className="eyebrow" style={{fontSize:10, color:'var(--ink-mute)', marginBottom:4}}>{CAT_LABEL[cat] || cat}</div>
                <PermBadges perms={perms} color="var(--accent)"/>
              </div>
            ))}
          </div>
        )}
      </section>

      <section className="card" style={{padding:18}}>
        <h3 style={{margin:'0 0 14px', fontSize:16}}>📍 Overrides dans les canaux ({channelOverrides.length})</h3>
        {channelOverrides.length === 0 ? (
          <div className="soft" style={{fontSize:13, textAlign:'center', padding:14}}>
            Ce rôle n'a aucun override spécifique dans un canal.
          </div>
        ) : (
          <div className="stack gap-2">
            {channelOverrides.map(({ channel, overwrite }) => {
              const meta = CHANNEL_TYPE[channel.type] || { label: '?', icon: '?' };
              return (
                <div key={channel.id} style={{padding:'10px 12px', background:'var(--surface-2)', borderRadius:'var(--r-sm)'}}>
                  <div style={{fontWeight:600, fontSize:13, marginBottom:6}}>
                    {meta.icon} {channel.name}
                  </div>
                  {overwrite.allow.length > 0 && (
                    <div style={{marginBottom:4}}>
                      <span className="eyebrow" style={{fontSize:10, color:'var(--mg-success)'}}>✓ Autorisé</span>
                      <PermBadges perms={overwrite.allow} color="var(--mg-success)"/>
                    </div>
                  )}
                  {overwrite.deny.length > 0 && (
                    <div>
                      <span className="eyebrow" style={{fontSize:10, color:'var(--mg-danger)'}}>✗ Refusé</span>
                      <PermBadges perms={overwrite.deny} color="var(--mg-danger)"/>
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        )}
      </section>
    </div>
  );
}

Object.assign(window, { AdminBot });
