import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { useWebSocket } from '../hooks/useWebSocket';
import { api } from '../api/client';
import type { Agent, AgentService } from '../types';
import NeonCard from '../components/NeonCard/NeonCard';
import LatencyBadge from '../components/Fleet/LatencyBadge';
import CreateGroupModal from '../components/Fleet/CreateGroupModal';
import FleetGroupsStrip from '../components/Fleet/FleetGroupsStrip';
import FleetToolbar from '../components/Fleet/FleetToolbar';
import CrucibleAgentMeta from '../components/Fleet/CrucibleAgentMeta';
import {
  DEFAULT_FLEET_FILTERS,
  filterFleetAgents,
  formatHashrate,
  type FleetFilterState,
} from '../help/fleetFilters';
import { useFleetBulkActions } from '../hooks/useFleetBulkActions';
import { primaryGroupForAgent } from '../help/fleetGroups';
import { useFleetGroups } from '../hooks/useFleetGroups';
import { useMatrixRain } from '../context/MatrixRainContext';
import { parseFullSysCheckMessage, type FullSysCheckReport } from '../types/syscheck';
import type { WSCommandResult } from '../types/ws';
import { sanitizeScreenshotBase64 } from '../help/screenshotDownload';
import FullSysCheckPanel from '../components/Fleet/FullSysCheckPanel';
import CrucibleExpandedOps from '../components/Fleet/CrucibleExpandedOps';
import AccessDepthPanel from '../components/Fleet/AccessDepthPanel';
import LotlAttemptsList from '../components/Fleet/LotlAttemptsList';
import LotlTierBadge from '../components/Fleet/LotlTierBadge';
import RiskBadge from '../components/Fleet/RiskBadge';
import FleetHeatMiniMap from '../components/Fleet/FleetHeatMiniMap';
import { parseTierReport } from '../types/lotl';
import { parseAccessDepthDiagnostics, type AccessDepthDiagnostics } from '../help/accessDepth';
import { platformIcon } from '../help/platform';
import AlsoHere from '../components/Presence/AlsoHere';
import { HelpTip } from '../components/HelpTip';
import '../components/Fleet/FullSysCheckPanel.css';
import '../components/Fleet/FleetToolbar.css';
import { TERM_RENDER_CAP, visibleTerminalLines } from '../help/terminalRenderCap';
import './CruciblePage.css';

// ── Types ──────────────────────────────────────────────────────────────────

type ShellType = 'powershell' | 'exec' | 'sh';

interface TermLine {
  id: string;
  agentId: string;
  agentName: string;
  isCmd: boolean;
  text: string;
  ts: Date;
  success?: boolean;
  // Whether this agent was in the active selection when the command was dispatched
  targeted?: boolean;
  // Structured data for rich terminal renderers
  richData?: RichTermData;
}

// ── Rich terminal data types ────────────────────────────────────────────────

interface RichListenPort {
  port: number;
  addr: string;
  proto: string;
  process?: string;
  pid?: number;
}

interface RichListenPorts {
  type: 'listen_ports';
  ports: RichListenPort[];
  count: number;
}

interface RichPatchStatus {
  type: 'patch_status';
  pending_updates?: number;
  last_patch?: string;
  last_patch_days?: number;
  reboot_pending?: boolean;
}

interface RichPostureSummary {
  type: 'posture';
  posture_score?: number;
  defender_enabled?: boolean;
  defender_rtp?: boolean;
  av_products?: string[];
  firewall_domain?: boolean;
  firewall_private?: boolean;
  firewall_public?: boolean;
  ssh_listening?: boolean;
  agent_elevated?: boolean;
  last_patch_days?: number;
  pending_updates?: number;
  reboot_pending?: boolean;
  services?: Array<{ name: string; display_name?: string; status: string; start_type: string }>;
}

interface RichScreenshot {
  type: 'screenshot';
  b64: string;
}

interface RichFullSysCheck {
  type: 'full_sys_check';
  report: FullSysCheckReport;
}

interface RichMiningDiagnostics {
  type: 'mining_diagnostics';
  generated_at?: string;
  active_method?: string;
  execution_mode?: string;
  likely_blockers: string[];
  av_recommendation?: string;
  lotl_tier?: string;
  lotl_attempts?: import('../types/lotl').TierAttempt[];
  mining_hashrate?: number;
}

type RichTermData =
  | RichListenPorts
  | RichPatchStatus
  | RichPostureSummary
  | RichScreenshot
  | RichFullSysCheck
  | RichMiningDiagnostics;

// ── Helpers ────────────────────────────────────────────────────────────────

const AGENT_COLORS = [
  '#00e8f5', '#39ff14', '#ff2da6', '#b24bf3',
  '#ffb020', '#ff6b35', '#00d4aa', '#f72585',
  '#7209b7', '#3a86ff', '#06d6a0', '#ffd60a',
];

export function agentColor(agentId: string, allIds: string[], groupColor?: string): string {
  if (groupColor) return groupColor;
  const idx = allIds.indexOf(agentId);
  return AGENT_COLORS[idx % AGENT_COLORS.length] ?? '#00e8f5';
}

export function sshBadge(agent: Agent) {
  if (agent.ssh_available === true)  return { label: 'SSH ON',  cls: 'ssh-on' };
  if (agent.ssh_available === false) return { label: 'SSH OFF', cls: 'ssh-off' };
  return { label: 'SSH ?', cls: 'ssh-unk' };
}

export function postureBadge(score?: number) {
  if (score === undefined) return { label: 'POSTURE ?', cls: 'posture-unk' };
  if (score >= 80) return { label: `POSTURE ${score}`, cls: 'posture-good' };
  if (score >= 40) return { label: `POSTURE ${score}`, cls: 'posture-warn' };
  return { label: `POSTURE ${score}`, cls: 'posture-bad' };
}

export function patchLabel(days?: number) {
  if (days === undefined) return null;
  return { label: `PATCH ${days}d`, cls: days <= 30 ? 'patch-ok' : 'patch-stale' };
}

export function portsBadge(count?: number): { label: string; cls: string } | null {
  if (count === undefined) return null;
  return { label: `PORTS ${count}`, cls: count > 20 ? 'ports-many' : 'ports-ok' };
}

export function postureTooltip(agent: Agent): string {
  const lines: string[] = [];
  const yn = (v?: boolean) => v === true ? '✓' : v === false ? '✗' : '?';
  const na = (v: unknown) => v !== undefined && v !== null ? String(v) : '?';

  lines.push(`Defender: ${yn(agent.defender_enabled)}  RTP: ${yn(agent.defender_rtp)}`);
  if (agent.av_products?.length) lines.push(`AV: ${agent.av_products.join(', ')}`);
  lines.push(`FW  Domain:${yn(agent.firewall_domain)}  Private:${yn(agent.firewall_private)}  Public:${yn(agent.firewall_public)}`);
  lines.push(`SSH: ${yn(agent.ssh_available)}  Elevated: ${yn(agent.agent_elevated)}`);
  if (agent.listen_port_count !== undefined) lines.push(`TCP listeners: ${agent.listen_port_count}  (run listen_ports for full list)`);
  lines.push('──────────────────────');

  // Patch exposure
  if (agent.last_patch) lines.push(`Last patch:  ${agent.last_patch}  (${na(agent.last_patch_days)}d ago)`);
  else if (agent.last_patch_days !== undefined) lines.push(`Last patch:  ${agent.last_patch_days}d ago`);
  if (agent.pending_updates !== undefined) {
    const u = agent.pending_updates;
    lines.push(`Pending updates: ${u < 0 ? 'unknown' : u === 0 ? 'none ✓' : `${u} ⚠`}`);
  }
  if (agent.reboot_pending !== undefined) {
    lines.push(`Reboot required: ${agent.reboot_pending ? 'YES ⚠' : 'no ✓'}`);
  }
  // DNS config
  if (agent.dns_servers?.length) {
    lines.push('──────────────────────');
    lines.push(`DNS (T1016): ${agent.dns_servers.join(', ')}`);
    if (agent.dns_search_domains?.length) lines.push(`Search: ${agent.dns_search_domains.join(', ')}`);
    if (agent.dns_drifted) lines.push('⚠ DNS changed since last heartbeat!');
  }

  // Resource pressure
  const tempLabel = agent.gpu_temp_c !== undefined ? `GPU ${agent.gpu_temp_c}°C` : agent.cpu_temp_c !== undefined ? `CPU ${agent.cpu_temp_c}°C` : null;
  if (tempLabel || agent.disk_free_pct !== undefined || agent.cpu_throttle !== undefined) {
    lines.push('──────────────────────');
    if (tempLabel) lines.push(`Temp: ${tempLabel}${agent.cpu_throttle ? '  THROTTLED' : ''}`);
    if (agent.disk_free_pct !== undefined) {
      lines.push(`Disk: ${agent.disk_free_gb?.toFixed(1) ?? '?'} GB free  (${agent.disk_free_pct}% of ${agent.disk_total_gb?.toFixed(0) ?? '?'} GB)`);
    }
    if (agent.cpu_freq_mhz && agent.cpu_max_mhz) {
      lines.push(`CPU freq: ${agent.cpu_freq_mhz} / ${agent.cpu_max_mhz} MHz`);
    }
    if (agent.gpu_usage_pct !== undefined) lines.push(`GPU util: ${agent.gpu_usage_pct}%`);
  }

  if (agent.services?.length) {
    lines.push('──────────────────────');
    lines.push('Services (T1007):');
    for (const svc of agent.services) {
      const icon = svc.status === 'running' ? '●' : svc.status === 'stopped' ? '○' : '—';
      const st = svc.start_type !== 'unknown' ? ` [${svc.start_type}]` : '';
      lines.push(`  ${icon} ${svc.display_name ?? svc.name}${st}`);
    }
  }
  return lines.join('\n');
}

