import { useState, useEffect, useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { IconX, IconUserFilled, IconPuzzleFilled,
         IconTrophyFilled, IconBellFilled, IconWorldFilled, IconGraphFilled,
         IconTrashFilled, IconCheckFilled, IconSquareXFilled, IconPlayerPlayFilled,
         IconListFilled, IconSnowflake, IconPlayerPauseFilled, IconArrowBack,
         IconBan, IconCircleCheckFilled, IconBold, IconItalic, IconHeading,
         IconList } from '@tabler/icons-react'
import Select from './ui/Select'
import NumberInput from './ui/NumberInput'
import Tooltip from './ui/Tooltip'
import { fetchAdminStatus, setupAdmin } from '../api'
import { renderSimpleMarkdown } from '../markdown'
import type { Icon as TablerIcon } from '@tabler/icons-react'

const BASE = '/api/admin'

type AdminConfigValue = string | number | boolean | string[]
type AdminConfig = Record<string, AdminConfigValue>

interface PresetStat { preset: string; solves: number; avg_time: number }
interface DailyActivity { date: string; solves: number }
interface Contributor { token: string; name: string | null; submitted: number; approved: number }

interface AdminStats {
  sessions: { total: number; active_today: number }
  solves: { total: number; today: number; avg_time: number; fastest: number }
  puzzles: { generated: number; daily: number; user_created: number; published: number }
  community_clues: { total: number; pending: number }
  ratings: { total: number; average: number }
  achievements_unlocked: number
  passkeys: number
  webhooks: number
  pending_reports: number
  federation: {
    enabled: boolean
    followers: number
    peers: number
    queue: number
    inbound: number
    outbound: number
    blocked: number
    key_changes: number
    domain: string | null
  }
  preset_stats: PresetStat[]
  daily_activity: DailyActivity[]
  top_contributors: Contributor[]
  dictionary: { words: number; clue_entries: number; cryptic_entries: number }
  cache: { puzzles: number; word_quality: number; clue_quality: number }
}

interface AdminPuzzleRow {
  id: string
  short_id: string
  daily_date: string | null
  preset: string | null
  width: number
  height: number
  difficulty: number
  created_at: string
  solves: number
  rating: number
}

interface AdminClueRow {
  id: number
  word: string
  clue: string
  submitted_by: string
  upvotes: number
  downvotes: number
  status: string
  created_at: string
}

interface AdminSessionRow {
  token: string
  display_name: string | null
  created_at: string
  last_seen: string
  solves: number
  streak: number | null
  banned: boolean
}

interface AdminAchievementRow {
  id: string
  name: string
  description: string
  icon: string
  color1: string
  color2: string
  unlock_count: number
}

interface AdminWebhookRow {
  id: string
  url: string
  events: string
  format: string
  enabled: boolean
  failure_count: number
}

interface AuditEntryRow {
  id: number
  action: string
  target: string | null
  detail: string | null
  timestamp: string
}

interface ReportRow {
  id: number
  reporter: string
  content_type: string
  content_id: string
  reason: string | null
  created_at: string
}

interface ReputationRow {
  domain: string
  score: number
  total_clues: number
  rejected_clues: number
  reports_against: number
  last_updated: string
}

interface PeerRow {
  domain: string
  actor_uri: string
  last_seen: string | null
}

interface BlockedRow {
  domain: string
  reason: string | null
  blocked_at: string
}

interface FollowerRow {
  actor_uri: string
  inbox_uri: string
  created_at: string
}

interface OutboxRow {
  id: string
  published: string
}

interface CommunityPuzzleRow {
  id: string
  title: string
  author: string | null
  width: number
  height: number
  created_at: string
  updated_at: string
}

interface HumorDefaults {
  taglines: string[]
  signoffs: string[]
  middles: string[]
}

function adminFetch(path: string, opts: RequestInit & { headers?: Record<string, string> } = {}) {
  const session = sessionStorage.getItem('cruciverb-admin-session') || ''
  return fetch(`${BASE}${path}`, {
    ...opts,
    headers: { 'Content-Type': 'application/json', 'X-Admin-Session': session, ...opts.headers },
  })
}

type Tab = 'overview' | 'content' | 'users' | 'achievements' | 'integrations' | 'federation' | 'audit'

function StatCard({ value, label, sub }: { value: string | number; label: string; sub?: string }) {
  return (
    <div className="admin-stat-card">
      <div className="admin-stat-value">{Number.isFinite(value) ? value.toLocaleString() : value}</div>
      <div className="admin-stat-label">{label}</div>
      {sub && <div className="admin-stat-sub">{sub}</div>}
    </div>
  )
}

function formatTime(secs: number): string {
  if (secs < 60) return `${Math.round(secs)}s`
  const m = Math.floor(secs / 60)
  const s = Math.round(secs % 60)
  return s > 0 ? `${m}m ${s}s` : `${m}m`
}

function ConfigRow({ name, value, desc, editable, onChange }: {
  name: string; value: AdminConfigValue | null; desc: string; editable?: boolean; onChange?: (val: string | number | boolean) => void
}) {
  // config payloads are JSON-derived, so values are boolean, number, string, or null
  const isBoolean = value === true || value === false
  const isNumber = !isBoolean && value !== null && value !== undefined && Number.isFinite(value)
  if (editable && onChange) {
    if (isBoolean) {
      return (
        <div className="admin-config-row">
          <span className="admin-config-name">{name}</span>
          <label className="admin-config-toggle">
            <input type="checkbox" checked={value} onChange={e => onChange(e.target.checked)} />
            <span className={value ? 'admin-config-value-true' : 'admin-config-value-false'}>{value ? 'on' : 'off'}</span>
          </label>
          <span className="admin-config-desc">{desc}</span>
        </div>
      )
    }
    if (isNumber) {
      return (
        <div className="admin-config-row">
          <span className="admin-config-name">{name}</span>
          <NumberInput value={Number(value)} onChange={v => onChange(v)} className="admin-config-input" style={{ width: '5rem' }} aria-label={name} />
          <span className="admin-config-desc">{desc}</span>
        </div>
      )
    }
    return (
      <div className="admin-config-row">
        <span className="admin-config-name">{name}</span>
        <input type="text" className="admin-config-input" value={String(value ?? '')}
          onChange={e => onChange(e.target.value)} placeholder="not set" aria-label={name} />
        <span className="admin-config-desc">{desc}</span>
      </div>
    )
  }
  const display = isBoolean ? (value ? 'true' : 'false') : value === null || value === undefined ? 'not set' : String(value)
  const cls = isBoolean ? (value ? 'admin-config-value admin-config-value-true' : 'admin-config-value admin-config-value-false') : 'admin-config-value'
  return (
    <div className="admin-config-row">
      <span className="admin-config-name">{name}</span>
      <span className={cls}>{display}</span>
      <span className="admin-config-desc">{desc}</span>
    </div>
  )
}

function Dialog({ title, children, onClose, actions }: {
  title: string; children: React.ReactNode; onClose: () => void
  actions?: { label: string; primary?: boolean; onClick: () => void }[]
}) {
  return (
    <div className="settings-overlay" onClick={onClose}>
      <div className="admin-dialog" onClick={e => e.stopPropagation()} role="dialog" aria-modal="true" aria-label={title}>
        <div className="admin-dialog-header">
          <h3>{title}</h3>
          <button className="btn btn-sm btn-ghost" onClick={onClose} aria-label="Close"><IconX size={14} aria-hidden="true" /></button>
        </div>
        <div className="admin-dialog-body">{children}</div>
        {actions && (
          <div className="admin-dialog-actions">
            {actions.map((a, i) => (
              <button key={i} className={`btn btn-sm ${a.primary ? 'btn-primary' : ''}`} onClick={a.onClick}>{a.label}</button>
            ))}
          </div>
        )}
      </div>
    </div>
  )
}

function AdminSpinner({ label }: { label?: string }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '3rem 1rem', gap: 8, opacity: 0.5 }}>
      <div className="admin-spinner" />
      {label && <span style={{ fontSize: '0.625rem' }}>{label}</span>}
    </div>
  )
}

