// windows.jsx
// OS chrome layer: a Windows 3.1-style window manager (drag, focus, z-stack,
// min/max/restore), Program-Manager-style desktop icons, a minimized-window
// strip, and the secondary "documents" the icons open. The SimTower itself
// is rendered as one window kind ("agents") via children supplied by app.jsx.

const MIN_W = 280;
const MIN_H = 180;
const TITLE_H = 22;

// ───────────────────────────────────────────────────────────────────────────
// Generic Window shell
// ───────────────────────────────────────────────────────────────────────────

function Win31Window({
  id, title, x, y, w, h, focused, maximized, closeable,
  onFocus, onClose, onMinimize, onMaximize, onMove, onResize,
  children,
}) {
  const winRef = React.useRef(null);
  const dragRef = React.useRef(null);

  function startGesture(mode, e) {
    if (maximized) return;
    onFocus();
    dragRef.current = {
      mode,
      startX: e.clientX,
      startY: e.clientY,
      origX: x, origY: y,
      origW: w, origH: h,
      curX: x, curY: y, curW: w, curH: h,
    };
    document.addEventListener('mousemove', onMouseMove);
    document.addEventListener('mouseup', onMouseUp);
    e.preventDefault();
    e.stopPropagation();
  }

  function onMouseMove(e) {
    const ds = dragRef.current;
    if (!ds || !winRef.current) return;
    const dx = e.clientX - ds.startX;
    const dy = e.clientY - ds.startY;
    if (ds.mode === 'drag') {
      ds.curX = ds.origX + dx;
      ds.curY = Math.max(0, ds.origY + dy);
      winRef.current.style.left = ds.curX + 'px';
      winRef.current.style.top = ds.curY + 'px';
    } else if (ds.mode === 'resize') {
      ds.curW = Math.max(MIN_W, ds.origW + dx);
      ds.curH = Math.max(MIN_H, ds.origH + dy);
      winRef.current.style.width = ds.curW + 'px';
      winRef.current.style.height = ds.curH + 'px';
    }
  }

  function onMouseUp() {
    document.removeEventListener('mousemove', onMouseMove);
    document.removeEventListener('mouseup', onMouseUp);
    const ds = dragRef.current;
    dragRef.current = null;
    if (!ds) return;
    if (ds.mode === 'drag') onMove(ds.curX, ds.curY);
    else if (ds.mode === 'resize') onResize(ds.curW, ds.curH);
  }

  const style = maximized
    ? { position: 'absolute', inset: 0, zIndex: 100 + (focused ? 50 : 0) }
    : { position: 'absolute', left: x, top: y, width: w, height: h, zIndex: 100 + (focused ? 50 : 0) };

  return (
    <div
      ref={winRef}
      className={`win31-window ${focused ? 'is-focused' : 'is-blurred'} ${maximized ? 'is-max' : ''}`}
      style={style}
      onMouseDown={onFocus}
    >
      <div
        className="win31-titlebar"
        onMouseDown={(e) => startGesture('drag', e)}
        onDoubleClick={onMaximize}
      >
        <button className="win31-sysmenu" aria-label="System menu" tabIndex={-1} onClick={(e) => e.stopPropagation()}>
          <span className="win31-sysmenu-glyph"/>
        </button>
        <div className="win31-title">{title}</div>
        <div className="win31-titlebar-controls">
          <button
            className="win31-titlebar-btn"
            aria-label="Minimize"
            tabIndex={-1}
            onMouseDown={(e) => e.stopPropagation()}
            onClick={(e) => { e.stopPropagation(); onMinimize && onMinimize(); }}
          >
            <span className="win31-titlebar-glyph win31-min-glyph">▼</span>
          </button>
          <button
            className="win31-titlebar-btn"
            aria-label={maximized ? 'Restore' : 'Maximize'}
            tabIndex={-1}
            onMouseDown={(e) => e.stopPropagation()}
            onClick={(e) => { e.stopPropagation(); onMaximize && onMaximize(); }}
          >
            <span className="win31-titlebar-glyph win31-max-glyph">{maximized ? '◰' : '▲'}</span>
          </button>
          {closeable && (
            <button
              className="win31-titlebar-btn win31-close-btn"
              aria-label="Close"
              tabIndex={-1}
              onMouseDown={(e) => e.stopPropagation()}
              onClick={(e) => { e.stopPropagation(); onClose && onClose(); }}
            >
              <span className="win31-titlebar-glyph">✕</span>
            </button>
          )}
        </div>
      </div>

      <div className="win31-body">{children}</div>

      {!maximized && (
        <div
          className="win31-resize-handle"
          onMouseDown={(e) => startGesture('resize', e)}
          aria-hidden="true"
        />
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────────────────────────
// Desktop icon (single click to select, double click to open)
// ───────────────────────────────────────────────────────────────────────────

function DesktopIcon({ kind, label, icon, x, y, selected, onSelect, onOpen }) {
  return (
    <button
      className={`mos-icon ${selected ? 'is-selected' : ''}`}
      style={{ left: x, top: y }}
      onClick={() => onSelect(kind)}
      onDoubleClick={() => onOpen(kind)}
      onMouseDown={(e) => e.stopPropagation()}
      aria-label={label}
    >
      <div className="mos-icon-glyph" aria-hidden="true">{icon}</div>
      <div className="mos-icon-label">{label}</div>
    </button>
  );
}

// ───────────────────────────────────────────────────────────────────────────
// Minimized strip (Win 3.1 dropped minimized windows onto the desktop as
// little title-bar tabs near the bottom). Click to restore.
// ───────────────────────────────────────────────────────────────────────────

function MinimizedStrip({ windows, onRestore }) {
  if (!windows.length) return null;
  return (
    <div className="mos-min-strip">
      {windows.map((w) => (
        <button
          key={w.id}
          className="mos-min-tab"
          onClick={() => onRestore(w.id)}
          onMouseDown={(e) => e.stopPropagation()}
        >
          <span className="mos-min-tab-glyph" aria-hidden="true">▣</span>
          <span className="mos-min-tab-label">{w.title}</span>
        </button>
      ))}
    </div>
  );
}

// ───────────────────────────────────────────────────────────────────────────
// 16x16 SVG glyphs for icons (pixel-style, no antialiasing)
// ───────────────────────────────────────────────────────────────────────────

const PixelIcon = ({ kind }) => {
  const common = { width: 32, height: 32, viewBox: '0 0 16 16', shapeRendering: 'crispEdges', xmlns: 'http://www.w3.org/2000/svg' };
  if (kind === 'agents') {
    return (
      <svg {...common}>
        <rect x="3" y="2" width="10" height="12" fill="#c0c0c0" stroke="#000"/>
        <rect x="5" y="4" width="2" height="2" fill="#0000aa"/>
        <rect x="9" y="4" width="2" height="2" fill="#0000aa"/>
        <rect x="5" y="7" width="2" height="2" fill="#0000aa"/>
        <rect x="9" y="7" width="2" height="2" fill="#aa5500"/>
        <rect x="5" y="10" width="2" height="2" fill="#0000aa"/>
        <rect x="9" y="10" width="2" height="2" fill="#0000aa"/>
        <rect x="7" y="0" width="2" height="2" fill="#aa0000"/>
      </svg>
    );
  }
  if (kind === 'doc') {
    return (
      <svg {...common}>
        <polygon points="3,1 11,1 13,3 13,15 3,15" fill="#fff" stroke="#000"/>
        <polyline points="11,1 11,3 13,3" fill="none" stroke="#000"/>
        <line x1="5" y1="6" x2="11" y2="6" stroke="#000"/>
        <line x1="5" y1="8" x2="11" y2="8" stroke="#000"/>
        <line x1="5" y1="10" x2="11" y2="10" stroke="#000"/>
        <line x1="5" y1="12" x2="9"  y2="12" stroke="#000"/>
      </svg>
    );
  }
  if (kind === 'info') {
    return (
      <svg {...common}>
        <circle cx="8" cy="8" r="6" fill="#0000aa" stroke="#000"/>
        <rect x="7" y="4" width="2" height="2" fill="#fff"/>
        <rect x="7" y="7" width="2" height="5" fill="#fff"/>
      </svg>
    );
  }
  if (kind === 'mail') {
    return (
      <svg {...common}>
        <rect x="2" y="4" width="12" height="9" fill="#fff" stroke="#000"/>
        <polyline points="2,4 8,9 14,4" fill="none" stroke="#000"/>
        <polyline points="2,13 8,9 14,13" fill="none" stroke="#000"/>
      </svg>
    );
  }
  if (kind === 'clock') {
    return (
      <svg {...common}>
        <rect x="1" y="1" width="14" height="14" fill="#c0c0c0" stroke="#000"/>
        <circle cx="8" cy="8" r="5" fill="#fff" stroke="#000"/>
        <rect x="7" y="2" width="2" height="1" fill="#000"/>
        <rect x="7" y="13" width="2" height="1" fill="#000"/>
        <rect x="2" y="7" width="1" height="2" fill="#000"/>
        <rect x="13" y="7" width="1" height="2" fill="#000"/>
        <line x1="8" y1="8" x2="8" y2="5" stroke="#000" strokeWidth="1"/>
        <line x1="8" y1="8" x2="11" y2="8" stroke="#000" strokeWidth="1"/>
      </svg>
    );
  }
  if (kind === 'filemgr') {
    return (
      <svg {...common}>
        <rect x="2" y="3" width="12" height="11" fill="#c0c0c0" stroke="#000"/>
        <rect x="2" y="3" width="6" height="2" fill="#aa5500" stroke="#000"/>
        <rect x="3" y="6" width="2" height="1" fill="#000"/>
        <rect x="3" y="8" width="2" height="1" fill="#000"/>
        <rect x="3" y="10" width="2" height="1" fill="#000"/>
        <rect x="3" y="12" width="2" height="1" fill="#000"/>
        <rect x="6" y="6" width="7" height="1" fill="#000"/>
        <rect x="6" y="8" width="5" height="1" fill="#000"/>
        <rect x="6" y="10" width="6" height="1" fill="#000"/>
      </svg>
    );
  }
  if (kind === 'dos') {
    return (
      <svg {...common}>
        <rect x="1" y="2" width="14" height="12" fill="#000" stroke="#000"/>
        <rect x="2" y="3" width="12" height="2" fill="#c0c0c0"/>
        <rect x="3" y="7" width="1" height="1" fill="#4ade80"/>
        <rect x="4" y="7" width="1" height="1" fill="#4ade80"/>
        <rect x="5" y="7" width="1" height="1" fill="#4ade80"/>
        <rect x="6" y="7" width="1" height="1" fill="#4ade80"/>
        <rect x="7" y="7" width="1" height="1" fill="#4ade80"/>
        <rect x="3" y="9" width="2" height="1" fill="#4ade80"/>
        <rect x="6" y="9" width="1" height="1" fill="#4ade80"/>
        <rect x="3" y="11" width="1" height="1" fill="#4ade80"/>
      </svg>
    );
  }
  if (kind === 'win') {
    return (
      <svg {...common}>
        <rect x="1" y="1" width="14" height="14" fill="#c0c0c0" stroke="#000"/>
        <rect x="3" y="3" width="4" height="4" fill="#aa0000"/>
        <rect x="9" y="3" width="4" height="4" fill="#00aa00"/>
        <rect x="3" y="9" width="4" height="4" fill="#0000aa"/>
        <rect x="9" y="9" width="4" height="4" fill="#aa5500"/>
      </svg>
    );
  }
  if (kind === 'doom') {
    return (
      <svg {...common}>
        <rect x="1" y="2" width="14" height="12" fill="#000" stroke="#000"/>
        <rect x="3" y="4" width="2" height="6" fill="#aa0000"/>
        <rect x="11" y="4" width="2" height="6" fill="#aa0000"/>
        <rect x="5" y="4" width="2" height="2" fill="#aa0000"/>
        <rect x="9" y="4" width="2" height="2" fill="#aa0000"/>
        <rect x="5" y="8" width="6" height="2" fill="#aa0000"/>
        <rect x="3" y="11" width="10" height="2" fill="#440000"/>
      </svg>
    );
  }
  if (kind === 'wolf') {
    // Stylized "W" + brick wall — Wolf3D vibe.
    return (
      <svg {...common}>
        <rect x="1" y="2" width="14" height="12" fill="#5a3a14" stroke="#000"/>
        <rect x="2" y="3" width="3" height="2" fill="#8a5a24"/>
        <rect x="6" y="3" width="3" height="2" fill="#8a5a24"/>
        <rect x="10" y="3" width="3" height="2" fill="#8a5a24"/>
        <rect x="3" y="6" width="3" height="2" fill="#8a5a24"/>
        <rect x="7" y="6" width="3" height="2" fill="#8a5a24"/>
        <rect x="11" y="6" width="2" height="2" fill="#8a5a24"/>
        <rect x="3" y="10" width="2" height="3" fill="#ffcf3f"/>
        <rect x="5" y="11" width="2" height="2" fill="#ffcf3f"/>
        <rect x="7" y="10" width="2" height="3" fill="#ffcf3f"/>
        <rect x="9" y="11" width="2" height="2" fill="#ffcf3f"/>
        <rect x="11" y="10" width="2" height="3" fill="#ffcf3f"/>
      </svg>
    );
  }
  return null;
};

// ───────────────────────────────────────────────────────────────────────────
// Notepad-style document body (used by README, MANIFESTO)
// ───────────────────────────────────────────────────────────────────────────

function NotepadDoc({ text }) {
  return (
    <div className="notepad">
      <div className="notepad-menubar">
        <span className="win31-menu-item"><u>F</u>ile</span>
        <span className="win31-menu-item"><u>E</u>dit</span>
        <span className="win31-menu-item"><u>S</u>earch</span>
        <span className="win31-menu-item"><u>H</u>elp</span>
      </div>
      <div className="notepad-paper">
        <pre>{text}</pre>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────────────────────
// About box body (compact dialog-style)
// ───────────────────────────────────────────────────────────────────────────

function AboutBox({ onClose }) {
  return (
    <div className="aboutbox">
      <div className="aboutbox-splash">
        <img
          className="aboutbox-logo"
          src="assets/multiplayer-os-logo.svg"
          alt="Multiplayer OS"
        />
      </div>
      <div className="aboutbox-body">
        <div className="aboutbox-version">Version 0.1.0 &mdash; pre-release</div>
        <div className="aboutbox-spacer"/>
        <div className="aboutbox-line">An operating system for the Multiplayer Era,</div>
        <div className="aboutbox-line">when humans and AI agents share the same workspace.</div>
        <div className="aboutbox-spacer"/>
        <div className="aboutbox-line">Substrate &nbsp;&middot;&nbsp; open source, Apache 2.0</div>
        <div className="aboutbox-line">Control plane &nbsp;&middot;&nbsp; federated</div>
        <div className="aboutbox-spacer"/>
        <div className="aboutbox-line aboutbox-foot">&copy; 2026 Multiplayer OS Project &mdash; multiplayeros.com</div>
      </div>
      <div className="aboutbox-controls">
        <button className="win31-button" onClick={onClose}>OK</button>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────────────────────
// Document copy
// ───────────────────────────────────────────────────────────────────────────

const README_TEXT = `                    *** MULTIPLAYER OS ***
                          v0.1.0


  Multiplayer OS is an operating system for the Multiplayer Era,
  when humans and AI agents share the same workspace, the same
  memory, and the same accountability.

  Open source substrate.   Federated control plane.   Visible work.


  Agents are first-class. So are humans. Both leave fingerprints
  on the same git history.


  > Double-click AGENTS.EXE for a live cutaway of an MoS company.
  > Double-click MANIFESTO.TXT for the philosophy.
  > Double-click ABOUT for what runs underneath.


  ---

  (c) 2026 -- Pre-release. Things may move.
`;

const MANIFESTO_TEXT = `THE MULTIPLAYER ERA
===================


  For sixty years, the OS was a single-player game. One human, one
  session, one keyboard, one set of files. Software did what the
  human told it to do, when the human told it to.


  The Multiplayer Era is different. Agents work alongside humans.
  They write code, file PRs, send messages, take meetings, query
  data. They do this when the human is asleep, in a meeting, or in
  another agent's session. They make decisions and leave traces.


  A single-player OS cannot host this. It has no notion of who is
  acting, what they are allowed to do, or how their work composes
  with anyone else's. It hides agent activity inside black-box
  services and reduces accountability to "the user did it."


  Multiplayer OS makes the substrate explicit:


    *  Agents and humans are both first-class principals.
    *  Every action is signed and visible in the same audit log.
    *  Permissions are declarative, repo-backed, and reviewable.
    *  The control plane is a lens onto the substrate, not a
       source of truth.
    *  The substrate is open source. The lens can be too.


  ---

  This is a working draft. The product is being built in public.
`;

// ───────────────────────────────────────────────────────────────────────────
// Clock — analog face + digital readout. Live, 1Hz.
// ───────────────────────────────────────────────────────────────────────────

function ClockApp() {
  const [now, setNow] = React.useState(() => new Date());
  React.useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id);
  }, []);

  const hours = now.getHours();
  const mins = now.getMinutes();
  const secs = now.getSeconds();
  const hourAngle = ((hours % 12) + mins / 60) * 30;
  const minAngle = (mins + secs / 60) * 6;
  const secAngle = secs * 6;
  const digital = now.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', second: '2-digit' });

  // Analog face is a 200x200 SVG built up from pixel-style ticks.
  const ticks = [];
  for (let i = 0; i < 12; i++) {
    const a = i * 30;
    const isHour = i % 3 === 0;
    ticks.push(
      <line key={i}
            x1={100 + Math.sin(a * Math.PI / 180) * (isHour ? 78 : 82)}
            y1={100 - Math.cos(a * Math.PI / 180) * (isHour ? 78 : 82)}
            x2={100 + Math.sin(a * Math.PI / 180) * 92}
            y2={100 - Math.cos(a * Math.PI / 180) * 92}
            stroke="#000" strokeWidth={isHour ? 3 : 1}/>
    );
  }

  return (
    <div className="clock">
      <div className="clock-face-wrap">
        <svg className="clock-face" viewBox="0 0 200 200" preserveAspectRatio="xMidYMid meet">
          <circle cx="100" cy="100" r="96" fill="#fff" stroke="#000" strokeWidth="2"/>
          {ticks}
          {/* Hour hand */}
          <line x1="100" y1="100"
                x2={100 + Math.sin(hourAngle * Math.PI / 180) * 50}
                y2={100 - Math.cos(hourAngle * Math.PI / 180) * 50}
                stroke="#000" strokeWidth="5" strokeLinecap="square"/>
          {/* Minute hand */}
          <line x1="100" y1="100"
                x2={100 + Math.sin(minAngle * Math.PI / 180) * 76}
                y2={100 - Math.cos(minAngle * Math.PI / 180) * 76}
                stroke="#000" strokeWidth="3" strokeLinecap="square"/>
          {/* Second hand */}
          <line x1="100" y1="100"
                x2={100 + Math.sin(secAngle * Math.PI / 180) * 84}
                y2={100 - Math.cos(secAngle * Math.PI / 180) * 84}
                stroke="#aa0000" strokeWidth="1.5" strokeLinecap="square"/>
          <circle cx="100" cy="100" r="3" fill="#000"/>
        </svg>
      </div>
      <div className="clock-digital">{digital}</div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────────────────────
// File Manager — Win 3.1 winfile.exe vibe; faux MoS substrate filesystem
// ───────────────────────────────────────────────────────────────────────────

const FILESYSTEM = {
  name: 'C:\\',
  kind: 'drive',
  children: [
    { name: 'MOS', kind: 'dir', children: [
      { name: 'SUBSTRATE', kind: 'dir', children: [
        { name: 'schemas',     kind: 'dir',  children: [
          { name: 'agent.toml',      kind: 'file', size: 412 },
          { name: 'human.toml',      kind: 'file', size: 308 },
          { name: 'permission.toml', kind: 'file', size: 1024 },
          { name: 'audit.toml',      kind: 'file', size: 612 },
        ]},
        { name: 'permissions', kind: 'dir',  children: [
          { name: 'default.toml',    kind: 'file', size: 822 },
          { name: 'agents-can.toml', kind: 'file', size: 1411 },
        ]},
        { name: 'audit',       kind: 'dir',  children: [
          { name: 'log.jsonl',       kind: 'file', size: 18432 },
        ]},
        { name: 'gateway.ref',     kind: 'file', size: 9211 },
        { name: 'README.txt',      kind: 'file', size: 1480 },
      ]},
      { name: 'AGENTS', kind: 'dir', children: [
        { name: 'orchestrator-1.toml', kind: 'file', size: 612 },
        { name: 'eng-a.toml',  kind: 'file', size: 488 },
        { name: 'eng-b.toml',  kind: 'file', size: 488 },
        { name: 'data.toml',   kind: 'file', size: 412 },
        { name: 'design.toml', kind: 'file', size: 388 },
        { name: 'cs.toml',     kind: 'file', size: 360 },
        { name: 'sales.toml',  kind: 'file', size: 360 },
      ]},
      { name: 'HUMANS', kind: 'dir', children: [
        { name: 'steve.toml',     kind: 'file', size: 720 },
      ]},
      { name: 'MEMORY', kind: 'dir', children: [
        { name: 'wiki.git', kind: 'file', size: 4194304 },
      ]},
      { name: 'README.TXT',      kind: 'file', size: 1822 },
      { name: 'MANIFESTO.TXT',   kind: 'file', size: 3144 },
      { name: 'LICENSE.TXT',     kind: 'file', size: 11357 },
    ]},
    { name: 'AGENTS.CITY', kind: 'dir', children: [
      { name: 'building.dat',   kind: 'file', size: 22480 },
      { name: 'roster.json',    kind: 'file', size: 5120 },
      { name: 'session.log',    kind: 'file', size: 162144 },
    ]},
    { name: 'AUTOEXEC.BAT', kind: 'file', size: 128 },
    { name: 'CONFIG.SYS',   kind: 'file', size: 96 },
  ],
};

function flattenPaths(node, path = []) {
  const here = [...path, node.name];
  const out = [{ key: here.join('/'), node, path: here, depth: path.length }];
  if (node.children) {
    for (const c of node.children) {
      if (c.kind === 'dir' || c.kind === 'drive') out.push(...flattenPaths(c, here));
    }
  }
  return out;
}

function findNodeByPath(root, pathParts) {
  let cur = root;
  for (let i = 1; i < pathParts.length; i++) {
    if (!cur.children) return null;
    cur = cur.children.find(c => c.name === pathParts[i]);
    if (!cur) return null;
  }
  return cur;
}

function FileManagerApp() {
  const [openDirs, setOpenDirs] = React.useState(() => new Set(['C:\\', 'C:\\/MOS']));
  const [selected, setSelected] = React.useState(['C:\\', 'MOS']);

  const allDirs = flattenPaths(FILESYSTEM);
  const visibleDirs = allDirs.filter(({ path }) => {
    if (path.length <= 1) return true;
    for (let i = 1; i < path.length; i++) {
      const ancestorKey = path.slice(0, i).join('/');
      if (!openDirs.has(ancestorKey)) return false;
    }
    return true;
  });

  function toggle(key) {
    setOpenDirs(prev => {
      const next = new Set(prev);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      return next;
    });
  }

  const selectedKey = selected.join('/');
  const selectedNode = findNodeByPath(FILESYSTEM, selected);
  const listing = (selectedNode?.children || []).filter(c => true);

  function fmtSize(n) {
    if (n == null) return '';
    if (n < 1024) return `${n}`;
    if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}K`;
    return `${(n / 1024 / 1024).toFixed(1)}M`;
  }

  const totalBytes = listing.reduce((s, c) => s + (c.size || 0), 0);

  return (
    <div className="filemgr">
      <div className="filemgr-menubar">
        <span className="win31-menu-item"><u>F</u>ile</span>
        <span className="win31-menu-item"><u>D</u>isk</span>
        <span className="win31-menu-item"><u>T</u>ree</span>
        <span className="win31-menu-item"><u>V</u>iew</span>
        <span className="win31-menu-item"><u>O</u>ptions</span>
        <span className="win31-menu-item"><u>H</u>elp</span>
      </div>

      <div className="filemgr-pathbar">{selected.join('\\')}</div>

      <div className="filemgr-panes">
        <div className="filemgr-tree">
          {visibleDirs.map(({ key, node, path, depth }) => {
            const hasChildren = (node.children || []).some(c => c.kind === 'dir' || c.kind === 'drive');
            const isOpen = openDirs.has(key);
            const isSel = key === selectedKey;
            return (
              <div
                key={key}
                className={`filemgr-tree-row ${isSel ? 'is-selected' : ''}`}
                style={{ paddingLeft: 6 + depth * 14 }}
                onClick={() => setSelected(path)}
                onDoubleClick={() => toggle(key)}
              >
                <span className="filemgr-tree-toggle" onClick={(e) => { e.stopPropagation(); toggle(key); }}>
                  {hasChildren ? (isOpen ? '−' : '+') : ' '}
                </span>
                <span className="filemgr-tree-icon" aria-hidden="true">
                  {node.kind === 'drive' ? '▣' : (isOpen ? '▤' : '▦')}
                </span>
                <span className="filemgr-tree-label">{node.name}</span>
              </div>
            );
          })}
        </div>

        <div className="filemgr-list">
          <div className="filemgr-list-header">
            <span className="col-name">Name</span>
            <span className="col-size">Size</span>
            <span className="col-kind">Kind</span>
          </div>
          <div className="filemgr-list-body">
            {listing.length === 0 && (
              <div className="filemgr-list-empty">(empty)</div>
            )}
            {listing.map(c => (
              <div
                key={c.name}
                className="filemgr-list-row"
                onDoubleClick={() => {
                  if (c.kind === 'dir') setSelected([...selected, c.name]);
                }}
              >
                <span className="col-name">
                  <span className="filemgr-list-icon">{c.kind === 'dir' ? '▦' : '▤'}</span>
                  {c.name}
                </span>
                <span className="col-size">{c.kind === 'dir' ? '<DIR>' : fmtSize(c.size)}</span>
                <span className="col-kind">{c.kind === 'dir' ? 'Folder' : (c.name.split('.').pop() || '').toUpperCase()}</span>
              </div>
            ))}
          </div>
        </div>
      </div>

      <div className="filemgr-statusbar">
        <span>{listing.length} item{listing.length === 1 ? '' : 's'}</span>
        <span>·</span>
        <span>{fmtSize(totalBytes)} bytes</span>
        <span>·</span>
        <span>C: 314,572,800 bytes free</span>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────────────────────
// DOS / Win 3.11 emulator window (js-dos 8 in an iframe so its Tailwind
// reset can't bleed into the parent chrome)
// ───────────────────────────────────────────────────────────────────────────

function DosApp({ kind, splash }) {
  // The iframe loads dosbox.html which mounts js-dos 8 with DOSBox-X WASM
  // and streams the requested .jsdos bundle from /assets/dos/.
  const src = `dosbox.html?kind=${encodeURIComponent(kind)}`;
  const [showFrame, setShowFrame] = React.useState(false);

  return (
    <div className="dosapp">
      {!showFrame && (
        <div className="dosapp-prelaunch">
          <div className="dosapp-screen">
            <div className="dosapp-screen-line">MOS / DOSBOX-X / js-dos 8</div>
            <div className="dosapp-screen-line dim">{splash || 'Press BOOT to start.'}</div>
            <div className="dosapp-screen-line dim">First boot streams the bundle and DOSBox-X WASM.</div>
            <div className="dosapp-screen-line">&nbsp;</div>
            <div className="dosapp-screen-line">C:\&gt; <span className="dosapp-cursor">_</span></div>
          </div>
          <div className="dosapp-launchbar">
            <button className="win31-button" onClick={() => setShowFrame(true)}>BOOT</button>
          </div>
        </div>
      )}
      {showFrame && (
        <iframe
          className="dosapp-frame"
          title="DOSBox"
          src={src}
          allow="autoplay; gamepad; fullscreen; cross-origin-isolated; keyboard-lock"
        />
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────────────────────────
// App registry (window kind -> metadata, default size, content renderer)
// ───────────────────────────────────────────────────────────────────────────

const APPS = {
  agents: {
    title: 'Gumbo City',
    icon: 'agents',
    iconLabel: 'GUMBO.EXE',
    defaultSize: { w: 1080, h: 660 },
    closeable: false,
  },
  readme: {
    title: 'README.TXT — Notepad',
    icon: 'doc',
    iconLabel: 'README.TXT',
    defaultSize: { w: 480, h: 380 },
    closeable: true,
  },
  manifesto: {
    title: 'MANIFESTO.TXT — Notepad',
    icon: 'doc',
    iconLabel: 'MANIFESTO.TXT',
    defaultSize: { w: 540, h: 460 },
    closeable: true,
  },
  about: {
    title: 'About Multiplayer OS',
    icon: 'info',
    iconLabel: 'About',
    defaultSize: { w: 420, h: 420 },
    closeable: true,
  },
  clock: {
    title: 'Clock',
    icon: 'clock',
    iconLabel: 'CLOCK.EXE',
    defaultSize: { w: 240, h: 280 },
    closeable: true,
  },
  filemgr: {
    title: 'File Manager — [C:\\MOS]',
    icon: 'filemgr',
    iconLabel: 'FILEMAN.EXE',
    defaultSize: { w: 580, h: 420 },
    closeable: true,
  },
  dos: {
    title: 'DOSBox — Sandbox',
    icon: 'dos',
    iconLabel: 'DOS.EXE',
    defaultSize: { w: 700, h: 480 },
    closeable: true,
    splash: 'Bare DOSBox-X session — no OS, just a DOS prompt.',
  },
  doom2: {
    title: 'DOOM II',
    icon: 'doom',
    iconLabel: 'DOOM2.EXE',
    defaultSize: { w: 720, h: 520 },
    closeable: true,
    splash: 'DOOM II (id Software, 1994). Mouse + keyboard. ~6 MB.',
  },
  wolf3d: {
    title: 'Wolfenstein 3D',
    icon: 'wolf',
    iconLabel: 'WOLF3D.EXE',
    defaultSize: { w: 720, h: 520 },
    closeable: true,
    splash: 'Wolfenstein 3D (id Software, 1992). Keyboard. ~1 MB.',
  },
};

const DESKTOP_ICONS = [
  { kind: 'agents',    x: 22, y: 18 },
  { kind: 'readme',    x: 22, y: 110 },
  { kind: 'manifesto', x: 22, y: 202 },
  { kind: 'about',     x: 22, y: 294 },
  { kind: 'clock',     x: 22, y: 386 },
  { kind: 'filemgr',   x: 22, y: 478 },
  { kind: 'dos',       x: 22, y: 570 },
  { kind: 'doom2',     x: 22, y: 662 },
  { kind: 'wolf3d',    x: 22, y: 754 },
];

// Body renderer for non-agents windows. The 'agents' kind body comes from
// app.jsx since it owns the SimTower state.
function renderWindowBody(kind, ctx) {
  if (kind === 'readme')    return <NotepadDoc text={README_TEXT}/>;
  if (kind === 'manifesto') return <NotepadDoc text={MANIFESTO_TEXT}/>;
  if (kind === 'about')     return <AboutBox onClose={ctx.onClose}/>;
  if (kind === 'clock')     return <ClockApp/>;
  if (kind === 'filemgr')   return <FileManagerApp/>;
  if (kind === 'dos')       return <DosApp kind="dos"   splash={APPS.dos.splash}/>;
  if (kind === 'doom2')     return <DosApp kind="doom2"  splash={APPS.doom2.splash}/>;
  if (kind === 'wolf3d')    return <DosApp kind="wolf3d" splash={APPS.wolf3d.splash}/>;
  return null;
}

Object.assign(window, {
  Win31Window,
  DesktopIcon,
  MinimizedStrip,
  PixelIcon,
  NotepadDoc,
  AboutBox,
  renderWindowBody,
  APPS,
  DESKTOP_ICONS,
  README_TEXT,
  MANIFESTO_TEXT,
});