export function pendingBadge(agent: Agent): { label: string; cls: string } | null {
  const u = agent.pending_updates;
  if (u === undefined) return null;
  if (u < 0)  return { label: 'UPD ?',   cls: 'upd-unk' };
  if (u === 0) return { label: 'UP TO DATE', cls: 'upd-ok' };
  if (u <= 5)  return { label: `${u} UPD`,   cls: 'upd-warn' };
  return             { label: `${u} UPD`,   cls: 'upd-bad' };
}

function rebootBadge(agent: Agent): { label: string; cls: string } | null {
  if (agent.reboot_pending === undefined) return null;
  if (agent.reboot_pending) return { label: 'REBOOT!', cls: 'rb-pending' };
  return null;
}

// ── Resource pressure badges ───────────────────────────────────────────────

export function thermalBadge(agent: Agent): { label: string; cls: string } | null {
  const t = agent.gpu_temp_c ?? agent.cpu_temp_c;
  if (t === undefined) return null;
  if (t > 80) return { label: `${t}°`, cls: 'therm-hot' };
  if (t > 65) return { label: `${t}°`, cls: 'therm-warm' };
  return null; // cool enough — no badge clutter
}

function diskBadge(agent: Agent): { label: string; cls: string } | null {
  const pct = agent.disk_free_pct;
  if (pct === undefined) return null;
  if (pct < 5)  return { label: `DISK ${pct}%`,  cls: 'disk-crit' };
  if (pct < 15) return { label: `DISK ${pct}%`,  cls: 'disk-warn' };
  return null; // plenty of space — no badge
}

function throttleBadge(agent: Agent): { label: string; cls: string } | null {
  if (!agent.cpu_throttle) return null;
  const pct = agent.cpu_freq_mhz && agent.cpu_max_mhz
    ? Math.round(agent.cpu_freq_mhz / agent.cpu_max_mhz * 100)
    : null;
  const label = pct !== null ? `THRTTL ${pct}%` : 'THRTTL';
  return { label, cls: 'therm-warm' };
}

// ── DNS helpers (T1016) ────────────────────────────────────────────────────

function dnsBadge(agent: Agent): { label: string; cls: string } | null {
  if (agent.dns_drifted) return { label: 'DNS DRIFT', cls: 'dns-drift' };
  return null;
}

/** Contingency onion depth — Fleet AI Control single-host branch tree. */
export function contingencyDepthBadge(agent: Agent): { label: string; cls: string } | null {
  const depth = agent.contingency_depth ?? 0;
  if (depth <= 0) return null;
  return { label: `ONION ${depth}`, cls: depth >= 8 ? 'cn-contingency-deep' : 'cn-contingency' };
}

// ── Service helpers (T1007) ────────────────────────────────────────────────

// Human-readable label for well-known service names
const SVC_LABELS: Record<string, string> = {
  sshd: 'SSH',
  ssh: 'SSH',
  'openssh ssh server': 'SSH',
  cloudflared: 'CF Tunnel',
  wuauserv: 'WU',
  windefend: 'Defender',
  ufw: 'UFW',
  fail2ban: 'Fail2Ban',
};

function svcLabel(svc: AgentService): string {
  return SVC_LABELS[svc.name.toLowerCase()] ?? svc.display_name ?? svc.name;
}

function svcDot(status: string): string {
  if (status === 'running') return '●';
  if (status === 'stopped') return '○';
  return '—';
}

function svcDotClass(status: string): string {
  if (status === 'running') return 'svc-run';
  if (status === 'stopped') return 'svc-stop';
  return 'svc-unk';
}

// Only surface services that are interesting to show (skip self-service clutter)
const IMPORTANT_SVCS = new Set(['sshd', 'ssh', 'openssh ssh server', 'cloudflared', 'wuauserv', 'windefend']);
function importantServices(svcs: AgentService[]): AgentService[] {
  return svcs.filter(s => IMPORTANT_SVCS.has(s.name.toLowerCase()) || s.status === 'running');
}

let _lineId = 0;
function mkId() { return `tl-${++_lineId}`; }

// ── Wake SSH commands ──────────────────────────────────────────────────────

const WAKE_SSH_PS = `
$ErrorActionPreference='SilentlyContinue'
$cap=Get-WindowsCapability -Online -Name OpenSSH.Server~~~~* 2>$null
if ($cap -and $cap.State -ne 'Installed'){Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0}
Set-Service -Name sshd -StartupType Automatic
Start-Service sshd
$ip=(Get-NetIPAddress -AddressFamily IPv4|Where{$_.InterfaceAlias -notlike '*Loopback*'}|Select -First 1).IPAddress
"SSH_WAKE_OK ip=$ip port=22"
`.trim();

const PROBE_SSH_PS = `
$s=Get-Service sshd -ErrorAction SilentlyContinue
if($s -and $s.Status -eq 'Running'){'SSH_PROBE:ONLINE'}else{'SSH_PROBE:OFFLINE'}
`.trim();

const PROBE_SSH_SH = `ss -tlnp 2>/dev/null | grep -q ':22' && echo SSH_PROBE:ONLINE || echo SSH_PROBE:OFFLINE`;

/** Roster page size — avoids rendering 500+ node cards at once. */
const ROSTER_PAGE_SIZE = 80;

// ── Component ──────────────────────────────────────────────────────────────