export default function AdminPanel({ onClose }: { onClose: () => void }) {
  const { t } = useTranslation()
  const [tab, setTab] = useState<Tab>('overview')
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')
  const [confirmPassword, setConfirmPassword] = useState('')
  const [loginError, setLoginError] = useState('')
  const [setupError, setSetupError] = useState('')
  const [setupSuccess, setSetupSuccess] = useState('')
  const [authenticated, setAuthenticated] = useState(false)
  const [, setAdminConfigured] = useState<boolean | null>(null)
  const [showSetup, setShowSetup] = useState(false)
  const [stats, setStats] = useState<AdminStats | null>(null)
  const [sessions, setSessions] = useState<AdminSessionRow[]>([])
  const [sessionSearch, setSessionSearch] = useState('')
  const [puzzles, setPuzzles] = useState<AdminPuzzleRow[]>([])
  const [pendingClues, setPendingClues] = useState<AdminClueRow[]>([])
  const [achievementStats, setAchievementStats] = useState<AdminAchievementRow[]>([])
  const [webhooks, setWebhooks] = useState<AdminWebhookRow[]>([])
  const [newWebhook, setNewWebhook] = useState({ url: '', events: 'daily_puzzle.published', format: 'json', secret: '' })
  const [auditLog, setAuditLog] = useState<AuditEntryRow[]>([])
  const [reports, setReports] = useState<ReportRow[]>([])
  const [reputations, setReputations] = useState<ReputationRow[]>([])
  const [peers, setPeers] = useState<PeerRow[]>([])
  const [blocked, setBlocked] = useState<BlockedRow[]>([])
  const [newPeerDomain, setNewPeerDomain] = useState('')
  const [blockDomain, setBlockDomain] = useState('')
  const [blockReason, setBlockReason] = useState('')
  const [fedStatus, setFedStatus] = useState<{ frozen: boolean; allowlist_mode?: boolean; approve_followers?: boolean; new_peer_review?: boolean; outbox_limit?: number } | null>(null)
  const [pendingFollowers, setPendingFollowers] = useState<FollowerRow[]>([])
  const [config, setConfig] = useState<AdminConfig | null>(null)
  const [blockedWord, setBlockedWord] = useState('')
  const [outboxItems, setOutboxItems] = useState<OutboxRow[]>([])
  const [deleteUri, setDeleteUri] = useState('')
  const [profile, setProfile] = useState({ ap_actor_name: '', ap_bio: '' })
  const [avatarPreview, setAvatarPreview] = useState<string | null>(null)
  const [bannerPreview, setBannerPreview] = useState<string | null>(null)
  const [uploadStatus, setUploadStatus] = useState('')
  const [profileDirty, setProfileDirty] = useState(false)
  const [apTaglines, setApTaglines] = useState('')
  const [apSignoffs, setApSignoffs] = useState('')
  const [apCommunityMiddles, setApCommunityMiddles] = useState('')
  const [tabLoading, setTabLoading] = useState<Record<Tab, boolean>>({ overview: false, content: false, users: false, achievements: false, integrations: false, federation: false, audit: false })
  const [loadedTabs, setLoadedTabs] = useState<Set<string>>(new Set())
  const [communityPuzzles, setCommunityPuzzles] = useState<CommunityPuzzleRow[]>([])
  const [selectedCommunity, setSelectedCommunity] = useState<Set<string>>(new Set())
  const [puzzleFilter, setPuzzleFilter] = useState<'all' | 'daily' | 'custom'>('all')
  const [selectedPuzzles, setSelectedPuzzles] = useState<Set<string>>(new Set())
  const rulesRef = useRef<HTMLTextAreaElement>(null)

  const [dialog, setDialog] = useState<{ title: string; message: string; onConfirm?: () => void; input?: boolean; inputValue?: string } | null>(null)
  const [dialogInput, setDialogInput] = useState('')
  const [notification, setNotification] = useState('')

  const notify = useCallback((msg: string) => {
    setNotification(msg)
    setTimeout(() => setNotification(''), 3000)
  }, [])

  const login = async () => {
    setLoginError('')
    const res = await fetch('/api/admin/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password }),
    })
    if (res.ok) {
      const data = await res.json()
      sessionStorage.setItem('cruciverb-admin-session', data.session)
      setAuthenticated(true)
      setPassword('')
      const r = await adminFetch('/stats')
      if (r.ok) { setStats(await r.json()); setLoadedTabs(new Set(['overview'])) }
    } else {
      setLoginError(t('admin.invalidCredentials'))
    }
  }

  useEffect(() => {
    fetchAdminStatus().then(data => {
      setAdminConfigured(data.configured)
      setShowSetup(!data.configured)
    })
    const saved = sessionStorage.getItem('cruciverb-admin-session')
    if (saved) adminFetch('/stats').then(async r => { if (r.ok) { setAuthenticated(true); setStats(await r.json()); setLoadedTabs(new Set(['overview'])) } else { sessionStorage.removeItem('cruciverb-admin-session') } })
  }, [])

  const handleSetup = async () => {
    setSetupError('')
    setSetupSuccess('')
    if (username.length < 3) { setSetupError(t('admin.setup.usernameTooShort')); return }
    if (password.length < 8) { setSetupError(t('admin.setup.passwordTooShort')); return }
    if (password !== confirmPassword) { setSetupError(t('admin.setup.passwordMismatch')); return }
    try {
      await setupAdmin(username, password)
      setSetupSuccess(t('admin.setup.success'))
      setShowSetup(false)
      setAdminConfigured(true)
      setPassword('')
      setConfirmPassword('')
    } catch (e) {
      setSetupError(e instanceof Error ? e.message : t('admin.setup.failed'))
    }
  }

  useEffect(() => {
    if (!authenticated) return
    if (loadedTabs.has(tab)) return
    setLoadedTabs(prev => new Set(prev).add(tab))
    loadTab(tab)
  }, [tab, authenticated])

  const loadTab = async (t: Tab) => {
    setTabLoading(prev => ({ ...prev, [t]: true }))
    try {
      if (t === 'overview') { const r = await adminFetch('/stats'); if (r.ok) setStats(await r.json()) }
      if (t === 'content') {
        const [rp, rc, rr, rcp] = await Promise.all([
          adminFetch(`/puzzles/filtered?type=${puzzleFilter}`),
          adminFetch('/clues/pending'),
          adminFetch('/reports'),
          adminFetch('/community-puzzles'),
        ])
        if (rp.ok) setPuzzles((await rp.json()).puzzles || [])
        if (rc.ok) setPendingClues((await rc.json()).clues || [])
        if (rr.ok) setReports((await rr.json()).reports || [])
        if (rcp.ok) setCommunityPuzzles((await rcp.json()).puzzles || [])
        setSelectedPuzzles(new Set())
        setSelectedCommunity(new Set())
      }
      if (t === 'users') { const r = await adminFetch('/sessions'); if (r.ok) setSessions((await r.json()).sessions || []) }
      if (t === 'achievements') { const r = await adminFetch('/achievements'); if (r.ok) setAchievementStats((await r.json()).achievements || []) }
      if (t === 'integrations') {
        const [rw, rc] = await Promise.all([fetch('/api/webhooks'), adminFetch('/config/all')])
        if (rw.ok) setWebhooks((await rw.json()).webhooks || [])
        if (rc.ok) setConfig(await rc.json())
      }
      if (t === 'audit') { const r = await adminFetch('/audit-log'); if (r.ok) setAuditLog((await r.json()).entries || []) }
      if (t === 'federation') {
        const [rp, fs, pf, rc, rd, ro] = await Promise.all([
          adminFetch('/peers'), adminFetch('/federation/status'),
          adminFetch('/followers/pending'), adminFetch('/config/all'),
          adminFetch('/ap-humor-defaults'), adminFetch('/outbox'),
        ])
        if (rp.ok) { const d = await rp.json(); setPeers(d.peers || []); setBlocked(d.blocked || []); setReputations(d.reputations || []) }
        if (fs.ok) setFedStatus(await fs.json())
        if (pf.ok) setPendingFollowers((await pf.json()).pending_followers || [])
        if (ro.ok) setOutboxItems((await ro.json()).items || [])

        let defaults: HumorDefaults | null = null
        if (rd.ok) defaults = await rd.json()

        if (rc.ok) {
          const cfg = await rc.json()
          setConfig(cfg)
          setProfile({
            ap_actor_name: cfg.ap_actor_name || '',
            ap_bio: cfg.ap_bio || '',
          })
          setProfileDirty(false)
          fetch('/avatar').then(r => r.ok ? setAvatarPreview('/avatar?' + Date.now()) : null).catch(() => {})
          fetch('/banner').then(r => r.ok ? setBannerPreview('/banner?' + Date.now()) : null).catch(() => {})
          // load AP humor lines - use saved if present, otherwise use defaults
          const tl = cfg.ap_daily_taglines
          const so = cfg.ap_daily_signoffs
          const cm = cfg.ap_community_middles
          const hasTl = Array.isArray(tl) && tl.length > 0
          const hasSo = Array.isArray(so) && so.length > 0
          const hasCm = Array.isArray(cm) && cm.length > 0
          setApTaglines(hasTl ? tl.join('\n') : (defaults?.taglines || []).join('\n'))
          setApSignoffs(hasSo ? so.join('\n') : (defaults?.signoffs || []).join('\n'))
          setApCommunityMiddles(hasCm ? cm.join('\n') : (defaults?.middles || []).join('\n'))
        }
      }
    } finally {
      setTabLoading(prev => ({ ...prev, [t]: false }))
    }
  }

  // reload puzzles when filter changes
  useEffect(() => {
    if (authenticated && tab === 'content') {
      setTabLoading(prev => ({ ...prev, content: true }))
      adminFetch(`/puzzles/filtered?type=${puzzleFilter}`).then(async r => {
        if (r.ok) setPuzzles((await r.json()).puzzles || [])
        setSelectedPuzzles(new Set())
        setTabLoading(prev => ({ ...prev, content: false }))
      })
    }
  }, [puzzleFilter])

  const togglePuzzleSelect = (id: string) => {
    setSelectedPuzzles(prev => {
      const next = new Set(prev)
      if (next.has(id)) next.delete(id); else next.add(id)
      return next
    })
  }

  const toggleCommunitySelect = (id: string) => {
    setSelectedCommunity(prev => {
      const next = new Set(prev)
      if (next.has(id)) next.delete(id); else next.add(id)
      return next
    })
  }

  const batchDeletePuzzles = () => {
    if (selectedPuzzles.size === 0) return
    setDialog({ title: t('admin.content.deleteSelected'), message: t('admin.content.deleteSelectedMsg', { count: selectedPuzzles.size }), onConfirm: async () => {
      await adminFetch('/puzzles/batch-delete', { method: 'POST', body: JSON.stringify({ ids: [...selectedPuzzles] }) })
      setPuzzles(p => p.filter(x => !selectedPuzzles.has(x.id)))
      setSelectedPuzzles(new Set())
      setDialog(null)
      notify(t('admin.content.puzzlesDeleted', { count: selectedPuzzles.size }))
    }})
  }

  const deleteCommunityPuzzle = (id: string) => {
    setDialog({ title: t('admin.content.deleteCommunityPuzzle'), message: t('admin.content.deleteCommunityPuzzleMsg'), onConfirm: async () => {
      await adminFetch(`/community-puzzles/${id}`, { method: 'DELETE' })
      setCommunityPuzzles(p => p.filter(x => x.id !== id))
      setDialog(null)
      notify(t('admin.content.communityPuzzleDeleted'))
    }})
  }

  const batchDeleteCommunity = () => {
    if (selectedCommunity.size === 0) return
    setDialog({ title: t('admin.content.deleteSelected'), message: t('admin.content.deleteSelectedMsg', { count: selectedCommunity.size }), onConfirm: async () => {
      await adminFetch('/community-puzzles/batch-delete', { method: 'POST', body: JSON.stringify({ ids: [...selectedCommunity] }) })
      setCommunityPuzzles(p => p.filter(x => !selectedCommunity.has(x.id)))
      setSelectedCommunity(new Set())
      setDialog(null)
      notify(t('admin.content.puzzlesDeleted', { count: selectedCommunity.size }))
    }})
  }

  const insertMarkdown = (syntax: string, wrap?: boolean) => {
    const ta = rulesRef.current
    if (!ta) return
    const start = ta.selectionStart
    const end = ta.selectionEnd
    const val = ta.value
    const selected = val.substring(start, end)
    let newText: string
    let cursorPos: number
    if (wrap && selected) {
      newText = val.substring(0, start) + syntax + selected + syntax + val.substring(end)
      cursorPos = end + syntax.length * 2
    } else if (wrap) {
      newText = val.substring(0, start) + syntax + syntax + val.substring(end)
      cursorPos = start + syntax.length
    } else {
      // line prefix (heading, list)
      const lineStart = val.lastIndexOf('\n', start - 1) + 1
      newText = val.substring(0, lineStart) + syntax + val.substring(lineStart)
      cursorPos = start + syntax.length
    }
    setConfig(c => ({ ...(c ?? {}), instance_rules: newText }))
    setTimeout(() => { ta.focus(); ta.setSelectionRange(cursorPos, cursorPos) }, 0)
  }

  const deleteSession = (tk: string) => {
    setDialog({ title: 'Delete session', message: 'This will remove the user session and their settings. Solve records and achievements will remain.', onConfirm: async () => {
      await adminFetch(`/sessions/${tk}`, { method: 'DELETE' }); setSessions(p => p.filter(s => s.token !== tk)); setDialog(null); notify('Session deleted')
    }})
  }

  const purgeSessions = () => {
    setDialog({ title: 'Purge inactive sessions', message: 'Delete all sessions that have been inactive for more than this many days:', input: true, inputValue: '90', onConfirm: async () => {
      const r = await adminFetch('/sessions/purge', { method: 'POST', body: JSON.stringify({ older_than_days: Number(dialogInput || 90) }) })
      if (r.ok) { const d = await r.json(); notify(`Purged ${d.deleted} sessions`); loadTab('users') }
      setDialog(null)
    }})
    setDialogInput('90')
  }

  const resolveReport = async (id: number, action: string) => {
    await adminFetch(`/reports/${id}/resolve`, { method: 'POST', body: JSON.stringify({ action }) })
    setReports(p => p.filter(r => r.id !== id))
    notify(`Report ${action === 'remove' ? 'removed' : 'dismissed'}`)
  }

  const moderateClue = async (id: number, action: string) => {
    await adminFetch(`/clues/${id}/moderate`, { method: 'POST', body: JSON.stringify({ action }) })
    setPendingClues(p => p.filter(c => c.id !== id))
    notify(`Clue ${action}d`)
  }

  const deletePuzzle = (id: string) => {
    setDialog({ title: 'Delete puzzle', message: 'This will permanently delete the puzzle and all associated solve records. This cannot be undone.', onConfirm: async () => {
      await adminFetch(`/puzzles/${id}`, { method: 'DELETE' }); setPuzzles(p => p.filter(x => x.id !== id)); setDialog(null); notify('Puzzle deleted')
    }})
  }

  const createWebhook = async () => {
    await fetch('/api/webhooks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newWebhook) })
    setNewWebhook({ url: '', events: 'daily_puzzle.published', format: 'json', secret: '' })
    loadTab('integrations'); notify('Webhook created')
  }

  const deleteWebhook = (id: string) => {
    setDialog({ title: 'Delete webhook', message: 'Remove this webhook? It will no longer receive events.', onConfirm: async () => {
      await fetch(`/api/webhooks/${id}`, { method: 'DELETE' }); setWebhooks(p => p.filter(w => w.id !== id)); setDialog(null); notify('Webhook deleted')
    }})
  }

  const testWebhook = async (id: string) => {
    await adminFetch(`/webhooks/${id}/test`, { method: 'POST' })
    notify('Test event sent')
  }

  const logout = () => { sessionStorage.removeItem('cruciverb-admin-session'); setAuthenticated(false); setUsername(''); setPassword('') }

  const updateConfig = async (key: string, value: AdminConfigValue) => {
    const updated = { ...(config ?? {}), [key]: value }
    setConfig(updated)
    await adminFetch('/config/all', { method: 'PUT', body: JSON.stringify({ [key]: value }) })
    notify(`${key} updated`)
  }

  const connectPeer = async () => {
    if (!newPeerDomain.trim()) return
    try {
      const r = await adminFetch('/peers/connect', { method: 'POST', body: JSON.stringify({ domain: newPeerDomain.trim() }) })
      if (r.ok) { setNewPeerDomain(''); loadTab('federation'); notify('Peer connected') }
      else { const t = await r.text(); notify(t) }
    } catch (e) { notify(e instanceof Error ? e.message : String(e)) }
  }

  const disconnectPeer = (domain: string) => {
    setDialog({ title: 'Disconnect peer', message: `Stop federating with ${domain}? This removes the peer and any pending deliveries.`, onConfirm: async () => {
      await adminFetch(`/peers/${encodeURIComponent(domain)}`, { method: 'DELETE' })
      setPeers(p => p.filter(x => x.domain !== domain)); setDialog(null); notify('Peer disconnected')
    }})
  }

  const blockInstance = async () => {
    if (!blockDomain.trim()) return
    setDialog({ title: 'Block instance', message: `Block ${blockDomain}? This removes all federated content from this domain and prevents future federation.`, onConfirm: async () => {
      await adminFetch('/block', { method: 'POST', body: JSON.stringify({ domain: blockDomain.trim(), reason: blockReason }) })
      setBlockDomain(''); setBlockReason(''); loadTab('federation'); setDialog(null); notify('Instance blocked')
    }})
  }

  const toggleFreeze = async () => {
    const frozen = fedStatus?.frozen
    const endpoint = frozen ? '/federation/unfreeze' : '/federation/freeze'
    setDialog({
      title: frozen ? 'Unfreeze federation' : 'Freeze federation',
      message: frozen ? 'Resume all federation activity?' : 'This will pause all inbound/outbound federation. Inbox returns 503, delivery and discovery workers pause.',
      onConfirm: async () => {
        await adminFetch(endpoint, { method: 'POST' })
        setFedStatus({ ...fedStatus, frozen: !frozen })
        setDialog(null)
        notify(frozen ? 'Federation unfrozen' : 'Federation frozen')
      }
    })
  }

  const approveFollower = async (uri: string) => {
    await adminFetch('/followers/approve', { method: 'POST', body: JSON.stringify({ actor_uri: uri }) })
    setPendingFollowers(p => p.filter(f => f.actor_uri !== uri))
    notify('Follower approved')
  }

  const rejectFollower = async (uri: string) => {
    await adminFetch('/followers/reject', { method: 'POST', body: JSON.stringify({ actor_uri: uri }) })
    setPendingFollowers(p => p.filter(f => f.actor_uri !== uri))
    notify('Follower rejected')
  }

  const updateProfile = (key: string, val: string) => {
    setProfile(p => ({ ...p, [key]: val }))
    setProfileDirty(true)
  }

  const saveProfile = async () => {
    await adminFetch('/config/all', { method: 'PUT', body: JSON.stringify(profile) })
    setConfig(c => ({ ...(c ?? {}), ...profile }))
    setProfileDirty(false)
    notify(t('admin.federation.profileSaved'))
  }

  const uploadImage = async (kind: 'avatar' | 'banner', file: File) => {
    const form = new FormData()
    form.append('file', file)
    setUploadStatus(t('admin.federation.uploading'))
    try {
      const session = sessionStorage.getItem('cruciverb-admin-session') || ''
      const r = await fetch(`${BASE}/${kind}`, {
        method: 'POST',
        headers: { 'X-Admin-Session': session },
        body: form,
      })
      if (r.ok) {
        const bust = '?' + Date.now()
        if (kind === 'avatar') setAvatarPreview('/avatar' + bust)
        else setBannerPreview('/banner' + bust)
        setUploadStatus(t('admin.federation.uploadSuccess'))
      } else if (r.status === 401) {
        sessionStorage.removeItem('cruciverb-admin-session')
        setAuthenticated(false)
      } else {
        const msg = await r.text()
        setUploadStatus(msg || t('admin.federation.uploadFailed'))
      }
    } catch (_e) {
      setUploadStatus(t('admin.federation.uploadFailed'))
    }
    setTimeout(() => setUploadStatus(''), 3000)
  }

  const unblockInstance = (domain: string) => {
    setDialog({ title: 'Unblock instance', message: `Allow ${domain} to federate again?`, onConfirm: async () => {
      await adminFetch(`/unblock/${encodeURIComponent(domain)}`, { method: 'POST' })
      setBlocked(b => b.filter(x => x.domain !== domain)); setDialog(null); notify('Instance unblocked')
    }})
  }

  const banUser = (token: string, name: string) => {
    setDialog({ title: t('admin.users.banUser'), message: t('admin.users.banMsg', { name: name || t('common.anonymous') }), onConfirm: async () => {
      await adminFetch(`/users/${token}/ban`, { method: 'POST' })
      setSessions(p => p.map(s => s.token === token ? { ...s, banned: true } : s))
      setDialog(null); notify(t('admin.users.userBanned'))
    }})
  }

  const unbanUser = async (token: string) => {
    await adminFetch(`/users/${token}/unban`, { method: 'POST' })
    setSessions(p => p.map(s => s.token === token ? { ...s, banned: false } : s))
    notify(t('admin.users.userUnbanned'))
  }

  const addBlockedWord = async () => {
    if (!blockedWord.trim()) return
    const r = await adminFetch('/blocked-words', { method: 'POST', body: JSON.stringify({ word: blockedWord.trim() }) })
    if (r.ok) { setBlockedWord(''); notify(t('admin.content.wordBlocked')) }
    else { notify(t('admin.content.wordBlockFailed')) }
  }

  const filteredSessions = sessionSearch ? sessions.filter(s => (s.display_name || '').toLowerCase().includes(sessionSearch.toLowerCase()) || s.token.includes(sessionSearch)) : sessions

  if (!authenticated) {
    return (
      <div className="admin-fullscreen" role="main" aria-label={t('admin.title')}>
        <div className="admin-header"><div className="header-left"><button type="button" className="btn btn-sm btn-ghost" onClick={onClose} aria-label={t('common.back')}><IconArrowBack size={16} aria-hidden="true" /></button><button type="button" className="header-brand" onClick={onClose}>{t('app.name')}</button></div></div>
        <div className="admin-login">
          {showSetup ? (
            <div className="admin-login-form">
              <h3 style={{ margin: '0 0 4px', fontSize: '0.85rem' }}>{t('admin.setup.title')}</h3>
              <p style={{ margin: '0 0 12px', fontSize: '0.7rem', opacity: 0.7 }}>{t('admin.setup.desc')}</p>
              <input type="text" value={username} onChange={e => setUsername(e.target.value)} placeholder={t('admin.username')} className="admin-input" autoComplete="username" />
              <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder={t('admin.password')} className="admin-input" autoComplete="new-password" />
              <input type="password" value={confirmPassword} onChange={e => setConfirmPassword(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSetup()} placeholder={t('admin.setup.confirmPassword')} className="admin-input" autoComplete="new-password" />
              {setupError && <p className="admin-login-error">{setupError}</p>}
              <button className="btn btn-primary" onClick={handleSetup} style={{ width: '100%' }}>{t('admin.setup.createAccount')}</button>
            </div>
          ) : (
            <div className="admin-login-form">
              {setupSuccess && <p style={{ color: 'var(--color-correct)', fontSize: '0.7rem', margin: '0 0 8px' }}>{setupSuccess}</p>}
              <input type="text" value={username} onChange={e => setUsername(e.target.value)} onKeyDown={e => e.key === 'Enter' && login()} placeholder={t('admin.username')} className="admin-input" autoComplete="username" />
              <input type="password" value={password} onChange={e => setPassword(e.target.value)} onKeyDown={e => e.key === 'Enter' && login()} placeholder={t('admin.password')} className="admin-input" autoComplete="current-password" />
              {loginError && <p className="admin-login-error">{loginError}</p>}
              <button className="btn btn-primary" onClick={login} style={{ width: '100%' }}>{t('common.signIn')}</button>
            </div>
          )}
        </div>
      </div>
    )
  }

  const tabs: [Tab, string, TablerIcon][] = [
    ['overview', t('admin.tabs.overview'), IconGraphFilled],
    ['content', t('admin.tabs.content'), IconPuzzleFilled],
    ['users', t('admin.tabs.users'), IconUserFilled],
    ['achievements', t('admin.tabs.achievements'), IconTrophyFilled],
    ['integrations', t('admin.tabs.integrations'), IconBellFilled],
    ['federation', t('admin.tabs.federation'), IconWorldFilled],
    ['audit', t('admin.tabs.auditLog'), IconListFilled],
  ]

  return (
    <div className="admin-fullscreen" role="main" aria-label={t('admin.title')}>
      <div className="admin-header">
        <div className="admin-header-inner">
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 12 }}>
            <button type="button" className="btn btn-sm btn-ghost" onClick={onClose} aria-label={t('common.back')}><IconArrowBack size={14} aria-hidden="true" /></button>
            <span style={{ fontFamily: 'var(--font-display)', fontSize: '1rem', fontStyle: 'italic', fontWeight: 400 }}>{t('admin.title')}</span>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: '0.5rem', color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: '0.1em' }}>{t('app.name')}</span>
          </div>
          <button className="btn btn-sm btn-ghost" onClick={logout} style={{ fontFamily: 'var(--font-mono)', fontSize: '0.563rem', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{t('common.signOut')}</button>
        </div>
      </div>

      {notification && <div className="admin-notification">{notification}</div>}

      <div className="admin-body">
        <nav className="admin-nav" aria-label="Admin sections">
          {tabs.map(([key, label, Icon]) => (
            <button key={key} className={`admin-nav-item ${tab === key ? 'admin-nav-active' : ''}`} onClick={() => setTab(key)}>
              <Icon size={14} /> {label}
              {key === 'content' && stats && (stats.community_clues?.pending > 0 || stats.pending_reports > 0) && (
                <span className="admin-badge">{(stats.community_clues?.pending || 0) + (stats.pending_reports || 0)}</span>
              )}
            </button>
          ))}
        </nav>

        <div className="admin-content">

          {/* ---- OVERVIEW ---- */}
          {tab === 'overview' && tabLoading.overview && <AdminSpinner label={t('common.loading')} />}
          {tab === 'overview' && !tabLoading.overview && stats && (<>
            <div className="admin-grid">
              <StatCard value={stats.sessions.total} label={t('admin.overview.totalUsers')} sub={t('admin.overview.activeToday', { count: stats.sessions.active_today })} />
              <StatCard value={stats.solves.total} label={t('admin.overview.puzzlesSolved')} sub={t('admin.overview.today', { count: stats.solves.today })} />
              <StatCard value={formatTime(stats.solves.avg_time)} label={t('admin.overview.avgSolveTime')} sub={t('admin.overview.fastest', { time: formatTime(stats.solves.fastest) })} />
              <StatCard value={stats.ratings.average} label={t('admin.overview.avgPuzzleRating')} sub={t('admin.overview.ratingCount', { count: stats.ratings.total })} />
              <StatCard value={stats.puzzles.generated} label={t('admin.overview.puzzlesGenerated')} sub={t('admin.overview.dailyCount', { count: stats.puzzles.daily })} />
              <StatCard value={stats.puzzles.published} label={t('admin.overview.communityPuzzles')} sub={t('admin.overview.drafts', { count: stats.puzzles.user_created })} />
              <StatCard value={stats.community_clues.total} label={t('admin.overview.communityClues')} sub={t('admin.overview.pending', { count: stats.community_clues.pending })} />
              <StatCard value={stats.achievements_unlocked} label={t('admin.overview.badgesEarned')} />
              <StatCard value={stats.passkeys} label={t('admin.overview.passkeys')} />
              <StatCard value={stats.webhooks} label={t('admin.overview.webhooks')} />
            </div>
            {stats.preset_stats?.length > 0 && (<>
              <h3 className="admin-section-heading">{t('admin.overview.solveStatsByDifficulty')}</h3>
              <table className="admin-table"><thead><tr><th>{t('admin.overview.preset')}</th><th>{t('admin.overview.solves')}</th><th>{t('analytics.avgTime')}</th></tr></thead><tbody>
                {stats.preset_stats.map(p => <tr key={p.preset}><td>{p.preset}</td><td>{p.solves}</td><td className="admin-mono">{formatTime(p.avg_time)}</td></tr>)}
              </tbody></table>
            </>)}
            {stats.daily_activity?.length > 0 && (<>
              <h3 className="admin-section-heading">{t('admin.overview.last7Days')}</h3>
              <div className="admin-activity-chart">
                {stats.daily_activity.map(d => (
                  <div key={d.date} className="admin-bar-col">
                    <div className="admin-bar-value">{d.solves}</div>
                    <div className="admin-bar" style={{ height: `${Math.min(100, d.solves * 15)}%` }} />
                    <span className="admin-bar-label">{d.date.slice(5)}</span>
                  </div>
                ))}
              </div>
            </>)}
            {stats.top_contributors?.length > 0 && (<>
              <h3 className="admin-section-heading">{t('admin.overview.topClueContributors')}</h3>
              <table className="admin-table"><thead><tr><th>{t('admin.overview.user')}</th><th>{t('admin.overview.submitted')}</th><th>{t('admin.overview.approved')}</th><th>{t('admin.overview.rate')}</th></tr></thead><tbody>
                {stats.top_contributors.map(c => <tr key={c.token}><td>{c.name ? c.name : t('common.anonymous')}</td><td>{c.submitted}</td><td>{c.approved}</td><td className="admin-mono">{c.submitted > 0 ? Math.round(100 * c.approved / c.submitted) : 0}%</td></tr>)}
              </tbody></table>
            </>)}
            <h3 className="admin-section-heading">{t('admin.overview.system')}</h3>
            <div className="admin-grid">
              <StatCard value={stats.dictionary.words.toLocaleString()} label={t('admin.overview.dictionaryWords')} />
              <StatCard value={stats.dictionary.clue_entries.toLocaleString()} label={t('admin.overview.clueDbEntries')} />
              <StatCard value={stats.dictionary.cryptic_entries.toLocaleString()} label={t('admin.overview.crypticClues')} />
              <StatCard value={stats.cache.puzzles} label={t('admin.overview.cachedInMemory')} />
            </div>
          </>)}

          {/* ---- CONTENT ---- */}
          {tab === 'content' && tabLoading.content && <AdminSpinner label={t('common.loading')} />}
          {tab === 'content' && !tabLoading.content && (<>
            <h3 className="admin-section-heading">{t('admin.content.puzzles')}</h3>
            <div className="admin-toolbar">
              <div style={{ display: 'flex', gap: 4 }}>
                {(['all', 'daily', 'custom'] as const).map(f => (
                  <button key={f} className={`btn btn-sm ${puzzleFilter === f ? 'btn-primary' : ''}`} onClick={() => setPuzzleFilter(f)}>
                    {t(`admin.content.filter_${f}`)}
                  </button>
                ))}
              </div>
              {selectedPuzzles.size > 0 && (
                <button className="btn btn-sm" style={{ color: 'var(--color-error)' }} onClick={batchDeletePuzzles}>
                  <IconTrashFilled size={11} /> {t('admin.content.deleteSelected')} ({selectedPuzzles.size})
                </button>
              )}
            </div>
            <table className="admin-table"><thead><tr><th style={{ width: 24 }}><input type="checkbox" checked={puzzles.length > 0 && selectedPuzzles.size === puzzles.length} onChange={e => { if (e.target.checked) setSelectedPuzzles(new Set(puzzles.map(p => p.id))); else setSelectedPuzzles(new Set()) }} /></th><th>{t('admin.content.id')}</th><th>{t('admin.content.type')}</th><th>{t('admin.content.size')}</th><th>{t('admin.overview.solves')}</th><th>{t('admin.content.rating')}</th><th>{t('admin.content.created')}</th><th></th></tr></thead><tbody>
              {puzzles.map(p => (
                <tr key={p.id}>
                  <td><input type="checkbox" checked={selectedPuzzles.has(p.id)} onChange={() => togglePuzzleSelect(p.id)} /></td>
                  <td className="admin-mono">{p.short_id}</td>
                  <td>{p.daily_date ? <><strong>{p.preset}</strong> {p.daily_date}</> : t('admin.content.random')}</td>
                  <td>{p.width}x{p.height}</td>
                  <td>{p.solves}</td>
                  <td>{p.rating > 0 ? `${p.rating}/5` : '-'}</td>
                  <td className="admin-mono">{new Date(p.created_at).toLocaleDateString()}</td>
                  <td><button className="cc-delete" onClick={() => deletePuzzle(p.id)}><IconTrashFilled size={11} /></button></td>
                </tr>
              ))}
            </tbody></table>

            <h3 className="admin-section-heading">{t('admin.content.communityPuzzles')}</h3>
            {communityPuzzles.length === 0 && <p className="admin-empty">{t('admin.content.noCommunityPuzzles')}</p>}
            {communityPuzzles.length > 0 && (<>
              {selectedCommunity.size > 0 && (
                <div style={{ marginBottom: 6 }}>
                  <button className="btn btn-sm" style={{ color: 'var(--color-error)' }} onClick={batchDeleteCommunity}>
                    <IconTrashFilled size={11} /> {t('admin.content.deleteSelected')} ({selectedCommunity.size})
                  </button>
                </div>
              )}
              <table className="admin-table"><thead><tr><th style={{ width: 24 }}><input type="checkbox" checked={communityPuzzles.length > 0 && selectedCommunity.size === communityPuzzles.length} onChange={e => { if (e.target.checked) setSelectedCommunity(new Set(communityPuzzles.map(p => p.id))); else setSelectedCommunity(new Set()) }} /></th><th>{t('admin.content.title')}</th><th>{t('admin.content.author')}</th><th>{t('admin.content.size')}</th><th>{t('admin.content.created')}</th><th></th></tr></thead><tbody>
                {communityPuzzles.map(p => (
                  <tr key={p.id}>
                    <td><input type="checkbox" checked={selectedCommunity.has(p.id)} onChange={() => toggleCommunitySelect(p.id)} /></td>
                    <td>{p.title}</td>
                    <td>{p.author}</td>
                    <td>{p.width}x{p.height}</td>
                    <td className="admin-mono">{new Date(p.created_at).toLocaleDateString()}</td>
                    <td><button className="cc-delete" onClick={() => deleteCommunityPuzzle(p.id)}><IconTrashFilled size={11} /></button></td>
                  </tr>
                ))}
              </tbody></table>
            </>)}

            <h3 className="admin-section-heading">{t('admin.content.pendingClues')}</h3>
            {pendingClues.length === 0 && <p className="admin-empty">{t('admin.content.noCluesPending')}</p>}
            {pendingClues.map(c => (
              <div key={c.id} className="admin-clue-card">
                <div className="admin-clue-header">
                  <span className="admin-clue-word">{c.word}</span>
                  <span className="admin-clue-meta">{c.upvotes} up / {c.downvotes} down - {new Date(c.created_at).toLocaleDateString()}</span>
                </div>
                <div className="admin-clue-text">{c.clue}</div>
                <div className="admin-clue-actions">
                  <button className="btn btn-sm" onClick={() => moderateClue(c.id, 'approve')}><IconCheckFilled size={12} /> {t('common.approve')}</button>
                  <button className="btn btn-sm" onClick={() => moderateClue(c.id, 'reject')}><IconSquareXFilled size={12} /> {t('common.reject')}</button>
                  <button className="btn btn-sm" onClick={() => moderateClue(c.id, 'delete')}><IconTrashFilled size={12} /> {t('common.delete')}</button>
                </div>
              </div>
            ))}

            <h3 className="admin-section-heading">{t('admin.content.reports')}</h3>
            {reports.length === 0 && <p className="admin-empty">{t('admin.content.noPendingReports')}</p>}
            {reports.map(r => (
              <div key={r.id} className="admin-clue-card">
                <div className="admin-clue-header">
                  <span className="admin-clue-word">{r.content_type} #{r.content_id}</span>
                  <span className="admin-clue-meta">{new Date(r.created_at).toLocaleDateString()}</span>
                </div>
                {r.reason && <div className="admin-clue-text">{r.reason}</div>}
                <div className="admin-clue-actions">
                  <button className="btn btn-sm" onClick={() => resolveReport(r.id, 'remove')}><IconTrashFilled size={12} /> {t('admin.content.removeContent')}</button>
                  <button className="btn btn-sm" onClick={() => resolveReport(r.id, 'dismiss')}><IconSquareXFilled size={12} /> {t('common.dismiss')}</button>
                </div>
              </div>
            ))}

            <h3 className="admin-section-heading">{t('admin.content.blockedWords')}</h3>
            <p style={{ fontSize: '0.625rem', color: 'var(--text-tertiary)', marginBottom: 8 }}>{t('admin.content.blockedWordsDesc')}</p>
            <div className="admin-webhook-form">
              <input placeholder={t('admin.content.blockedWordPlaceholder')} value={blockedWord}
                onChange={e => setBlockedWord(e.target.value)}
                onKeyDown={e => e.key === 'Enter' && addBlockedWord()}
                className="admin-input" style={{ maxWidth: '16rem' }} />
              <button className="btn btn-sm btn-primary" onClick={addBlockedWord} disabled={!blockedWord.trim()}>{t('common.add')}</button>
            </div>
          </>)}

          {/* ---- USERS ---- */}
          {tab === 'users' && tabLoading.users && <AdminSpinner label={t('common.loading')} />}
          {tab === 'users' && !tabLoading.users && (<>
            <div className="admin-toolbar">
              <input placeholder={t('admin.users.searchPlaceholder')} value={sessionSearch} onChange={e => setSessionSearch(e.target.value)} className="admin-input" style={{ maxWidth: '16rem' }} />
              <button className="btn btn-sm" onClick={purgeSessions}>{t('admin.users.purgeInactive')}</button>
              <span className="admin-toolbar-count">{t('admin.users.userCount', { count: filteredSessions.length })}</span>
            </div>
            <table className="admin-table"><thead><tr><th>{t('admin.users.name')}</th><th>{t('admin.users.status')}</th><th>{t('admin.users.solves')}</th><th>{t('admin.users.streak')}</th><th>{t('admin.users.lastActive')}</th><th>{t('admin.users.created')}</th><th></th></tr></thead><tbody>
              {filteredSessions.map(s => (
                <tr key={s.token}>
                  <td>{s.display_name || <span className="admin-anon">{t('common.anonymous')}</span>}</td>
                  <td>{s.banned ? <span className="admin-error">{t('admin.users.banned')}</span> : <span style={{ opacity: 0.5 }}>{t('admin.users.activeStatus')}</span>}</td>
                  <td>{s.solves}</td>
                  <td>{s.streak || 0}</td>
                  <td className="admin-mono">{new Date(s.last_seen).toLocaleDateString()}</td>
                  <td className="admin-mono">{new Date(s.created_at).toLocaleDateString()}</td>
                  <td style={{ display: 'flex', gap: 4 }}>
                    {s.banned
                      ? <Tooltip text={t('admin.users.unban')}><button className="btn btn-sm" onClick={() => unbanUser(s.token)}><IconCircleCheckFilled size={11} /></button></Tooltip>
                      : <Tooltip text={t('admin.users.ban')}><button className="cc-delete" onClick={() => banUser(s.token, s.display_name ?? '')}><IconBan size={11} /></button></Tooltip>
                    }
                    <Tooltip text={t('common.delete')}><button className="cc-delete" onClick={() => deleteSession(s.token)}><IconTrashFilled size={11} /></button></Tooltip>
                  </td>
                </tr>
              ))}
            </tbody></table>
          </>)}

          {/* ---- ACHIEVEMENTS ---- */}
          {tab === 'achievements' && tabLoading.achievements && <AdminSpinner label={t('common.loading')} />}
          {tab === 'achievements' && !tabLoading.achievements && (
            <table className="admin-table"><thead><tr><th>Badge</th><th>Description</th><th>Unlocked by</th></tr></thead><tbody>
              {achievementStats.map(a => <tr key={a.id}><td><strong>{a.name}</strong></td><td>{a.description}</td><td>{a.unlock_count} player{a.unlock_count !== 1 ? 's' : ''}</td></tr>)}
            </tbody></table>
          )}

          {/* ---- INTEGRATIONS ---- */}
          {tab === 'integrations' && tabLoading.integrations && <AdminSpinner label={t('common.loading')} />}
          {tab === 'integrations' && !tabLoading.integrations && (<>
            <h3 className="admin-section-heading">{t('admin.integrations.webhooks')}</h3>
            <div className="admin-webhook-form">
              <input placeholder={t('admin.integrations.webhookUrl')} value={newWebhook.url} onChange={e => setNewWebhook({ ...newWebhook, url: e.target.value })} className="admin-input" />
              <Select value={newWebhook.events} onChange={v => setNewWebhook({ ...newWebhook, events: String(v) })} options={[
                { value: 'daily_puzzle.published', label: t('admin.integrations.dailyPublished') },
                { value: 'puzzle.completed', label: t('admin.integrations.puzzleCompleted') },
                { value: 'achievement.unlocked', label: t('admin.integrations.achievementUnlocked') },
                { value: 'daily_puzzle.published,puzzle.completed,achievement.unlocked', label: t('admin.integrations.allEvents') },
              ]} />
              <Select value={newWebhook.format} onChange={v => setNewWebhook({ ...newWebhook, format: String(v) })} options={[
                { value: 'json', label: 'JSON' },
                { value: 'discord', label: 'Discord' },
                { value: 'slack', label: 'Slack' },
              ]} />
              <input placeholder={t('admin.integrations.signingSecret')} value={newWebhook.secret} onChange={e => setNewWebhook({ ...newWebhook, secret: e.target.value })} className="admin-input" style={{ maxWidth: '10rem' }} />
              <button className="btn btn-sm btn-primary" onClick={createWebhook} disabled={!newWebhook.url}>{t('common.add')}</button>
            </div>
            {webhooks.length === 0 && <p className="admin-empty">{t('admin.integrations.noWebhooks')}</p>}
            <table className="admin-table"><thead><tr><th>{t('admin.integrations.url')}</th><th>{t('admin.integrations.events')}</th><th>{t('admin.integrations.format')}</th><th>{t('admin.integrations.failures')}</th><th></th></tr></thead><tbody>
              {webhooks.map(w => (
                <tr key={w.id}>
                  <td className="admin-mono" style={{ maxWidth: '20rem', overflow: 'hidden', textOverflow: 'ellipsis' }}>{w.url}</td>
                  <td style={{ fontSize: '0.625rem' }}>{w.events}</td>
                  <td>{w.format}</td>
                  <td className={w.failure_count > 0 ? 'admin-error' : ''}>{w.failure_count}</td>
                  <td style={{ display: 'flex', gap: 4 }}>
                    <Tooltip text="Test"><button className="cc-delete" onClick={() => testWebhook(w.id)}><IconPlayerPlayFilled size={11} /></button></Tooltip>
                    <Tooltip text="Delete"><button className="cc-delete" onClick={() => deleteWebhook(w.id)}><IconTrashFilled size={11} /></button></Tooltip>
                  </td>
                </tr>
              ))}
            </tbody></table>

            <h3 className="admin-section-heading">{t('admin.integrations.environment')}</h3>
            {config ? (
              <div>
                <ConfigRow name="CORS_ORIGIN" value={config.CORS_ORIGIN} desc="Restricts which domains can make API requests" editable onChange={v => updateConfig('CORS_ORIGIN', v)} />
                <ConfigRow name="SECURE_COOKIES" value={config.SECURE_COOKIES} desc="Sets Secure flag on session cookies" editable onChange={v => updateConfig('SECURE_COOKIES', v)} />
                <ConfigRow name="HSTS" value={config.HSTS} desc="Sends Strict-Transport-Security header" editable onChange={v => updateConfig('HSTS', v)} />
                <ConfigRow name="PUBLIC_MOD_LOG" value={config.PUBLIC_MOD_LOG} desc="Makes moderation log visible to all users" editable onChange={v => updateConfig('PUBLIC_MOD_LOG', v)} />
                <ConfigRow name="INSTANCE_CONTACT" value={config.INSTANCE_CONTACT} desc="Contact info shown on about page" editable onChange={v => updateConfig('INSTANCE_CONTACT', v)} />
                <ConfigRow name="ADMIN_PATH" value={config.ADMIN_PATH} desc={t('admin.integrations.adminPathDesc')} editable onChange={v => updateConfig('admin_path', v)} />
              </div>
            ) : <p className="admin-empty">Loading...</p>}

            <h3 className="admin-section-heading">{t('admin.integrations.instanceRules')}</h3>
            <p style={{ fontSize: '0.625rem', color: 'var(--text-tertiary)', marginBottom: 8 }}>{t('admin.integrations.instanceRulesDesc')}</p>
            <div style={{ maxWidth: '28rem' }}>
              <div className="admin-md-toolbar">
                <Tooltip text={t('admin.integrations.mdBold')}><button className="btn btn-sm btn-ghost" onClick={() => insertMarkdown('**', true)}><IconBold size={13} /></button></Tooltip>
                <Tooltip text={t('admin.integrations.mdItalic')}><button className="btn btn-sm btn-ghost" onClick={() => insertMarkdown('*', true)}><IconItalic size={13} /></button></Tooltip>
                <Tooltip text={t('admin.integrations.mdHeading')}><button className="btn btn-sm btn-ghost" onClick={() => insertMarkdown('# ')}><IconHeading size={13} /></button></Tooltip>
                <Tooltip text={t('admin.integrations.mdList')}><button className="btn btn-sm btn-ghost" onClick={() => insertMarkdown('- ')}><IconList size={13} /></button></Tooltip>
              </div>
              <textarea
                ref={rulesRef}
                className="admin-input"
                rows={6}
                value={String(config?.instance_rules ?? '')}
                onChange={e => setConfig(c => ({ ...(c ?? {}), instance_rules: e.target.value }))}
                placeholder="Be respectful.&#10;Submit quality clues.&#10;No spam."
                style={{ resize: 'vertical', fontFamily: 'inherit', width: '100%', borderTopLeftRadius: 0, borderTopRightRadius: 0 }}
              />
              {config?.instance_rules && (
                <div className="admin-md-preview">
                  <span style={{ fontSize: '0.55rem', textTransform: 'uppercase', opacity: 0.5, letterSpacing: '0.05em' }}>{t('admin.integrations.preview')}</span>
                  <div className="admin-md-rendered" dangerouslySetInnerHTML={{ __html: renderSimpleMarkdown(String(config?.instance_rules ?? '')) }} />
                </div>
              )}
              <button
                className="btn btn-primary"
                onClick={() => updateConfig('instance_rules', config?.instance_rules || '')}
                style={{ marginTop: 8, width: '100%' }}
              >{t('common.save')}</button>
            </div>
          </>)}

          {/* ---- FEDERATION ---- */}
          {tab === 'federation' && tabLoading.federation && <AdminSpinner label={t('common.loading')} />}
          {tab === 'federation' && !tabLoading.federation && stats && (<>
            {/* Status */}
            <h3 className="admin-section-heading">{t('admin.federation.status')}</h3>
            <div className="admin-grid" style={{ maxWidth: '32rem' }}>
              <StatCard value={stats.federation.enabled ? t('admin.federation.active') : t('admin.federation.notConfigured')} label={t('admin.federation.status')} sub={stats.federation.domain || ''} />
              <StatCard value={stats.federation.followers} label={t('admin.federation.followers')} />
              <StatCard value={peers.length} label={t('instance.peers')} />
              <StatCard value={stats.federation.queue} label={t('admin.federation.deliveryQueue')} />
              <StatCard value={stats.federation.inbound || 0} label={t('admin.federation.inboundActivities')} />
              <StatCard value={stats.federation.outbound || 0} label={t('admin.federation.outboundDeliveries')} />
              <StatCard value={stats.federation.blocked || 0} label={t('admin.federation.blockedInstances')} />
              {fedStatus?.frozen && <StatCard value="Frozen" label="federation paused" sub="inbox returns 503" />}
              {stats.federation.key_changes > 0 && <StatCard value={stats.federation.key_changes} label="key changes detected" sub="check audit log" />}
            </div>

            {stats.federation.enabled ? (<>
              {/* Profile */}
              <h3 className="admin-section-heading">{t('admin.federation.profile')}</h3>
              <div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 12 }}>
                <label style={{ fontSize: '0.625rem', color: 'var(--text-secondary)' }}>{t('admin.federation.actorUsername')}</label>
                <input className="admin-input" value={profile.ap_actor_name} onChange={e => updateProfile('ap_actor_name', e.target.value)} placeholder="crossword" />
                <label style={{ fontSize: '0.625rem', color: 'var(--text-secondary)' }}>{t('admin.federation.bio')}</label>
                <textarea className="admin-input" rows={4} value={profile.ap_bio} onChange={e => updateProfile('ap_bio', e.target.value)} placeholder={t('admin.federation.bioPlaceholder')} style={{ resize: 'vertical', fontFamily: 'inherit' }} />
                <label style={{ fontSize: '0.625rem', color: 'var(--text-secondary)' }}>{t('admin.federation.avatar')}</label>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                  {avatarPreview && <img src={avatarPreview} alt="avatar" style={{ width: 48, height: 48, borderRadius: 4, objectFit: 'cover' }} />}
                  <input type="file" accept="image/*" onChange={e => { if (e.target.files?.[0]) uploadImage('avatar', e.target.files[0]) }} style={{ fontSize: '0.625rem' }} />
                </div>
                <label style={{ fontSize: '0.625rem', color: 'var(--text-secondary)' }}>{t('admin.federation.banner')}</label>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                  {bannerPreview && <img src={bannerPreview} alt="banner" style={{ width: 120, height: 40, borderRadius: 4, objectFit: 'cover' }} />}
                  <input type="file" accept="image/*" onChange={e => { if (e.target.files?.[0]) uploadImage('banner', e.target.files[0]) }} style={{ fontSize: '0.625rem' }} />
                </div>
                {uploadStatus && <p style={{ fontSize: '0.6rem', color: 'var(--text-secondary)', margin: 0 }}>{uploadStatus}</p>}
                <button className="btn btn-sm btn-primary" onClick={saveProfile} disabled={!profileDirty} style={{ alignSelf: 'flex-start', marginTop: 4 }}>{t('admin.federation.saveProfile')}</button>
              </div>

              {/* Controls */}
              <h3 className="admin-section-heading">{t('admin.federation.controls')}</h3>
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 8 }}>
                <button className={`btn btn-sm ${fedStatus?.frozen ? 'btn-primary' : ''}`} onClick={toggleFreeze}>
                  {fedStatus?.frozen ? <><IconPlayerPauseFilled size={12} /> {t('admin.federation.unfreeze')}</> : <><IconSnowflake size={12} /> {t('admin.federation.freezeFederation')}</>}
                </button>
              </div>
              {fedStatus?.frozen && <p style={{ color: 'var(--color-error)', fontSize: '0.625rem', marginBottom: 8 }}>Federation is frozen - inbox returns 503, delivery and discovery paused</p>}
              {fedStatus && (
                <div style={{ fontSize: '0.625rem', marginBottom: 12, opacity: 0.7 }}>
                  {fedStatus.allowlist_mode && <span style={{ marginRight: 8 }}>Allowlist mode ON</span>}
                  {fedStatus.approve_followers && <span style={{ marginRight: 8 }}>Follower approval ON</span>}
                  {fedStatus.new_peer_review && <span style={{ marginRight: 8 }}>New peer review ON</span>}
                  <span>Outbox limit: {fedStatus.outbox_limit}</span>
                </div>
              )}

              {/* Outbox */}
              <h3 className="admin-section-heading">Outbox</h3>
              {outboxItems.length === 0 && <p className="admin-empty" style={{ padding: '1rem' }}>No published posts</p>}
              {outboxItems.length > 0 && (
                <table className="admin-table">
                  <thead><tr><th>Post</th><th>Published</th><th></th></tr></thead>
                  <tbody>{outboxItems.map((item, i) => {
                    const label = item.id.replace(/\/activity$/, '').split('/ap/').pop() || item.id
                    return (
                      <tr key={item.id}>
                        <td className="admin-mono" style={{ maxWidth: '24rem', overflow: 'hidden', textOverflow: 'ellipsis' }}>{label}</td>
                        <td className="admin-mono">{new Date(item.published).toLocaleString()}</td>
                        <td><button className="cc-delete" onClick={() => {
                          setDialog({ title: 'Delete post', message: `Delete "${label}" and send Delete activity to all followers?`, onConfirm: async () => {
                            await adminFetch(`/outbox/${i}`, { method: 'DELETE' })
                            setOutboxItems(prev => prev.filter((_, j) => j !== i))
                            setDialog(null)
                            notify('Post deleted')
                          }})
                        }}><IconTrashFilled size={11} /></button></td>
                      </tr>
                    )
                  })}</tbody>
                </table>
              )}

              <div className="admin-webhook-form" style={{ marginTop: 8 }}>
                <input placeholder="Post URI to delete from remote instances" value={deleteUri}
                  onChange={e => setDeleteUri(e.target.value)}
                  onKeyDown={e => e.key === 'Enter' && deleteUri.trim() && (async () => {
                    await adminFetch('/outbox/delete-remote', { method: 'POST', body: JSON.stringify({ uri: deleteUri.trim() }) })
                    setDeleteUri(''); notify('Delete activity sent')
                  })()}
                  className="admin-input" />
                <button className="btn btn-sm" onClick={async () => {
                  if (!deleteUri.trim()) return
                  await adminFetch('/outbox/delete-remote', { method: 'POST', body: JSON.stringify({ uri: deleteUri.trim() }) })
                  setDeleteUri(''); notify('Delete activity sent')
                }} disabled={!deleteUri.trim()}>Send delete</button>
              </div>

              {/* Peers */}
              <h3 className="admin-section-heading">{t('admin.federation.peersTitle')}</h3>
              <div className="admin-webhook-form">
                <input placeholder={t('admin.federation.peerDomainPlaceholder')} value={newPeerDomain}
                  onChange={e => setNewPeerDomain(e.target.value)}
                  onKeyDown={e => e.key === 'Enter' && connectPeer()}
                  className="admin-input" />
                <button className="btn btn-sm btn-primary" onClick={connectPeer} disabled={!newPeerDomain.trim()}>{t('common.connect')}</button>
              </div>
              {peers.length > 0 && (
                <table className="admin-table">
                  <thead><tr><th>{t('admin.federation.domain')}</th><th>{t('admin.federation.lastSeen')}</th><th></th></tr></thead>
                  <tbody>{peers.map(p => (
                    <tr key={p.domain}>
                      <td className="admin-mono">{p.domain}</td>
                      <td className="admin-mono">{p.last_seen ? new Date(p.last_seen).toLocaleString() : 'never'}</td>
                      <td><button className="cc-delete" onClick={() => disconnectPeer(p.domain)}><IconTrashFilled size={11} /></button></td>
                    </tr>
                  ))}</tbody>
                </table>
              )}

              {/* Followers */}
              <h3 className="admin-section-heading">{t('admin.federation.followersTitle')}</h3>
              <p style={{ fontSize: '0.625rem', color: 'var(--text-tertiary)', marginBottom: 4 }}>{stats.federation.followers} approved follower{stats.federation.followers !== 1 ? 's' : ''}</p>
              {pendingFollowers.length > 0 && (<>
                <p style={{ fontSize: '0.625rem', marginBottom: 4 }}>{pendingFollowers.length} pending approval:</p>
                <table className="admin-table">
                  <thead><tr><th>Actor</th><th>Requested</th><th></th></tr></thead>
                  <tbody>{pendingFollowers.map(f => (
                    <tr key={f.actor_uri}>
                      <td className="admin-mono" style={{ maxWidth: '20rem', overflow: 'hidden', textOverflow: 'ellipsis' }}>{f.actor_uri}</td>
                      <td className="admin-mono">{new Date(f.created_at).toLocaleDateString()}</td>
                      <td style={{ display: 'flex', gap: 4 }}>
                        <button className="btn btn-sm" onClick={() => approveFollower(f.actor_uri)}><IconCheckFilled size={11} /></button>
                        <button className="cc-delete" onClick={() => rejectFollower(f.actor_uri)}><IconSquareXFilled size={11} /></button>
                      </td>
                    </tr>
                  ))}</tbody>
                </table>
              </>)}

              {/* Security */}
              <h3 className="admin-section-heading">{t('admin.federation.security')}</h3>
              <div className="admin-webhook-form">
                <input placeholder={t('admin.federation.domainToBlock')} value={blockDomain} onChange={e => setBlockDomain(e.target.value)} className="admin-input" />
                <input placeholder={t('admin.federation.reasonOptional')} value={blockReason} onChange={e => setBlockReason(e.target.value)} className="admin-input" style={{ maxWidth: '12rem' }} />
                <button className="btn btn-sm" onClick={blockInstance} disabled={!blockDomain.trim()}>{t('common.block')}</button>
              </div>
              {blocked.length > 0 && (
                <table className="admin-table">
                  <thead><tr><th>{t('admin.federation.domain')}</th><th>{t('admin.federation.reason')}</th><th>{t('admin.federation.blocked')}</th><th></th></tr></thead>
                  <tbody>{blocked.map(b => (
                    <tr key={b.domain}>
                      <td className="admin-mono">{b.domain}</td>
                      <td>{b.reason || '-'}</td>
                      <td className="admin-mono">{new Date(b.blocked_at).toLocaleDateString()}</td>
                      <td><button className="btn btn-sm" onClick={() => unblockInstance(b.domain)} style={{ fontSize: '0.5rem' }}>{t('common.unblock')}</button></td>
                    </tr>
                  ))}</tbody>
                </table>
              )}
              <div className="settings-row" style={{ marginTop: 8 }}>
                <button className="btn btn-sm" onClick={async () => {
                  const token = sessionStorage.getItem('cruciverb-admin-session') || ''
                  const res = await fetch('/api/admin/blocklist/export', { headers: { 'X-Admin-Session': token } })
                  if (!res.ok) return
                  const blob = await res.blob()
                  const url = URL.createObjectURL(blob)
                  const a = document.createElement('a'); a.href = url; a.download = 'blocklist.json'; a.click()
                  URL.revokeObjectURL(url)
                }}>{t('admin.federation.exportBlocklist')}</button>
                <button className="btn btn-sm" onClick={() => {
                  const input = document.createElement('input')
                  input.type = 'file'; input.accept = '.json'
                  input.onchange = async () => {
                    if (!input.files?.length) return
                    try {
                      const data = JSON.parse(await input.files[0].text())
                      const token = sessionStorage.getItem('cruciverb-admin-session') || ''
                      const r = await fetch('/api/admin/blocklist/import', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Session': token }, body: JSON.stringify(data) })
                      if (r.ok) { const d = await r.json(); notify(`Imported ${d.imported} blocked domains`); loadTab('federation') }
                    } catch { /* ignored */ }
                  }
                  input.click()
                }}>{t('admin.federation.importBlocklist')}</button>
              </div>

              {reputations.length > 0 && (
                <table className="admin-table" style={{ marginTop: 8 }}>
                  <thead><tr><th>Domain</th><th>Score</th><th>Clues</th><th>Rejected</th><th>Reports</th><th>Updated</th></tr></thead>
                  <tbody>{reputations.map(r => (
                    <tr key={r.domain}>
                      <td className="admin-mono">{r.domain}</td>
                      <td className={r.score < 50 ? 'admin-error' : ''}>{r.score}</td>
                      <td>{r.total_clues}</td>
                      <td>{r.rejected_clues}</td>
                      <td>{r.reports_against}</td>
                      <td className="admin-mono">{r.last_updated ? new Date(r.last_updated).toLocaleDateString() : '-'}</td>
                    </tr>
                  ))}</tbody>
                </table>
              )}

              {/* AP post humor */}
              <h3 className="admin-section-heading">{t('admin.federation.apHumor')}</h3>
              <p style={{ fontSize: '0.6rem', color: 'var(--text-tertiary)', margin: '0 0 8px' }}>
                {t('admin.federation.apHumorDesc')}
              </p>
              <div style={{ maxWidth: '28rem', display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 12 }}>
                <label style={{ fontSize: '0.625rem', color: 'var(--text-secondary)' }}>
                  {t('admin.federation.dailyTaglines')} ({apTaglines.trim() ? apTaglines.trim().split('\n').filter(Boolean).length : 0} {t('admin.federation.entries')})
                </label>
                <textarea className="admin-input" rows={4} value={apTaglines} onChange={e => setApTaglines(e.target.value)}
                  placeholder={t('admin.federation.onePerLine')}
                  style={{ resize: 'vertical', fontFamily: 'var(--font-mono)', fontSize: '0.6rem' }} />

                <label style={{ fontSize: '0.625rem', color: 'var(--text-secondary)' }}>
                  {t('admin.federation.dailySignoffs')} ({apSignoffs.trim() ? apSignoffs.trim().split('\n').filter(Boolean).length : 0} {t('admin.federation.entries')})
                </label>
                <textarea className="admin-input" rows={4} value={apSignoffs} onChange={e => setApSignoffs(e.target.value)}
                  placeholder={t('admin.federation.onePerLine')}
                  style={{ resize: 'vertical', fontFamily: 'var(--font-mono)', fontSize: '0.6rem' }} />

                <label style={{ fontSize: '0.625rem', color: 'var(--text-secondary)' }}>
                  {t('admin.federation.communityMiddles')} ({apCommunityMiddles.trim() ? apCommunityMiddles.trim().split('\n').filter(Boolean).length : 0} {t('admin.federation.entries')})
                </label>
                <textarea className="admin-input" rows={4} value={apCommunityMiddles} onChange={e => setApCommunityMiddles(e.target.value)}
                  placeholder={t('admin.federation.onePerLine')}
                  style={{ resize: 'vertical', fontFamily: 'var(--font-mono)', fontSize: '0.6rem' }} />

                <button className="btn btn-sm btn-primary" style={{ alignSelf: 'flex-start' }} onClick={async () => {
                  const taglines = apTaglines.trim().split('\n').filter(Boolean)
                  const signoffs = apSignoffs.trim().split('\n').filter(Boolean)
                  const middles = apCommunityMiddles.trim().split('\n').filter(Boolean)
                  await adminFetch('/config/all', { method: 'PUT', body: JSON.stringify({
                    ap_daily_taglines: taglines,
                    ap_daily_signoffs: signoffs,
                    ap_community_middles: middles,
                  }) })
                  notify(t('admin.federation.humorSaved'))
                }}>{t('common.save')}</button>
              </div>

              {/* Configuration */}
              <h3 className="admin-section-heading">{t('admin.federation.configuration')}</h3>
              {config ? (
                <div>
                  <ConfigRow name="AP_DOMAIN" value={config.AP_DOMAIN} desc="Public domain for ActivityPub (requires restart to change)" />
                  <ConfigRow name="AP_ACTOR_NAME" value={config.AP_ACTOR_NAME} desc="Actor name in @name@domain format" />
                  <ConfigRow name="AP_ALLOWLIST_MODE" value={config.AP_ALLOWLIST_MODE} desc="Only federate with explicitly connected peers" editable onChange={v => updateConfig('AP_ALLOWLIST_MODE', v)} />
                  <ConfigRow name="AP_APPROVE_FOLLOWERS" value={config.AP_APPROVE_FOLLOWERS} desc="Require admin approval for new followers" editable onChange={v => updateConfig('AP_APPROVE_FOLLOWERS', v)} />
                  <ConfigRow name="AP_NEW_PEER_REVIEW" value={config.AP_NEW_PEER_REVIEW} desc="Hold first 10 clues from new peers for review" editable onChange={v => updateConfig('AP_NEW_PEER_REVIEW', v)} />
                  <ConfigRow name="AP_OUTBOX_LIMIT" value={config.AP_OUTBOX_LIMIT} desc="Max items in outbox (0 to disable)" editable onChange={v => updateConfig('AP_OUTBOX_LIMIT', v)} />
                  <ConfigRow name="AP_SHARED_BLOCKLIST" value={config.AP_SHARED_BLOCKLIST} desc="Auto-apply blocklist updates from peers" editable onChange={v => updateConfig('AP_SHARED_BLOCKLIST', v)} />
                </div>
              ) : <p className="admin-empty">Loading...</p>}
            </>) : (
              <div className="admin-federation-info">
                <h4>ActivityPub federation</h4>
                <p>Set <code>AP_DOMAIN</code> to enable. Required env vars:</p>
                <ul><li><code>AP_DOMAIN</code> - your public domain</li><li><code>AP_ACTOR_NAME</code> - optional, defaults to "cruciverb"</li></ul>
                <p>Database tables are ready. RSA keys generate on first startup.</p>
              </div>
            )}
          </>)}

          {/* ---- AUDIT LOG ---- */}
          {tab === 'audit' && tabLoading.audit && <AdminSpinner label={t('common.loading')} />}
          {tab === 'audit' && !tabLoading.audit && (<>
            {auditLog.length === 0 && <p className="admin-empty">{t('admin.audit.noEntries')}</p>}
            <table className="admin-table"><thead><tr><th>{t('admin.audit.action')}</th><th>{t('admin.audit.target')}</th><th>{t('admin.audit.detail')}</th><th>{t('admin.audit.time')}</th></tr></thead><tbody>
              {auditLog.map(e => <tr key={e.id}><td><strong>{e.action}</strong></td><td className="admin-mono">{e.target || '-'}</td><td>{e.detail || '-'}</td><td className="admin-mono">{new Date(e.timestamp).toLocaleString()}</td></tr>)}
            </tbody></table>
          </>)}

        </div>
      </div>

      {dialog && (
        <Dialog title={dialog.title} onClose={() => setDialog(null)} actions={[
          { label: t('common.cancel'), onClick: () => setDialog(null) },
          { label: t('common.confirm'), primary: true, onClick: () => dialog.onConfirm?.() },
        ]}>
          <p>{dialog.message}</p>
          {dialog.input && <input className="admin-input" value={dialogInput} onChange={e => setDialogInput(e.target.value)} autoFocus style={{ marginTop: 8 }} />}
        </Dialog>
      )}
    </div>
  )
}
