// app.jsx — root component wiring everything together + tick loop

const { useState, useEffect, useRef, useMemo } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "scanlines": 0.35,
  "halftone": 0.18,
  "agentDensity": 1,
  "showBeams": true,
  "buildingScale": 0.6
}/*EDITMODE-END*/;

function localTimeMode(at = new Date()) {
  const hour = at.getHours() + at.getMinutes() / 60;
  if (hour >= 7 && hour < 18) return 'day';
  if ((hour >= 5 && hour < 7) || (hour >= 18 && hour < 20)) return 'dusk';
  return 'night';
}

function localClockLabel(at = new Date()) {
  return at.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
}

// Persist window state across sessions. Bump STORAGE_VERSION when window
// shape changes in a way that older saved state would render badly (e.g.
// dropped kinds, renamed APPS keys). Older state is then ignored on load.
const STORAGE_KEY = 'mos-landing:windows';
const STORAGE_VERSION = 1;

function defaultInitialWindows() {
  const a = window.APPS.agents;
  return [{
    id: 'agents', kind: 'agents',
    x: 130, y: 36,
    w: a.defaultSize.w, h: a.defaultSize.h,
    maximized: false, minimized: false,
    z: 10,
  }];
}

function loadSavedWindows() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    const parsed = JSON.parse(raw);
    if (!parsed || parsed.version !== STORAGE_VERSION) return null;
    if (!Array.isArray(parsed.windows) || parsed.windows.length === 0) return null;
    const validKinds = new Set(Object.keys(window.APPS));
    const cleaned = parsed.windows
      .filter(w => w && validKinds.has(w.kind))
      .map(w => ({
        id: String(w.id || w.kind),
        kind: w.kind,
        x: Number.isFinite(w.x) ? w.x : 64,
        y: Number.isFinite(w.y) ? w.y : 36,
        w: Math.max(280, Number(w.w) || window.APPS[w.kind].defaultSize.w),
        h: Math.max(180, Number(w.h) || window.APPS[w.kind].defaultSize.h),
        maximized: !!w.maximized,
        minimized: !!w.minimized,
        z: Number.isFinite(w.z) ? w.z : 10,
      }));
    if (!cleaned.length) return null;
    // The Gumbo City window is the hero and not closeable — if a stale save
    // somehow lost it, prepend the default.
    if (!cleaned.find(w => w.kind === 'agents')) {
      return [...defaultInitialWindows(), ...cleaned];
    }
    return cleaned;
  } catch (e) {
    return null;
  }
}