export default function CruciblePage() {
  const { agents, commandResults, latestMessage } = useWebSocket();
  const { setCrucibleFocus } = useMatrixRain();

  // Selection
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
  const [filters, setFilters] = useState<FleetFilterState>(DEFAULT_FLEET_FILTERS);
  const [showGroupModal, setShowGroupModal] = useState(false);
  const { groups, addGroup, removeGroup } = useFleetGroups();
  const { bulkBusy, handleBulkAction } = useFleetBulkActions({
    agents,
    selectedIds,
    commandResults,
  });

  // Terminal
  const [termLines, setTermLines] = useState<TermLine[]>([]);
  const [cmd, setCmd] = useState('');
  const [shellType, setShellType] = useState<ShellType>('powershell');
  const [busy, setBusy] = useState(false);
  const termEndRef = useRef<HTMLDivElement>(null);
  const cmdRef = useRef<HTMLInputElement>(null);
  const lastSeqRef = useRef(0);
  const lastLatestCmdRef = useRef<typeof latestMessage>(null);

  // Command history
  const [cmdHistory, setCmdHistory] = useState<string[]>([]);
  const [histIdx, setHistIdx] = useState(-1);

  const [tunnelStatusMsg, setTunnelStatusMsg] = useState('');
  const [activeTab, setActiveTab] = useState<'ops' | 'recon' | 'files' | 'spread' | 'tunnels'>('ops');
  const [rosterPage, setRosterPage] = useState(0);

  // SSH / posture overrides (from on-demand probes)
  const [sshOverride, setSshOverride] = useState<Record<string, boolean>>({});
  const [postureOverride, setPostureOverride] = useState<Record<string, { score: number; patchDays?: number }>>({});
  const [accessDepthByAgent, setAccessDepthByAgent] = useState<Record<string, AccessDepthDiagnostics>>({});

  const allIds = useMemo(() => agents.map((a) => a.id), [agents]);

  const sortedAgents = useMemo(() => [...agents].sort((a, b) => {
    if (a.status === 'online' && b.status !== 'online') return -1;
    if (a.status !== 'online' && b.status === 'online') return 1;
    const ta = a.last_seen ? new Date(a.last_seen).getTime() : 0;
    const tb = b.last_seen ? new Date(b.last_seen).getTime() : 0;
    if (tb !== ta) return tb - ta;
    return a.name.localeCompare(b.name);
  }), [agents]);

  const filteredAgents = useMemo(
    () => filterFleetAgents(sortedAgents, filters),
    [sortedAgents, filters],
  );

  const rosterPageCount = Math.max(1, Math.ceil(filteredAgents.length / ROSTER_PAGE_SIZE));
  const rosterPageSafe = Math.min(rosterPage, rosterPageCount - 1);
  const rosterSlice = useMemo(() => {
    const start = rosterPageSafe * ROSTER_PAGE_SIZE;
    return filteredAgents.slice(start, start + ROSTER_PAGE_SIZE);
  }, [filteredAgents, rosterPageSafe]);

  useEffect(() => {
    setRosterPage(0);
  }, [filters]);
  const selectedAgents = useMemo(
    () => agents.filter((a) => selectedIds.has(a.id)),
    [agents, selectedIds]
  );

  const online = (a: Agent) => a.status === 'online';

  const fmCommandResults = useMemo(
    () =>
      commandResults
        ?.filter((r) => r.agent_id && r.action != null)
        .map((r) => ({
          agentId: r.agent_id as string,
          action: r.action as string,
          success: !!r.success,
          message: r.message ?? '',
        })) ?? [],
    [commandResults]
  );

  const singleSelectedAgent = selectedAgents.length === 1 ? selectedAgents[0] : null;

  const encryptTargets = useMemo(
    () => selectedAgents.filter(online).map((a) => ({ id: a.id, name: a.name })),
    [selectedAgents]
  );

  const browseAgent = singleSelectedAgent ?? selectedAgents.find(online) ?? null;

  const { visible: visibleTermLines, truncated: termTruncated, hidden: termHidden } = useMemo(
    () => visibleTerminalLines(termLines),
    [termLines],
  );

  const dispatchTunnelCommand = useCallback(
    async (action: string, args?: Record<string, unknown>) => {
      if (!singleSelectedAgent) return;
      await api.sendAgentCommand(singleSelectedAgent.id, action, args);
      setTermLines((prev) => [
        ...prev,
        {
          id: mkId(),
          agentId: 'local',
          agentName: 'YOU',
          isCmd: true,
          text: `${action} → ${singleSelectedAgent.name}`,
          ts: new Date(),
        },
      ]);
    },
    [singleSelectedAgent]
  );

  const appendTerminalLine = useCallback((text: string, isCmd = false) => {
    setTermLines((prev) => [
      ...prev,
      {
        id: mkId(),
        agentId: 'local',
        agentName: 'YOU',
        isCmd,
        text,
        ts: new Date(),
        targeted: true,
      },
    ].slice(-2000));
  }, []);

  /** One online target selected — sidebar matrix switches to gold forge-style rain. */
  const crucibleTargetReady =
    selectedAgents.filter(online).length === 1 && selectedIds.size === 1;

  useEffect(() => {
    setCrucibleFocus(crucibleTargetReady);
    return () => setCrucibleFocus(false);
  }, [crucibleTargetReady, setCrucibleFocus]);

  // Prune selectedIds when agents are removed (e.g. after roster delete).
  useEffect(() => {
    const liveIds = new Set(agents.map((a) => a.id));
    setSelectedIds((prev) => {
      const pruned = new Set([...prev].filter((id) => liveIds.has(id)));
      return pruned.size === prev.size ? prev : pruned;
    });
  }, [agents]);

  // ── Auto-scroll terminal ───────────────────────────────────────────────

  useEffect(() => {
    const id = requestAnimationFrame(() => {
      termEndRef.current?.scrollIntoView({ behavior: 'auto', block: 'end' });
    });
    return () => cancelAnimationFrame(id);
  }, [termLines.length]);

  // ── Process incoming command_result messages ───────────────────────────

  useEffect(() => {
    if (!commandResults || commandResults.length === 0) return;
    const newEntries = commandResults.filter(
      (r) => typeof r._seq === 'number' && r._seq > lastSeqRef.current,
    );
    if (newEntries.length === 0) return;

    const lines: TermLine[] = [];
    let maxSeq = lastSeqRef.current;

    for (const r of newEntries) {
      if (typeof r._seq === 'number') {
        maxSeq = Math.max(maxSeq, r._seq);
      }
      const aid = r.agent_id;
      if (!aid) continue;

      const msg = r.message ?? '';

      if (r.action === 'tunnel_status' && r.success && msg) {
        if (selectedIds.size === 1 && selectedIds.has(aid)) {
          setTunnelStatusMsg(msg);
        }
      }

      // ── SSH badge updates ───────────────────────────────────────────────
      if (msg.includes('SSH_PROBE:ONLINE')) {
        setSshOverride((prev) => ({ ...prev, [aid]: true }));
      } else if (msg.includes('SSH_PROBE:OFFLINE')) {
        setSshOverride((prev) => ({ ...prev, [aid]: false }));
      }

      // ── Parse structured JSON for known actions ────────────────────────
      let richData: RichTermData | undefined;

      // Screenshot: result is a raw base64 PNG string (no JSON wrapper)
      if ((r.action === 'screenshot' || r.action === 'camera_snapshot') && r.success) {
        const cleanB64 = sanitizeScreenshotBase64(msg);
        if (cleanB64) {
          richData = { type: 'screenshot', b64: cleanB64 };
        }
      }

      const jsonStart = msg.indexOf('{');

      if (jsonStart >= 0) {
        try {
          const parsed = JSON.parse(msg.slice(jsonStart));

          if (r.action === 'listen_ports' && Array.isArray(parsed.ports)) {
            richData = { type: 'listen_ports', ports: parsed.ports, count: parsed.count ?? parsed.ports.length };
          } else if (r.action === 'patch_status') {
            richData = { type: 'patch_status', ...parsed };
          } else if (r.action === 'full_sys_check' && parsed.generated_at) {
            richData = { type: 'full_sys_check', report: parsed as FullSysCheckReport };
          } else if (r.action === 'posture' && typeof parsed.posture_score === 'number') {
            richData = { type: 'posture', ...parsed };
            // Update badge state
            setPostureOverride((prev) => ({
              ...prev,
              [aid]: { score: parsed.posture_score, patchDays: parsed.last_patch_days },
            }));
            if (parsed.ssh_listening === true) setSshOverride((prev) => ({ ...prev, [aid]: true }));
            if (parsed.ssh_listening === false) setSshOverride((prev) => ({ ...prev, [aid]: false }));
          } else if (r.action === 'mining_diagnostics') {
            const blockers = parsed.likely_blockers ?? parsed.blockers;
            const tierFields = parseTierReport(parsed as Record<string, unknown>);
            const depthDiag = parseAccessDepthDiagnostics(parsed as Record<string, unknown>);
            setAccessDepthByAgent((prev) => ({ ...prev, [aid]: depthDiag }));
            if (Array.isArray(blockers) || tierFields.lotl_attempts.length > 0) {
              richData = {
                type: 'mining_diagnostics',
                generated_at: parsed.generated_at,
                active_method: parsed.active_method,
                execution_mode: parsed.execution_mode,
                likely_blockers: Array.isArray(blockers)
                  ? blockers.filter((b: unknown) => typeof b === 'string')
                  : [],
                av_recommendation: parsed.av_recommendation,
                lotl_tier: tierFields.lotl_tier,
                lotl_attempts: tierFields.lotl_attempts,
                mining_hashrate: tierFields.mining_hashrate,
              };
            }
          } else {
            // Generic JSON with posture fields (legacy path)
            if (typeof parsed.posture_score === 'number') {
              setPostureOverride((prev) => ({
                ...prev,
                [aid]: { score: parsed.posture_score, patchDays: parsed.last_patch_days },
              }));
            }
            if (parsed.ssh_listening === true) setSshOverride((prev) => ({ ...prev, [aid]: true }));
            if (parsed.ssh_listening === false) setSshOverride((prev) => ({ ...prev, [aid]: false }));
          }
        } catch { /* malformed JSON — fall through to plain text */ }
      }

      const agent = agents.find((a) => a.id === aid);
      const name = agent?.name ?? aid.slice(0, 8);

      const targeted = selectedIds.size === 0 || selectedIds.has(aid);
      if (richData) {
        lines.push({
          id: mkId(), agentId: aid, agentName: name,
          isCmd: false, text: '', ts: new Date(),
          success: r.success, richData, targeted,
        });
      } else {
        const msgLines = msg.split('\n').filter(Boolean);
        if (msgLines.length === 0) {
          const label = r.action ? `[${r.action}]` : '[result]';
          msgLines.push(r.success ? `${label} OK` : `${label} FAILED`);
        }
        for (const line of msgLines) {
          lines.push({
            id: mkId(), agentId: aid, agentName: name,
            isCmd: false, text: line, ts: new Date(), success: r.success, targeted,
          });
        }
      }
    }

    lastSeqRef.current = maxSeq;
    if (lines.length > 0) {
      setTermLines((prev) => [...prev, ...lines].slice(-2000));
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [commandResults]);

  // Backup path: if commandResults batching ever misses an entry, latestMessage
  // still carries command_result (WS_LATEST_MESSAGE_TYPES includes it).
  useEffect(() => {
    if (!latestMessage || latestMessage.type !== 'command_result') return;
    if (latestMessage === lastLatestCmdRef.current) return;
    lastLatestCmdRef.current = latestMessage;

    let r = latestMessage.payload as WSCommandResult | string;
    if (typeof r === 'string') {
      try {
        r = JSON.parse(r) as WSCommandResult;
      } catch {
        return;
      }
    }
    const aid = r.agent_id;
    if (!aid) return;

    // commandResults effect owns entries already queued in the provider buffer
    if (
      commandResults?.some(
        (c) => c.agent_id === aid && c.action === r.action && c.message === r.message,
      )
    ) {
      return;
    }

    const msg = r.message ?? '';
    const agent = agents.find((a) => a.id === aid);
    const name = agent?.name ?? aid.slice(0, 8);
    const targeted = selectedIds.size === 0 || selectedIds.has(aid);
    const msgLines = msg.split('\n').filter(Boolean);
    if (msgLines.length === 0) {
      const label = r.action ? `[${r.action}]` : '[result]';
      msgLines.push(r.success ? `${label} OK` : `${label} FAILED`);
    }
    const lines: TermLine[] = msgLines.map((line) => ({
      id: mkId(),
      agentId: aid,
      agentName: name,
      isCmd: false,
      text: line,
      ts: new Date(),
      success: r.success,
      targeted,
    }));
    setTermLines((prev) => [...prev, ...lines].slice(-2000));
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [latestMessage, commandResults]);

  // ── Selection helpers ──────────────────────────────────────────────────

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

  const selectAgent = (id: string) => setSelectedIds(new Set([id]));

  const selectAll = () => setSelectedIds(new Set(agents.filter(online).map((a) => a.id)));
  const clearSel = () => setSelectedIds(new Set());

  const onlineAgentIds = useMemo(
    () => new Set(agents.filter((a) => a.status === 'online').map((a) => a.id)),
    [agents]
  );

  const activateGroup = (g: { agentIds: string[] }) => setSelectedIds(new Set(g.agentIds));

  // ── Dispatch command ───────────────────────────────────────────────────

  const dispatch = useCallback(async (command: string, shell: ShellType, targets?: Agent[]) => {
    const tgts = targets ?? selectedAgents.filter(online);
    if (tgts.length === 0 || !command.trim()) return;

    setBusy(true);
    // Echo command to terminal
    const echoLines: TermLine[] = tgts.map((a) => ({
      id: mkId(),
      agentId: a.id,
      agentName: a.name,
      isCmd: true,
      text: command,
      ts: new Date(),
    }));
    setTermLines((prev) => [...prev, ...echoLines].slice(-2000));
    setCmdHistory((h) => [command, ...h].slice(0, 50));
    setHistIdx(-1);

    const action = shell === 'powershell' ? 'powershell'
      : shell === 'sh' ? 'exec'
      : 'exec';

    await Promise.all(
      tgts.map(async (a) => {
        try {
          const res = await api.sendAgentCommand(a.id, action, { command });
          if (res.success === false) {
            setTermLines((prev) => [
              ...prev,
              {
                id: mkId(),
                agentId: a.id,
                agentName: a.name,
                isCmd: false,
                text: `[ERROR] ${res.error ?? 'command rejected'}`,
                ts: new Date(),
                success: false,
              },
            ]);
          }
        } catch (err) {
          setTermLines((prev) => [
            ...prev,
            {
              id: mkId(),
              agentId: a.id,
              agentName: a.name,
              isCmd: false,
              text: `[ERROR] ${err instanceof Error ? err.message : String(err)}`,
              ts: new Date(),
              success: false,
            },
          ]);
        }
      })
    );
    setBusy(false);
    cmdRef.current?.focus();
  }, [selectedAgents]);

  const sendCmd = () => {
    dispatch(cmd, shellType);
    setCmd('');
  };

  const probeSSH = (targets?: Agent[]) => {
    const tgts = targets ?? selectedAgents.filter(online);
    for (const a of tgts) {
      const isWin = a.platform?.toLowerCase().includes('win') ?? true;
      const probeCmd = isWin ? PROBE_SSH_PS : PROBE_SSH_SH;
      const shell: ShellType = isWin ? 'powershell' : 'sh';
      dispatch(probeCmd, shell, [a]);
    }
  };

  const wakeSSH = (targets?: Agent[]) => {
    const tgts = targets ?? selectedAgents.filter(online);
    for (const a of tgts) {
      const isWin = a.platform?.toLowerCase().includes('win') ?? true;
      if (isWin) {
        dispatch(WAKE_SSH_PS, 'powershell', [a]);
      } else {
        dispatch('which sshd && systemctl start sshd 2>/dev/null || service ssh start 2>/dev/null; echo SSH_WAKE_OK', 'sh', [a]);
      }
    }
  };

  const probePosture = (targets?: Agent[]) => {
    const tgts = targets ?? selectedAgents.filter(online);
    if (tgts.length === 0) { alert('No online agents selected.'); return; }
    Promise.all(
      tgts.map((a) =>
        api.sendAgentCommand(a.id, 'posture').catch((err) => {
          setTermLines((prev) => [
            ...prev,
            {
              id: mkId(), agentId: a.id, agentName: a.name, isCmd: false,
              text: `[ERROR] posture probe: ${err instanceof Error ? err.message : String(err)}`,
              ts: new Date(), success: false, targeted: selectedIds.has(a.id) || selectedIds.size === 0,
            },
          ]);
        })
      )
    );
  };

  // Fires posture + listen_ports + patch_status in parallel.
  // When agents are selected, targets only selection. Otherwise targets all online.
  const scanSelected = (targets?: Agent[]) => {
    const tgts = targets ?? (selectedAgents.filter(online).length > 0
      ? selectedAgents.filter(online)
      : agents.filter(online));
    if (tgts.length === 0) { alert('No online agents available.'); return; }
    const cmds = ['posture', 'listen_ports', 'patch_status'] as const;
    for (const a of tgts) {
      for (const cmd of cmds) {
        api.sendAgentCommand(a.id, cmd).catch((err) => {
          setTermLines((prev) => [
            ...prev,
            {
              id: mkId(), agentId: a.id, agentName: a.name, isCmd: false,
              text: `[ERROR] ${cmd}: ${err instanceof Error ? err.message : String(err)}`,
              ts: new Date(), success: false, targeted: selectedIds.has(a.id) || selectedIds.size === 0,
            },
          ]);
        });
      }
    }
  };

  // Focused agent — when exactly one is selected show its details prominently.
  const focusedAgent = selectedAgents.length === 1 ? selectedAgents[0] : null;

  const handleKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') { sendCmd(); return; }
    if (e.key === 'ArrowUp') {
      e.preventDefault();
      const next = Math.min(histIdx + 1, cmdHistory.length - 1);
      setHistIdx(next);
      setCmd(cmdHistory[next] ?? '');
    }
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      const next = Math.max(histIdx - 1, -1);
      setHistIdx(next);
      setCmd(next < 0 ? '' : cmdHistory[next] ?? '');
    }
  };

  // ── Effective SSH status ───────────────────────────────────────────────

  const sshStatus = (a: Agent) => {
    const override = sshOverride[a.id];
    if (override !== undefined) return { label: override ? 'SSH ON' : 'SSH OFF', cls: override ? 'ssh-on' : 'ssh-off' };
    return sshBadge({ ...a });
  };

  const postureStatus = (a: Agent) => {
    const o = postureOverride[a.id];
    const score = o?.score ?? a.posture_score;
    const patchDays = o?.patchDays ?? a.last_patch_days;
    return {
      ...postureBadge(score),
      patch: patchLabel(patchDays),
      ports: portsBadge(a.listen_port_count),
    };
  };

  // ── Rich terminal renderers ────────────────────────────────────────────

  const RichListenPortsTable = ({ d }: { d: RichListenPorts }) => (
    <div className="rich-block rich-ports">
      <div className="rich-header">
        <span className="rich-label">LISTEN PORTS</span>
        <span className="rich-count">{d.count} listener{d.count !== 1 ? 's' : ''}</span>
      </div>
      {d.ports.length === 0 ? (
        <div className="rich-empty">No listeners found</div>
      ) : (
        <table className="rich-table">
          <thead>
            <tr>
              <th>PORT</th>
              <th>ADDR</th>
              <th>PROTO</th>
              <th>PROCESS</th>
              <th>PID</th>
            </tr>
          </thead>
          <tbody>
            {d.ports.map((p, i) => (
              <tr key={i} className={p.port === 22 ? 'rich-row-ssh' : p.port < 1024 ? 'rich-row-sys' : ''}>
                <td className="rich-port">{p.port}</td>
                <td className="rich-addr">{p.addr || '*'}</td>
                <td className="rich-proto">{p.proto.toUpperCase()}</td>
                <td className="rich-proc">{p.process || '—'}</td>
                <td className="rich-pid">{p.pid ?? '—'}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );

  const RichPatchStatusBlock = ({ d }: { d: RichPatchStatus }) => (
    <div className="rich-block rich-patch">
      <div className="rich-header">
        <span className="rich-label">PATCH STATUS</span>
        {d.reboot_pending && <span className="rich-tag rich-tag-warn blink-slow">REBOOT REQUIRED</span>}
      </div>
      <div className="rich-kv-row">
        <span className="rich-key">Pending Updates</span>
        <span className={`rich-val ${(d.pending_updates ?? 0) > 0 ? 'rich-val-warn' : 'rich-val-ok'}`}>
          {d.pending_updates ?? 0}
        </span>
      </div>
      <div className="rich-kv-row">
        <span className="rich-key">Last Patch</span>
        <span className={`rich-val ${(d.last_patch_days ?? 0) > 60 ? 'rich-val-bad' : (d.last_patch_days ?? 0) > 30 ? 'rich-val-warn' : 'rich-val-ok'}`}>
          {d.last_patch ?? '—'}{d.last_patch_days != null ? ` (${d.last_patch_days}d ago)` : ''}
        </span>
      </div>
      <div className="rich-kv-row">
        <span className="rich-key">Reboot Pending</span>
        <span className={`rich-val ${d.reboot_pending ? 'rich-val-warn' : 'rich-val-ok'}`}>
          {d.reboot_pending ? 'YES' : 'NO'}
        </span>
      </div>
    </div>
  );

  const fwIcon = (on?: boolean) => on ? <span className="rich-val-ok">ON</span> : <span className="rich-val-bad">OFF</span>;

  const RichPostureSummaryBlock = ({ d }: { d: RichPostureSummary }) => (
    <div className="rich-block rich-posture">
      <div className="rich-header">
        <span className="rich-label">POSTURE REPORT</span>
        {d.posture_score != null && (
          <span className={`rich-score ${d.posture_score >= 70 ? 'rich-score-ok' : d.posture_score >= 40 ? 'rich-score-warn' : 'rich-score-bad'}`}>
            {d.posture_score} / 100
          </span>
        )}
      </div>
      <div className="rich-posture-grid">
        <div className="rich-kv-row"><span className="rich-key">Defender</span>
          <span className={`rich-val ${d.defender_enabled ? 'rich-val-ok' : 'rich-val-bad'}`}>{d.defender_enabled ? 'ON' : 'OFF'}</span>
        </div>
        <div className="rich-kv-row"><span className="rich-key">Real-Time Prot</span>
          <span className={`rich-val ${d.defender_rtp ? 'rich-val-ok' : 'rich-val-warn'}`}>{d.defender_rtp ? 'ON' : 'OFF'}</span>
        </div>
        <div className="rich-kv-row"><span className="rich-key">Firewall Domain/Priv/Pub</span>
          <span className="rich-val">{fwIcon(d.firewall_domain)} / {fwIcon(d.firewall_private)} / {fwIcon(d.firewall_public)}</span>
        </div>
        <div className="rich-kv-row"><span className="rich-key">AV Products</span>
          <span className="rich-val">{d.av_products?.join(', ') || '—'}</span>
        </div>
        <div className="rich-kv-row"><span className="rich-key">SSH Listening</span>
          <span className={`rich-val ${d.ssh_listening ? 'rich-val-ok' : 'rich-val-bad'}`}>{d.ssh_listening ? 'YES' : 'NO'}</span>
        </div>
        <div className="rich-kv-row"><span className="rich-key">Agent Elevated</span>
          <span className={`rich-val ${d.agent_elevated ? 'rich-val-warn' : 'rich-val-ok'}`}>{d.agent_elevated ? 'YES (ADMIN)' : 'no'}</span>
        </div>
        {d.last_patch_days != null && (
          <div className="rich-kv-row"><span className="rich-key">Last Patch</span>
            <span className={`rich-val ${d.last_patch_days > 60 ? 'rich-val-bad' : d.last_patch_days > 30 ? 'rich-val-warn' : 'rich-val-ok'}`}>
              {d.last_patch_days}d ago{d.pending_updates ? ` · ${d.pending_updates} pending` : ''}
              {d.reboot_pending ? ' · REBOOT!' : ''}
            </span>
          </div>
        )}
      </div>
      {d.services && d.services.length > 0 && (
        <>
          <div className="rich-sub-label">SERVICES</div>
          <table className="rich-table">
            <thead><tr><th>SERVICE</th><th>STATUS</th><th>START</th></tr></thead>
            <tbody>
              {d.services.map((s, i) => (
                <tr key={i}>
                  <td className="rich-proc">{s.display_name || s.name}</td>
                  <td className={`rich-svc-status ${s.status === 'running' ? 'rich-val-ok' : 'rich-val-bad'}`}>{s.status.toUpperCase()}</td>
                  <td className="rich-addr">{s.start_type}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </>
      )}
    </div>
  );

  const RichMiningDiagnosticsBlock = ({ d }: { d: RichMiningDiagnostics }) => (
    <div className="rich-block rich-mining-diag">
      <div className="rich-header">
        <span className="rich-label">MINING DIAGNOSTICS</span>
        {d.lotl_tier && <LotlTierBadge tier={d.lotl_tier} attempts={d.lotl_attempts} variant="inline" />}
        {d.active_method && <span className="rich-tag">{d.active_method}</span>}
      </div>
      {d.execution_mode && (
        <div className="rich-kv-row">
          <span className="rich-key">Execution</span>
          <span className="rich-val">{d.execution_mode}</span>
        </div>
      )}
      {(d.lotl_attempts?.length ?? 0) > 0 && (
        <LotlAttemptsList
          attempts={d.lotl_attempts ?? []}
          activeTier={d.lotl_tier}
          miningHashrate={d.mining_hashrate}
        />
      )}
      <div className="rich-sub-label">LIKELY BLOCKERS</div>
      {d.likely_blockers.length === 0 ? (
        <div className="rich-empty">No blockers detected</div>
      ) : (
        <ul className="rich-blocker-list">
          {d.likely_blockers.map((b, i) => (
            <li key={i} className="rich-blocker-item">{b}</li>
          ))}
        </ul>
      )}
    </div>
  );

  const renderRichData = (d: RichTermData, lineAgentName?: string) => {
    if (d.type === 'listen_ports') return <RichListenPortsTable d={d} />;
    if (d.type === 'patch_status') return <RichPatchStatusBlock d={d} />;
    if (d.type === 'posture') return <RichPostureSummaryBlock d={d} />;
    if (d.type === 'mining_diagnostics') return <RichMiningDiagnosticsBlock d={d} />;
    if (d.type === 'full_sys_check') {
      return <FullSysCheckPanel report={d.report} agentName={lineAgentName ?? 'agent'} />;
    }
    if (d.type === 'screenshot') return (
      <div style={{ marginTop: '0.4rem' }}>
        <img
          src={`data:image/png;base64,${d.b64}`}
          alt="screenshot"
          style={{ maxWidth: '100%', maxHeight: 340, borderRadius: 4, border: '1px solid #333', cursor: 'pointer' }}
          onClick={() => window.open(`data:image/png;base64,${d.b64}`, '_blank')}
          title="Click to open full size"
        />
      </div>
    );
    return null;
  };

  // ── Render ─────────────────────────────────────────────────────────────

  return (
    <div className="page fade-in crucible-page operator-deck-page">
      <header className="deck-hero" style={{ marginBottom: '1rem' }}>
        <div className="deck-hero-text">
          <p className="deck-eyebrow font-tech">REMOTE OPERATIONS THEATER</p>
          <h1>Crucible</h1>
          <p className="page-subtitle">select nodes · group them · command them all at once</p>
        </div>
        <div className="deck-hero-status">
          <div className="crucible-sel-summary">
            <span className="font-tech" style={{ color: 'var(--neon-cyan)' }}>
              {selectedIds.size > 0 ? `${selectedIds.size} selected` : 'none selected'}
            </span>
            <span style={{ color: 'var(--text-muted)', fontSize: '0.8rem' }}>
              {agents.filter(online).length} online / {agents.length} total
            </span>
          </div>
          <button className="button crucible-btn" onClick={selectAll}>Select Online</button>
          <button className="button crucible-btn-muted" onClick={clearSel}>Clear</button>
          {selectedIds.size > 0 && (
            <button className="button crucible-btn" onClick={() => setShowGroupModal(true)}>
              Create group…
            </button>
          )}
        </div>
      </header>

      <AlsoHere page="/crucible" />

      {agents.length > 0 && (
        <FleetToolbar
          agents={agents}
          filters={filters}
          onChange={setFilters}
          selectedCount={selectedIds.size}
          selectedAgents={selectedAgents}
          filteredCount={filteredAgents.length}
          onSelectAllFiltered={() => setSelectedIds(new Set(filteredAgents.map((a) => a.id)))}
          onBulkAction={handleBulkAction}
          onCreateGroup={() => setShowGroupModal(true)}
          bulkBusy={bulkBusy}
        />
      )}

      <FleetGroupsStrip
        groups={groups}
        liveAgentIds={onlineAgentIds}
        selectedCount={selectedIds.size}
        onSelectGroup={activateGroup}
        onDeleteGroup={removeGroup}
        onCreateGroup={() => setShowGroupModal(true)}
      />

      <div className="crucible-layout">
        <aside className="crucible-sidebar">
          <NeonCard
            accent="cyan"
            className="crucible-heat-card operator-deck-card operator-interactive"
            tilt3d={false}
          >
            <div className="crucible-section-title font-tech" style={{ marginBottom: '0.5rem' }}>
              <span className="section-ornament">◆</span> HEAT MAP <HelpTip field="crucible_heat_map" />
            </div>
            <FleetHeatMiniMap
              agents={agents}
              groups={groups}
              allIds={allIds}
              selectedIds={selectedIds}
              onSelectAgent={selectAgent}
            />
          </NeonCard>
        </aside>

        <div className="crucible-main">
      {/* ── Node Roster ─────────────────────────────────────────────────── */}
      <NeonCard accent="cyan" className="crucible-roster-card operator-deck-card operator-interactive" hud tilt3d={false}>
        <div className="crucible-section-title font-tech">
          <span className="section-ornament">◆</span> NODE ROSTER <HelpTip field="crucible_node_roster" />
        </div>
        {agents.length === 0 ? (
          <p className="form-hint" style={{ marginTop: '0.5rem' }}>No nodes registered. Forge a build and deploy it to your machines.</p>
        ) : filteredAgents.length === 0 ? (
          <p className="form-hint" style={{ marginTop: '0.5rem' }}>No nodes match filters.</p>
        ) : (
          <>
          {filteredAgents.length > ROSTER_PAGE_SIZE && (
            <div className="crucible-roster-pager" style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.5rem', fontSize: '0.8rem' }}>
              <button type="button" className="btn btn-outline btn-sm" disabled={rosterPageSafe <= 0} onClick={() => setRosterPage((p) => Math.max(0, p - 1))}>← Prev</button>
              <span className="font-tech">
                {rosterPageSafe * ROSTER_PAGE_SIZE + 1}–{Math.min((rosterPageSafe + 1) * ROSTER_PAGE_SIZE, filteredAgents.length)} of {filteredAgents.length}
              </span>
              <button type="button" className="btn btn-outline btn-sm" disabled={rosterPageSafe >= rosterPageCount - 1} onClick={() => setRosterPage((p) => Math.min(rosterPageCount - 1, p + 1))}>Next →</button>
            </div>
          )}
          <div className="crucible-roster">
            {rosterSlice.map((a) => {
              const sel = selectedIds.has(a.id);
              const isOn = online(a);
              const ssh = sshStatus(a);
              const posture = postureStatus(a);
              const pg = primaryGroupForAgent(groups, a.id);
              const color = agentColor(a.id, allIds, pg?.color);
              return (
                <div
                  key={a.id}
                  className={`crucible-node-card operator-interactive ${sel ? 'selected' : ''} ${isOn ? '' : 'offline'}`}
                  style={{
                    ...(sel ? { '--sel-color': color } : {}),
                    ...(pg ? { borderLeft: `3px solid ${pg.color}` } : {}),
                  } as React.CSSProperties}
                  onClick={() => toggle(a.id)}
                >
                  <div className="crucible-node-check">
                    <span className={`cn-checkbox ${sel ? 'checked' : ''}`}
                      style={sel ? { borderColor: color, background: color + '33' } : undefined} />
                  </div>
                  <div className="crucible-node-body">
                    <div className="cn-name" style={sel || pg ? { color: pg?.color ?? color } : undefined}>
                      <span className="cn-platform">{platformIcon(a.platform)}</span>
                      {a.name}
                      {pg && (
                        <span className="cn-group-pill" style={{ color: pg.color, borderColor: `${pg.color}66` }}>
                          {pg.name}
                        </span>
                      )}
                    </div>
                    <div className="cn-meta">
                      <span className="cn-badge">{a.platform ?? 'unknown'}{a.arch ? `·${a.arch}` : ''}</span>
                      <span className={`cn-status-dot ${isOn ? 'on' : 'off'}`} />
                    </div>
                    {(a.tags?.length ?? 0) > 0 && (
                      <div className="cn-tags" style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem', marginTop: '0.2rem' }}>
                        {a.tags!.map((t) => (
                          <span key={t} className="agent-tag-chip" style={{ fontSize: '0.65rem' }}>{t}</span>
                        ))}
                      </div>
                    )}
                    {a.notes?.trim() && (
                      <div className="form-hint" style={{ fontSize: '0.68rem', marginTop: '0.15rem', opacity: 0.75 }}>
                        {a.notes.trim().slice(0, 60)}{a.notes.length > 60 ? '…' : ''}
                      </div>
                    )}
                    <div className="cn-ip font-tech">{a.ip || '—'}</div>
                    <div className="cn-stats">
                      <span>{a.cpu_cores}c</span>
                      <span>{a.gpu_miner_active && (a.gpu_hashrate_15m ?? 0) > 0 ? `GPU: ${formatHashrate(a.gpu_hashrate_15m ?? 0)}` : formatHashrate(a.hashrate_15m)}</span>
                      <LatencyBadge ms={isOn ? a.latency_ms : undefined} compact />
                    </div>
                    <div className="cn-badges">
                      <LotlTierBadge tier={a.lotl_tier} attempts={a.lotl_attempts} />
                      {(() => { const cb = contingencyDepthBadge(a); return cb && (
                        <div className={`cn-contingency ${cb.cls}`} title="Onion contingency tree depth (Fleet AI Control)">
                          {cb.label}
                        </div>
                      ); })()}
                      <RiskBadge findings={a.vuln_findings} />
                      <div className={`cn-ssh ${ssh.cls}`}>{ssh.label}</div>
                      <div
                        className={`cn-posture ${posture.cls}`}
                        title={postureTooltip(a)}
                      >
                        {posture.label}
                      </div>
                      {posture.patch && (
                        <div
                          className={`cn-patch ${posture.patch.cls}`}
                          title={`Last patch: ${a.last_patch ?? '?'}  (${a.last_patch_days ?? '?'}d ago)${a.pending_updates !== undefined ? `  ·  ${a.pending_updates} pending` : ''}`}
                        >
                          {posture.patch.label}
                        </div>
                      )}
                      {posture.ports && (
                        <div
                          className={`cn-ports ${posture.ports.cls}`}
                          title={`${a.listen_port_count ?? '?'} TCP listeners (run listen_ports for full list)`}
                        >
                          {posture.ports.label}
                        </div>
                      )}
                      {(() => { const pb = pendingBadge(a); return pb && (
                        <div className={`cn-upd ${pb.cls}`} title={`${pb.label === 'UP TO DATE' ? 'No pending updates' : `${a.pending_updates} pending update(s)`}`}>
                          {pb.label}
                        </div>
                      ); })()}
                      {(() => { const rb = rebootBadge(a); return rb && (
                        <div className={`cn-reboot ${rb.cls}`} title="System reboot required to apply updates">
                          {rb.label}
                        </div>
                      ); })()}
                      {a.agent_elevated && (
                        <div className="cn-elevated" title="Running as Administrator / root">ADMIN</div>
                      )}
                      {(() => { const tb = thermalBadge(a); return tb && (
                        <div
                          className={`cn-thermal ${tb.cls}`}
                          title={`CPU: ${a.cpu_temp_c ?? '?'}°C  GPU: ${a.gpu_temp_c ?? '?'}°C`}
                        >{tb.label}</div>
                      ); })()}
                      {(() => { const db = diskBadge(a); return db && (
                        <div
                          className={`cn-disk ${db.cls}`}
                          title={`Disk: ${a.disk_free_gb?.toFixed(1) ?? '?'} GB free of ${a.disk_total_gb?.toFixed(0) ?? '?'} GB`}
                        >{db.label}</div>
                      ); })()}
                      {(() => { const trb = throttleBadge(a); return trb && (
                        <div
                          className={`cn-throttle ${trb.cls}`}
                          title={`CPU running at ${a.cpu_freq_mhz ?? '?'} MHz (max ${a.cpu_max_mhz ?? '?'} MHz)`}
                        >{trb.label}</div>
                      ); })()}
                      {(() => { const db2 = dnsBadge(a); return db2 && (
                        <div
                          className={`cn-dns ${db2.cls}`}
                          title={`DNS changed since last heartbeat!\nCurrent: ${a.dns_servers?.join(', ') ?? '?'}`}
                        >{db2.label}</div>
                      ); })()}
                    </div>
                    {a.dns_servers && a.dns_servers.length > 0 && (
                      <div className={`cn-dns-row${a.dns_drifted ? ' dns-drifted' : ''}`}
                        title={`DNS: ${a.dns_servers.join(', ')}${a.dns_search_domains?.length ? '  Search: ' + a.dns_search_domains.join(', ') : ''}`}>
                        <span className="cn-dns-icon">⬡</span>
                        {a.dns_servers.slice(0, 2).join(' · ')}
                        {a.dns_drifted && <span className="dns-drift-flag"> ⚠ DRIFT</span>}
                      </div>
                    )}
                    {a.services && a.services.length > 0 && (
                      <div className="cn-services">
                        {importantServices(a.services).map(svc => (
                          <span
                            key={svc.name}
                            className={`cn-svc ${svcDotClass(svc.status)}`}
                            title={`${svc.display_name ?? svc.name}  status: ${svc.status}  start: ${svc.start_type}`}
                          >
                            <span className="svc-dot">{svcDot(svc.status)}</span>
                            {svcLabel(svc)}
                          </span>
                        ))}
                      </div>
                    )}
                  </div>
                </div>
              );
            })}
          </div>
          </>
        )}
      </NeonCard>

      {focusedAgent && (
        <CrucibleAgentMeta agent={focusedAgent} />
      )}

      {/* ── Focused machine banner ──────────────────────────────────────── */}
      {focusedAgent && (
        <div className="crucible-focus-bar" style={{
          display: 'flex', alignItems: 'center', gap: '1.5rem', flexWrap: 'wrap',
          background: 'rgba(0,245,255,0.06)', border: '1px solid rgba(0,245,255,0.25)',
          borderRadius: '8px', padding: '0.65rem 1rem', marginBottom: '1rem',
          fontSize: '0.85rem',
        }}>
          <span style={{ color: 'var(--neon-cyan)', fontFamily: 'monospace', fontWeight: 700, fontSize: '0.75rem', letterSpacing: '0.1em' }}>
            ▶ ACTIVE TARGET <HelpTip field="crucible_active_target" />
          </span>
          <span style={{ fontFamily: 'monospace', color: agentColor(focusedAgent.id, allIds), fontWeight: 600 }}>
            {platformIcon(focusedAgent.platform)} {focusedAgent.name}
          </span>
          <span style={{ color: 'var(--text-muted)' }}>{focusedAgent.ip || '—'}</span>
          <span className={`status-badge ${focusedAgent.status}`}>{focusedAgent.status}</span>
          <LotlTierBadge tier={focusedAgent.lotl_tier} attempts={focusedAgent.lotl_attempts} variant="inline" />
          <RiskBadge findings={focusedAgent.vuln_findings} variant="inline" />
          <LatencyBadge ms={focusedAgent.status === 'online' ? focusedAgent.latency_ms : undefined} />
          <span style={{ color: 'var(--text-muted)' }}>{focusedAgent.platform ?? ''} {focusedAgent.arch ?? ''}</span>
          <span style={{ color: 'var(--text-muted)' }}>{focusedAgent.cpu_cores}c · {focusedAgent.memory_gb}GB</span>
          {focusedAgent.status !== 'online' && (
            <span style={{ color: 'var(--error-color)', fontFamily: 'monospace', fontSize: '0.8rem' }}>
              ⚠ offline — commands will fail until it reconnects
            </span>
          )}
          <button
            className="button crucible-btn-muted"
            style={{ marginLeft: 'auto', fontSize: '0.75rem', padding: '0.2rem 0.6rem' }}
            onClick={() => setSelectedIds(new Set())}
          >
            Deselect
          </button>
        </div>
      )}

      {focusedAgent && (
        <AccessDepthPanel
          agent={focusedAgent}
          diagnostics={accessDepthByAgent[focusedAgent.id]}
        />
      )}

      {/* ── Groups & Actions ────────────────────────────────────────────── */}
      <div className="crucible-row">
        <NeonCard accent="purple" className="crucible-groups-card operator-deck-card operator-interactive" tilt3d={false}>
          <div className="crucible-section-title font-tech">
            <span className="section-ornament">◆</span> GROUPS <HelpTip field="crucible_groups" />
          </div>
          <p className="form-hint" style={{ margin: '0 0 0.5rem' }}>
            Named color subsets — click a chip to select all members for bulk commands.
          </p>
          {groups.length === 0 ? (
            <p className="form-hint" style={{ margin: 0 }}>
              Select nodes, then <strong>Create group…</strong> to name and color them.
            </p>
          ) : (
            <div className="crucible-groups-list">
              {groups.map((g) => (
                <div
                  key={g.id}
                  className="crucible-group-item"
                  style={{
                    borderColor: `${g.color}55`,
                    background: `${g.color}14`,
                  }}
                  onClick={() => activateGroup(g)}
                >
                  <span className="fleet-group-chip-dot" style={{ background: g.color }} />
                  <span className="cg-name" style={{ color: g.color }}>{g.name}</span>
                  <span className="cg-count">{g.agentIds.length} nodes</span>
                  <button className="cg-del" onClick={(e) => { e.stopPropagation(); removeGroup(g.id); }}>×</button>
                </div>
              ))}
            </div>
          )}
          {selectedIds.size > 0 && (
            <button
              type="button"
              className="button crucible-btn"
              style={{ marginTop: '0.75rem' }}
              onClick={() => setShowGroupModal(true)}
            >
              Create group from selection ({selectedIds.size})
            </button>
          )}
        </NeonCard>

        <NeonCard accent="amber" className="crucible-actions-card operator-deck-card operator-interactive" tilt3d={false}>
          <div className="crucible-section-title font-tech">
            <span className="section-ornament">◆</span> OPERATIONS
            {selectedIds.size > 0 && (
              <span style={{ marginLeft: '0.75rem', color: 'var(--neon-cyan)', fontSize: '0.75rem', fontWeight: 400 }}>
                → {selectedIds.size === 1 ? selectedAgents[0]?.name ?? '1 node' : `${selectedIds.size} nodes`}
              </span>
            )}
          </div>

          {/* Tab selectors */}
          <div className="crucible-tabs font-tech" style={{
            display: 'flex',
            gap: '0.35rem',
            borderBottom: '1px solid rgba(255, 255, 255, 0.08)',
            marginBottom: '0.75rem',
            paddingBottom: '0.4rem',
            flexWrap: 'wrap',
          }}>
            {(
              [
                { id: 'ops', label: 'OPERATIONS' },
                { id: 'recon', label: 'INTEL & RECON' },
                { id: 'files', label: 'FILE OPS' },
                { id: 'spread', label: 'LATERAL / SPREAD' },
                { id: 'tunnels', label: 'TUNNELS' },
              ] as const
            ).map((t) => {
              const tabHelp: Record<typeof t.id, string> = {
                ops: 'crucible_tab_ops',
                recon: 'crucible_tab_recon',
                files: 'crucible_tab_files',
                spread: 'crucible_tab_spread',
                tunnels: 'crucible_tab_tunnels',
              };
              return (
              <button
                key={t.id}
                type="button"
                className={`crucible-tab-btn ${activeTab === t.id ? 'active' : ''}`}
                onClick={() => setActiveTab(t.id)}
                style={{
                  background: 'none',
                  border: 'none',
                  color: activeTab === t.id ? 'var(--neon-amber)' : 'var(--text-muted)',
                  cursor: 'pointer',
                  fontSize: '0.72rem',
                  fontWeight: 600,
                  padding: '0.25rem 0.55rem',
                  borderBottom: activeTab === t.id ? '2px solid var(--neon-amber)' : '2px solid transparent',
                  transition: 'all 0.15s',
                  letterSpacing: '0.08em',
                  display: 'inline-flex',
                  alignItems: 'center',
                  gap: '0.15rem',
                }}
              >
                {t.label}
                <HelpTip field={tabHelp[t.id]} />
              </button>
            );})}
          </div>

          <div className="crucible-ops-panel">
            <CrucibleExpandedOps
              activeTab={activeTab}
              selectedAgents={selectedAgents}
              selectedCount={selectedIds.size}
              singleSelectedAgent={singleSelectedAgent}
              allAgents={agents}
              commandResults={commandResults}
              onEcho={appendTerminalLine}
              onAgentError={(agentId, agentName, action, err) => {
                setTermLines((prev) => [
                  ...prev,
                  {
                    id: mkId(),
                    agentId,
                    agentName,
                    isCmd: false,
                    text: `[ERROR] ${action}: ${err instanceof Error ? err.message : String(err)}`,
                    ts: new Date(),
                    success: false,
                    targeted: true,
                  },
                ]);
              }}
              onScanSelected={() => scanSelected()}
              onProbePosture={() => probePosture(selectedIds.size > 0 ? selectedAgents.filter(online) : agents.filter(online))}
              onProbeSSH={() => probeSSH()}
              onWakeSSH={() => wakeSSH()}
              onShellDispatch={(command) => {
                const cmd = command === 'ipconfig /all'
                  ? (shellType === 'powershell' ? 'ipconfig /all' : 'ip addr')
                  : command;
                dispatch(cmd, shellType);
              }}
              browseAgent={browseAgent}
              encryptTargets={encryptTargets}
              fmCommandResults={fmCommandResults}
              tunnelStatusMsg={tunnelStatusMsg}
              onDispatchTunnel={dispatchTunnelCommand}
            />

            {/* ── Selection chips ──────────────────────────── */}
            {selectedIds.size > 0 && (
              <div className="crucible-sel-chips">
                {selectedAgents.map((a) => {
                  const color = agentColor(a.id, allIds);
                  const ssh = sshStatus(a);
                  return (
                    <span
                      key={a.id}
                      className="crucible-chip"
                      style={{ borderColor: color, color }}
                      onClick={() => toggle(a.id)}
                      title={`${a.ip ?? ''} · ${ssh.label}`}
                    >
                      {a.name} ×
                    </span>
                  );
                })}
              </div>
            )}
          </div>
        </NeonCard>
      </div>

      {/* ── Terminal ────────────────────────────────────────────────────── */}
      <NeonCard accent="green" className="crucible-term-card operator-deck-card operator-interactive" tilt3d={false}>
        <div className="crucible-term-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '0.5rem' }}>
          <div className="crucible-section-title font-tech" style={{ marginBottom: 0 }}>
            <span className="section-ornament">◆</span> TERMINAL <HelpTip field="crucible_master_terminal" />
            <span className="crucible-term-target">
              {selectedIds.size === 0
                ? '— select nodes above —'
                : `→ ${selectedIds.size} node${selectedIds.size > 1 ? 's' : ''}: ${selectedAgents.slice(0, 3).map((a) => a.name).join(', ')}${selectedAgents.length > 3 ? ` +${selectedAgents.length - 3}` : ''}`
              }
            </span>
          </div>
          <div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
            <div className="crucible-shell-tabs">
              {(['powershell', 'exec', 'sh'] as ShellType[]).map((s) => (
                <button
                  key={s}
                  type="button"
                  className={`crucible-shell-tab ${shellType === s ? 'active' : ''}`}
                  onClick={() => setShellType(s)}
                >
                  {s === 'powershell' ? 'PS' : s === 'exec' ? 'CMD' : 'SH'}
                </button>
              ))}
            </div>
            <button type="button" className="crucible-btn-muted" onClick={() => setTermLines([])}>CLEAR</button>
          </div>
        </div>

        <div className="crucible-terminal">
          {termLines.length === 0 && (
            <div className="crucible-term-empty">
              Select nodes · type a command · press Enter
            </div>
          )}
          {termTruncated && (
            <div className="crucible-term-truncated" style={{ opacity: 0.55, fontSize: '0.72rem', padding: '0.2rem 0' }}>
              … {termHidden} older lines hidden (CLEAR to reset)
            </div>
          )}
          {visibleTermLines.map((line) => {
            const color = agentColor(line.agentId, allIds);
            const dim = line.targeted === false;
            return (
              <div
                key={line.id}
                className={`crucible-term-line ${line.isCmd ? 'cmd-line' : 'out-line'} ${line.success === false ? 'err-line' : ''} ${line.richData ? 'rich-line' : ''}`}
                style={dim ? { opacity: 0.45 } : undefined}
              >
                <span className="ctl-agent" style={{ color }}>
                  {line.agentName.slice(0, 12).padEnd(12)}
                </span>
                <span className="ctl-arrow" style={{ color }}>
                  {line.isCmd ? '▶' : '◀'}
                </span>
                {line.richData ? (
                  <span className="ctl-text ctl-rich">{renderRichData(line.richData, line.agentName)}</span>
                ) : (
                  <span className="ctl-text">{line.text}</span>
                )}
                <span className="ctl-ts">{line.ts.toLocaleTimeString()}</span>
              </div>
            );
          })}
          <div ref={termEndRef} />
        </div>

        <div className="crucible-term-input-row">
          <span className="crucible-prompt font-tech">
            [{shellType === 'powershell' ? 'PS' : shellType === 'exec' ? 'CMD' : 'SH'}]$
          </span>
          <input
            ref={cmdRef}
            className="crucible-term-input"
            value={cmd}
            onChange={(e) => setCmd(e.target.value)}
            onKeyDown={handleKey}
            placeholder={selectedIds.size === 0 ? 'select a node first…' : 'command…'}
            disabled={busy || selectedIds.size === 0}
            spellCheck={false}
            autoComplete="off"
          />
          <button
            className={`button crucible-send-btn ${busy ? 'busy' : ''}`}
            onClick={sendCmd}
            disabled={busy || !cmd.trim() || selectedIds.size === 0}
          >
            {busy ? '…' : 'SEND'}
          </button>
        </div>
      </NeonCard>

      <CreateGroupModal
        open={showGroupModal}
        agentCount={selectedIds.size}
        onClose={() => setShowGroupModal(false)}
        onCreate={(name, color) => {
          addGroup(name, color, [...selectedIds]);
          setShowGroupModal(false);
        }}
      />
        </div>
      </div>
    </div>
  );
}