function useWindowManager() {
  const [windows, setWindows] = useState(() => {
    return loadSavedWindows() || defaultInitialWindows();
  });
  const zRef = useRef(
    Math.max(10, ...windows.map(w => Number(w.z) || 0))
  );

  // Persist window state on every change. setWindows only fires on commit
  // (mouseup of a drag/resize), so this is naturally debounced — no need
  // for an extra throttle.
  useEffect(() => {
    try {
      localStorage.setItem(
        STORAGE_KEY,
        JSON.stringify({ version: STORAGE_VERSION, windows }),
      );
    } catch (e) {
      // localStorage disabled (private mode, full quota) — silent fail.
    }
  }, [windows]);

  function bumpZ() { zRef.current += 1; return zRef.current; }

  function focus(id) {
    const z = bumpZ();
    setWindows(ws => ws.map(w => w.id === id ? { ...w, z, minimized: false } : w));
  }

  function open(kind) {
    setWindows(ws => {
      const existing = ws.find(w => w.kind === kind);
      if (existing) {
        const z = bumpZ();
        return ws.map(w => w.id === existing.id ? { ...w, z, minimized: false } : w);
      }
      const cfg = window.APPS[kind];
      if (!cfg) return ws;
      const z = bumpZ();
      const offset = ws.length * 24;
      return [...ws, {
        id: kind,
        kind,
        x: 220 + offset,
        y: 80 + offset,
        w: cfg.defaultSize.w,
        h: cfg.defaultSize.h,
        maximized: false,
        minimized: false,
        z,
      }];
    });
  }

  function close(id) { setWindows(ws => ws.filter(w => w.id !== id)); }

  function minimize(id) {
    setWindows(ws => ws.map(w => w.id === id ? { ...w, minimized: true } : w));
  }

  function toggleMaximize(id) {
    setWindows(ws => ws.map(w => w.id === id ? { ...w, maximized: !w.maximized } : w));
  }

  function move(id, x, y) {
    setWindows(ws => ws.map(w => w.id === id ? { ...w, x, y } : w));
  }

  function resize(id, w_, h) {
    setWindows(ws => ws.map(w => w.id === id ? { ...w, w: w_, h } : w));
  }

  const visible = windows.filter(w => !w.minimized);
  const focusedId = visible.length
    ? visible.reduce((a, b) => a.z > b.z ? a : b).id
    : null;

  return { windows, focusedId, open, close, focus, minimize, toggleMaximize, move, resize };
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  const [agents, setAgents] = useState(() => window.makeAgents());
  const [packets, setPackets] = useState(() => window.makePackets(28));
  const elevatorRef = useRef({
    floor: window.floorIndex('lobby'),
    dir: 0, dwell: 0, target: null,
    requests: new Set(),
    passengers: new Set(),
  });
  const [, forceTick] = useState(0);

  const [speed, setSpeed] = useState(2);
  const [paused, setPaused] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  const [selectedFloorIdx, setSelectedFloorIdx] = useState(null);
  const [selectedElevator, setSelectedElevator] = useState(false);
  const [feed, setFeed] = useState([]);
  const [liveData, setLiveData] = useState(null);
  const [uptimeStart] = useState(Date.now());
  const [now, setNow] = useState(Date.now());

  const buildingScrollRef = useRef(null);

  const wm = useWindowManager();
  const [selectedIcon, setSelectedIcon] = useState(null);
  const [bootStage, setBootStage] = useState('show'); // 'show' -> 'fading' -> 'done'
  const [isPdaViewport, setIsPdaViewport] = useState(() => {
    if (!window.matchMedia || !window.MobilePdaHelpers) return false;
    return window.matchMedia(window.MobilePdaHelpers.PDA_VIEWPORT_QUERY).matches;
  });

  useEffect(() => {
    if (bootStage === 'show') {
      const id = setTimeout(() => setBootStage('fading'), 1800);
      return () => clearTimeout(id);
    }
    if (bootStage === 'fading') {
      const id = setTimeout(() => setBootStage('done'), 400);
      return () => clearTimeout(id);
    }
  }, [bootStage]);

  function dismissBoot() {
    if (bootStage === 'show')   setBootStage('fading');
    if (bootStage === 'fading') setBootStage('done');
  }

  useEffect(() => {
    if (!window.matchMedia || !window.MobilePdaHelpers) return;
    const query = window.MobilePdaHelpers.PDA_VIEWPORT_QUERY;
    const mq = window.matchMedia(query);
    const update = () => setIsPdaViewport(mq.matches);
    update();
    if (mq.addEventListener) {
      mq.addEventListener('change', update);
      return () => mq.removeEventListener('change', update);
    }
    mq.addListener(update);
    return () => mq.removeListener(update);
  }, []);

  // Tick loop
  useEffect(() => {
    if (paused) return;
    const id = setInterval(() => {
      setAgents(prev => window.tickAgents(prev, elevatorRef.current));
      elevatorRef.current = window.tickElevator(elevatorRef.current, agentsRef.current);
      setPackets(prev => window.tickPackets(prev));
      forceTick(n => n + 1);
    }, 1000 / (10 * speed));
    return () => clearInterval(id);
  }, [paused, speed]);

  // Always-fresh agents ref for elevator tick
  const agentsRef = useRef(agents);
  useEffect(() => { agentsRef.current = agents; }, [agents]);

  // Host bridge. A parent (Hermes plugin, multiplayeros.com landing, etc.) can
  // postMessage `agent-city:data` payloads into this iframe to drive the cast,
  // feed, and status bar from real systems. The SimTower motion loop stays local;
  // the bridge only swaps the data layer when a host is connected.
  useEffect(() => {
    function floorFor(home) {
      const idx = window.floorIndex(home || 'lobby');
      return idx >= 0 ? idx : window.floorIndex('lobby');
    }
    function isLiveWorking(raw) {
      const live = String(raw.live_state || '').toLowerCase();
      return ['active', 'running', 'connected', 'live'].includes(live);
    }
    function initialActivity(raw, id) {
      if (isLiveWorking(raw)) return 'working';
      const options = ['sitting', 'lounging', 'strolling', 'coffee', 'reading'];
      let h = 0;
      for (const ch of String(id || raw.name || 'idle')) h = (h * 31 + ch.charCodeAt(0)) % 1000003;
      return options[h % options.length];
    }
    function hydrateAgents(incoming, prev) {
      if (!Array.isArray(incoming) || incoming.length === 0) return prev;
      const prevById = new Map(prev.map(a => [a.id, a]));
      return incoming.map((raw, i) => {
        const id = String(raw.id || `live-${i}`);
        const existing = prevById.get(id);
        const homeId = raw.home || 'lobby';
        const home = floorFor(homeId);
        const floor = window.FLOORS[home] || window.FLOORS[window.floorIndex('lobby')];
        const stations = floor.stations && floor.stations.length ? floor.stations : [8, 12, 26, 32];
        const station = stations[i % stations.length] || 8;
        const liveWorking = isLiveWorking(raw);
        const liveChanged = existing && existing.liveState !== (raw.live_state || 'live');
        const state = liveWorking
          ? ((existing && !liveChanged && existing.state !== 'idle') ? existing.state : 'work')
          : ((existing && !liveChanged && existing.state !== 'work') ? existing.state : 'idle');
        const activity = liveWorking ? 'working' : ((existing && !liveChanged && existing.activity !== 'working') ? existing.activity : initialActivity(raw, id));
        return {
          ...(existing || {}),
          id,
          name: raw.name || id,
          type: raw.type === 'human' ? 'human' : 'agent',
          role: raw.role || 'Hermes Agent',
          color: raw.color || 'blue',
          home: homeId,
          floor: existing ? existing.floor : home,
          x: existing ? existing.x : station,
          tx: existing ? existing.tx : station,
          targetFloor: existing ? existing.targetFloor : home,
          state,
          activity,
          task: raw.task || 'watching Hermes runtime',
          progress: typeof raw.progress === 'number' ? raw.progress : (existing ? existing.progress : 0.4),
          mood: typeof raw.mood === 'number' ? raw.mood : (existing ? existing.mood : 0.75),
          step: existing ? existing.step : 0,
          cooldown: existing ? existing.cooldown : 180 + Math.floor(Math.random() * 360),
          liveState: raw.live_state || 'live',
          updatedAt: raw.updated_at,
        };
      });
    }
    function onMessage(event) {
      if (!event.data || event.data.type !== 'agent-city:data') return;
      const payload = event.data.payload || {};
      setLiveData(payload);
      if (Array.isArray(payload.agents)) {
        setAgents(prev => hydrateAgents(payload.agents, prev));
      }
      if (Array.isArray(payload.feed)) {
        setFeed(payload.feed.slice(0, 5));
      }
    }
    window.addEventListener('message', onMessage);
    window.parent.postMessage({ type: 'agent-city:ready' }, '*');
    return () => window.removeEventListener('message', onMessage);
  }, []);

  // Clock drives the user's local day/dusk/night mode.
  useEffect(() => {
    const id = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(id);
  }, []);

  // Synthesize a feed of messages from agent state changes. When live Hermes
  // data is connected, the backend feed owns the bar; otherwise use prototype
  // chatter so the design still has life when opened standalone.
  useEffect(() => {
    if (paused || liveData) return;
    const id = setInterval(() => {
      const a = agents[Math.floor(Math.random() * agents.length)];
      if (!a) return;
      const tags = ['msg','memory','skill','task','eval'];
      const tag = tags[Math.floor(Math.random() * tags.length)];
      const msgs = {
        msg: `${a.name} → bus.publish("${a.task}")`,
        memory: `${a.name} wrote memory entry · v${Math.floor(Math.random()*9)+1}`,
        skill: `${a.name} equipped skill · git.review`,
        task: `${a.name}: ${a.task}`,
        eval: `eval: ${a.name} score 0.${Math.floor(80+Math.random()*20)}`,
      };
      setFeed(prev => [{ t: ts(), tag, msg: msgs[tag] }, ...prev].slice(0, 5));
    }, 1400 / speed);
    return () => clearInterval(id);
  }, [agents, speed, paused, liveData]);

  function ts() {
    const d = new Date();
    return d.toTimeString().slice(0, 8);
  }

  const stats = useMemo(() => {
    const ag = agents.filter(a => a.type === 'agent').length;
    const hu = agents.filter(a => a.type === 'human').length;
    const active = agents.filter(a => a.state === 'work' || a.liveState === 'active' || a.liveState === 'running').length;
    const liveStatus = liveData && liveData.status;
    const upMs = now - uptimeStart;
    const mins = Math.floor(upMs / 60000);
    const secs = Math.floor((upMs % 60000) / 1000);
    function compact(n) {
      const value = Number(n) || 0;
      if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
      if (value >= 10000) return `${Math.round(value / 1000)}k`;
      if (value >= 1000) return `${(value / 1000).toFixed(1)}k`;
      return String(value);
    }
    return {
      agents: compact(liveStatus?.agents ?? ag),
      humans: compact(liveStatus?.humans ?? hu),
      sessions: compact(liveStatus?.sessions ?? 0),
      active: compact(liveStatus?.active ?? active),
      messages: compact(liveStatus?.messages ?? 0),
      tools: compact(liveStatus?.tools ?? 0),
      msgRate: Number(liveStatus?.msgRate ?? (6 + Math.sin(now / 800) * 2 + speed * 1.5)),
      uptime: liveStatus?.uptime || `${String(mins).padStart(2,'0')}:${String(secs).padStart(2,'0')}`,
    };
  }, [agents, now, speed, uptimeStart, liveData]);

  const selected = agents.find(a => a.id === selectedId);
  const selectedFloor = selectedFloorIdx == null ? null : window.FLOORS[selectedFloorIdx];
  const localDate = new Date(now);
  const time = localTimeMode(localDate);
  const clockLabel = localClockLabel(localDate);

  function floorOffset(floorIdx) {
    const heights = window.FLOORS.map(f => f.kind === 'roof' ? window.FLOOR_H * window.TILE * 0.85 : window.FLOOR_H * window.TILE);
    let y = 0;
    for (let i = 0; i < floorIdx; i++) y += heights[i];
    return { y, h: heights[floorIdx] || window.FLOOR_H * window.TILE };
  }

  function handleScroll(floorIdx, scaleOverride = null) {
    const el = buildingScrollRef.current;
    if (!el) return;
    const scale = Number(scaleOverride ?? t.buildingScale) || TWEAK_DEFAULTS.buildingScale;
    const { y, h } = floorOffset(floorIdx);
    const targetTop = Math.max(0, ((y + h / 2) * scale) - (el.clientHeight / 2));
    const targetLeft = Math.max(0, (el.scrollWidth - el.clientWidth) / 2);
    el.scrollTo({ top: targetTop, left: targetLeft, behavior: 'smooth' });
  }

  function focusFloorAtZoom(floorIdx, scale = 1.5) {
    setBuildingScale(scale);
    requestAnimationFrame(() => {
      requestAnimationFrame(() => handleScroll(floorIdx, scale));
    });
  }

  function selectFloor(floorIdx, shouldScroll = false, zoomFocus = false) {
    setSelectedFloorIdx(floorIdx);
    setSelectedId(null);
    setSelectedElevator(false);
    if (zoomFocus) focusFloorAtZoom(floorIdx, 1.5);
    else if (shouldScroll) handleScroll(floorIdx);
  }

  function selectAgent(agentId) {
    setSelectedId(agentId);
    setSelectedFloorIdx(null);
    setSelectedElevator(false);
  }

  function selectElevator() {
    setSelectedElevator(true);
    setSelectedId(null);
    setSelectedFloorIdx(null);
  }

  function clearInspector() {
    setSelectedId(null);
    setSelectedFloorIdx(null);
    setSelectedElevator(false);
  }

  function handleLocate() {
    if (selected) handleScroll(selected.floor);
  }

  function setBuildingScale(next) {
    const value = Math.max(0.35, Math.min(2.0, Math.round(next * 100) / 100));
    setTweak('buildingScale', value);
  }

  function zoomBuilding(delta) {
    setBuildingScale((Number(t.buildingScale) || TWEAK_DEFAULTS.buildingScale) + delta);
  }

  function resetBuildingZoom() {
    setBuildingScale(TWEAK_DEFAULTS.buildingScale);
  }

  function renderBootSplash() {
    if (bootStage === 'done') return null;
    return (
      <div
        className={`mos-boot ${bootStage === 'fading' ? 'is-fading' : ''}`}
        onClick={dismissBoot}
        role="presentation"
      >
        <img
          className="mos-boot-logo"
          src="assets/multiplayer-os-logo.svg"
          alt="Multiplayer OS"
        />
        <div className="mos-boot-version">Version 0.1.0</div>
        <div className="mos-boot-foot">
          <span className="mos-boot-dot"/> Starting Multiplayer OS&hellip;
        </div>
      </div>
    );
  }

  // The SimTower body lives inside the AGENTS.EXE window. Defined here
  // because it needs the SimTower state owned by App().
  function renderAgentsBody() {
    return (
      <>
        <div className="win31-menubar">
          <span className="win31-menu-item"><u>F</u>ile</span>
          <span className="win31-menu-item"><u>V</u>iew</span>
          <span className="win31-menu-item"><u>W</u>indow</span>
          <span className="win31-menu-item"><u>H</u>elp</span>
        </div>
        <div className="app" style={{
          '--scanlines': t.scanlines,
          '--halftone': t.halftone,
        }}>
          <div className="overlay-scanlines" style={{ opacity: t.scanlines }}/>
          <div className="overlay-halftone" style={{ opacity: t.halftone }}/>

          <TopBar speed={speed} setSpeed={setSpeed}
                  time={time} clockLabel={clockLabel}
                  paused={paused} setPaused={setPaused}
                  stats={stats}/>

          <div className="main">
            <MiniMap agents={agents} selectedFloorIdx={selectedFloorIdx} onScroll={(idx) => selectFloor(idx, true)}/>

            <div className="building-frame">
              <div className="building-sky" data-time={time}>
                {time === 'night' && <div className="stars"/>}
              </div>
              <div className="zoom-controls" aria-label="Building zoom controls">
                <button className="zoom-btn" onClick={() => zoomBuilding(-0.1)} title="Zoom out">−</button>
                <button className="zoom-readout" onClick={resetBuildingZoom} title="Reset zoom">
                  {Math.round((Number(t.buildingScale) || TWEAK_DEFAULTS.buildingScale) * 100)}%
                </button>
                <button className="zoom-btn" onClick={() => zoomBuilding(0.1)} title="Zoom in">+</button>
              </div>
              <div className="building-scroll" ref={buildingScrollRef}>
                <div className="building-stage" style={{
                  transform: `scale(${t.buildingScale})`,
                  '--building-scale': t.buildingScale,
                }}>
                  <Building agents={agents}
                            elevator={elevatorRef.current}
                            packets={packets}
                            selectedId={selectedId}
                            selectedFloorIdx={selectedFloorIdx}
                            selectedElevator={selectedElevator}
                            onClickAgent={selectAgent}
                            onClickFloor={(idx) => selectFloor(idx, false, true)}
                            onClickElevator={selectElevator}
                            showBeam={t.showBeams}
                            time={time}/>
                </div>
              </div>
            </div>

            <Inspector agent={selected}
                       floor={selected || selectedElevator ? null : selectedFloor}
                       elevator={selectedElevator ? elevatorRef.current : null}
                       agents={agents}
                       onClose={clearInspector}
                       onLocate={handleLocate}/>
          </div>

          <StatusBar messages={feed}/>
        </div>
      </>
    );
  }

  function bodyForKind(w) {
    if (w.kind === 'agents') return renderAgentsBody();
    return window.renderWindowBody(w.kind, { onClose: () => wm.close(w.id) });
  }

  const visibleWindows = wm.windows.filter(x => !x.minimized).sort((a, b) => a.z - b.z);
  const minimizedWindows = wm.windows
    .filter(x => x.minimized)
    .map(x => ({ ...x, title: window.APPS[x.kind]?.title || x.kind }));

  if (isPdaViewport) {
    return (
      <div className="mos-desktop pda-desktop" onMouseDown={() => setSelectedIcon(null)}>
        {renderBootSplash()}
        <MobilePdaShell
          agents={agents}
          packets={packets}
          elevator={elevatorRef.current}
          selected={selected}
          selectedFloor={selectedFloor}
          selectedElevator={selectedElevator}
          selectedId={selectedId}
          selectedFloorIdx={selectedFloorIdx}
          feed={feed}
          stats={stats}
          time={time}
          clockLabel={clockLabel}
          paused={paused}
          speed={speed}
          setPaused={setPaused}
          setSpeed={setSpeed}
          buildingScrollRef={buildingScrollRef}
          buildingScale={Number(t.buildingScale) || TWEAK_DEFAULTS.buildingScale}
          showBeams={t.showBeams}
          selectAgent={selectAgent}
          selectFloor={(idx) => selectFloor(idx, false, false)}
          selectElevator={selectElevator}
          clearInspector={clearInspector}
          handleLocate={handleLocate}
          zoomBuilding={zoomBuilding}
          resetBuildingZoom={resetBuildingZoom}
        />
      </div>
    );
  }

  return (
    <div className="mos-desktop" onMouseDown={() => setSelectedIcon(null)}>
      {renderBootSplash()}

      {window.DESKTOP_ICONS.map(ic => {
        const cfg = window.APPS[ic.kind];
        return (
          <DesktopIcon
            key={ic.kind}
            kind={ic.kind}
            label={cfg.iconLabel}
            icon={<PixelIcon kind={cfg.icon}/>}
            x={ic.x} y={ic.y}
            selected={selectedIcon === ic.kind}
            onSelect={setSelectedIcon}
            onOpen={wm.open}
          />
        );
      })}

      {visibleWindows.map(w => {
        const cfg = window.APPS[w.kind] || {};
        return (
          <Win31Window
            key={w.id}
            id={w.id}
            title={cfg.title || w.kind}
            x={w.x} y={w.y} w={w.w} h={w.h}
            maximized={w.maximized}
            focused={w.id === wm.focusedId}
            closeable={cfg.closeable !== false && w.kind !== 'agents'}
            onFocus={() => wm.focus(w.id)}
            onClose={() => wm.close(w.id)}
            onMinimize={() => wm.minimize(w.id)}
            onMaximize={() => wm.toggleMaximize(w.id)}
            onMove={(x, y) => wm.move(w.id, x, y)}
            onResize={(w_, h) => wm.resize(w.id, w_, h)}
          >
            {bodyForKind(w)}
          </Win31Window>
        );
      })}

      <MinimizedStrip windows={minimizedWindows} onRestore={wm.focus}/>

      <TweaksPanel>
        <TweakSection label="Atmosphere"/>
        <TweakSlider label="Scanlines" value={t.scanlines} min={0} max={1} step={0.05}
                     onChange={v => setTweak('scanlines', v)}/>
        <TweakSlider label="Halftone"  value={t.halftone}  min={0} max={1} step={0.05}
                     onChange={v => setTweak('halftone', v)}/>
        <TweakToggle label="Sky beam"  value={t.showBeams}
                     onChange={v => setTweak('showBeams', v)}/>
        <TweakSection label="Building"/>
        <TweakSlider label="Zoom" value={t.buildingScale} min={0.35} max={2.0} step={0.05}
                     onChange={v => setTweak('buildingScale', v)}/>
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
