/* ============ ATHARV.OS — apps (Media Player: MPX deck, iTunes previews) ============ */

/* ===== DOOM — js-dos runs in an ISOLATED same-origin iframe (doom.html) so
   the emulator's heavy WASM/worker code and any of its errors can never crash
   the portfolio. The 5MB shareware bundle loads only when this window opens. ===== */
function DoomContent() {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, background: '#000' }}>
      <iframe
        src="doom.html"
        title="DOOM (shareware, 1993)"
        style={{ flex: 1, minHeight: 0, width: '100%', border: 'none', background: '#000' }}
        allow="autoplay; fullscreen; gamepad"
        // Contain the emulator: no top-nav, no popups, no form posts. It needs
        // scripts + same-origin (to fetch the local bundle) + pointer-lock to play.
        sandbox="allow-scripts allow-same-origin allow-pointer-lock"
        referrerPolicy="no-referrer"
      />
      <div className="statusbar">
        <div className="scell flex">DOOM — id Software (1993 shareware)</div>
        <div className="scell">click the game · arrows + Ctrl</div>
      </div>
    </div>
  );
}

/* ===== Systems Status — live health of Atharv's deployed products (via /api/status) ===== */
function SystemsContent() {
  const [data, setData] = React.useState(null);
  const [state, setState] = React.useState('loading'); // loading | ok | error
  const load = React.useCallback(() => {
    setState(s => (s === 'ok' ? 'ok' : 'loading'));
    fetch('/api/status')
      .then(r => r.ok ? r.json() : Promise.reject())
      .then(j => { setData(j); setState('ok'); })
      .catch(() => setState('error'));
  }, []);
  React.useEffect(() => { load(); const iv = setInterval(load, 60000); return () => clearInterval(iv); }, [load]);

  const light = (up) => (
    <span style={{ display:'inline-block', width:12, height:12, borderRadius:'50%',
      background: up ? '#2ecc40' : '#ff4136', boxShadow: `0 0 6px ${up?'#2ecc40':'#ff4136'}`,
      border:'1px solid rgba(0,0,0,0.4)' }}/>
  );
  const services = (data && data.services) || [];
  const allUp = services.length && services.every(s => s.up);

  return (
    <div className="wbody white" style={{ padding:0, display:'flex', flexDirection:'column' }}>
      <div style={{ padding:'10px 12px', borderBottom:'1px solid #ccc', display:'flex', alignItems:'center', gap:10, background:'#f4f4f4' }}>
        <span style={{ fontSize:18 }}>{allUp ? '🟢' : services.length ? '🟠' : '⏳'}</span>
        <div style={{ flex:1 }}>
          <div style={{ fontWeight:700, fontSize:13 }}>Systems Status</div>
          <div style={{ fontSize:10, color:'#606060' }}>
            {state==='loading' && !data ? 'checking…' : allUp ? 'All systems operational' : services.length ? 'Some services degraded' : ''}
          </div>
        </div>
        <button className="btn" onClick={load} disabled={state==='loading'}>↻ Refresh</button>
      </div>
      <div style={{ flex:1, overflow:'auto', padding:'6px 0' }}>
        {state==='error' && !data && <div style={{ padding:16, fontSize:12, color:'#804000' }}>⚠ Couldn't reach the status service.</div>}
        {services.map((s, i) => (
          <div key={i} style={{ display:'flex', alignItems:'center', gap:10, padding:'8px 14px', borderBottom:'1px solid #eee' }}>
            {light(s.up)}
            <div style={{ flex:1, minWidth:0 }}>
              <div style={{ fontSize:12, fontWeight:700 }}>{s.name}</div>
              <a href={s.url} target="_blank" rel="noreferrer" style={{ fontSize:10, color:'#0000AA' }}>{s.url.replace(/^https?:\/\//,'').replace(/\/$/,'')}</a>
            </div>
            <div style={{ textAlign:'right', fontFamily:'Fixedsys, monospace', fontSize:11 }}>
              <div style={{ color: s.up ? '#1a7a1a' : '#b00' }}>{s.up ? 'UP' : (s.error || 'DOWN')}</div>
              <div style={{ color:'#808080' }}>{s.status ? `HTTP ${s.status}` : ''} {typeof s.ms==='number' ? `· ${s.ms}ms` : ''}</div>
            </div>
          </div>
        ))}
      </div>
      <div className="statusbar">
        <div className="scell flex">{data ? `Last checked ${new Date(data.checkedAt).toLocaleTimeString()}` : 'Live health monitor'}</div>
        <div className="scell">auto-refresh 60s</div>
      </div>
    </div>
  );
}

/* Pixel-art transport glyphs (12x12) */
const TP_GLYPHS = {
  prev: `<svg viewBox="0 0 12 12" width="12" height="12" shape-rendering="crispEdges"><rect x="1" y="2" width="1" height="8" fill="#000"/><rect x="2" y="2" width="1" height="8" fill="#000"/><polygon points="3,6 7,2 7,10" fill="#000"/><polygon points="7,6 11,2 11,10" fill="#000"/></svg>`,
  play: `<svg viewBox="0 0 12 12" width="12" height="12" shape-rendering="crispEdges"><polygon points="3,1 3,11 11,6" fill="#000"/></svg>`,
  pause: `<svg viewBox="0 0 12 12" width="12" height="12" shape-rendering="crispEdges"><rect x="3" y="2" width="2" height="8" fill="#000"/><rect x="7" y="2" width="2" height="8" fill="#000"/></svg>`,
  stop: `<svg viewBox="0 0 12 12" width="12" height="12" shape-rendering="crispEdges"><rect x="2" y="2" width="8" height="8" fill="#000"/></svg>`,
  next: `<svg viewBox="0 0 12 12" width="12" height="12" shape-rendering="crispEdges"><polygon points="1,2 5,6 1,10" fill="#000"/><polygon points="5,2 9,6 5,10" fill="#000"/><rect x="9" y="2" width="1" height="8" fill="#000"/><rect x="10" y="2" width="1" height="8" fill="#000"/></svg>`,
};

/* ============ Music engine — iTunes 30-second previews ============
   One shared <audio>; resolver: /api/music proxy (prod) → direct iTunes
   fetch (dev fallback) → localStorage cache (aos_music_cache_v1, 7d TTL).
   Exposes window.AOS_MUSIC; broadcasts state via the aos:music CustomEvent
   (app.jsx mirrors it into mp / NowPlayingWidget). Respects master mute:
   mute pauses instantly, unmute never auto-resumes. */
const AOS_MUSIC = window.AOS_MUSIC || (() => {
  const audio = new Audio();
  audio.crossOrigin = 'anonymous';   // lets the analyser read the Apple CDN
  audio.preload = 'auto';
  audio.volume = 0.7;
  const CACHE_KEY = 'aos_music_cache_v1';
  const TTL = 7 * 86400e3;
  let cache = {};
  try { cache = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}'); } catch (e) {}
  const cacheGet = (q) => { const h = cache[q]; return h && Date.now() - h.at < TTL ? h.d : null; };
  const cacheSet = (q, d) => { cache[q] = { at: Date.now(), d: d }; try { localStorage.setItem(CACHE_KEY, JSON.stringify(cache)); } catch (e) {} };
  const trim = (r) => r && r.previewUrl ? {
    title: r.trackName, artist: r.artistName, previewUrl: r.previewUrl,
    artworkUrl: (r.artworkUrl100 || '').replace('100x100', '300x300'),
    trackTimeMillis: r.trackTimeMillis
  } : null;
  async function resolve(track) {
    const q = track.artist + ' ' + track.title;
    const hit = cacheGet(q);
    if (hit) return hit.previewUrl ? hit : null;   // misses cached too
    let d = null;
    try {
      const r = await fetch('/api/music?q=' + encodeURIComponent(q) + '&limit=1');
      if (r.ok) { const j = await r.json(); if (j && j.previewUrl) d = j; }
    } catch (e) {}
    if (!d) {
      try { // dev fallback (no serverless locally): direct iTunes
        const r = await fetch('https://itunes.apple.com/search?term=' + encodeURIComponent(q) + '&media=music&limit=1');
        const j = await r.json();
        d = trim(j.results && j.results.find((x) => x.previewUrl));
      } catch (e) {}
    }
    cacheSet(q, d || {});
    return d;
  }

  let st = { pl: 0, idx: -1, playing: false, meta: null, resolving: false, error: false };
  const subs = new Set();
  function emit() {
    subs.forEach((f) => { try { f(st); } catch (e) {} });
    const tr = st.idx >= 0 ? D.playlists[st.pl].tracks[st.idx] : null;
    const cur = tr ? {
      playlist: D.playlists[st.pl].name,
      title: st.meta ? st.meta.title : tr.title,
      artist: st.meta ? st.meta.artist : tr.artist
    } : null;
    try { window.dispatchEvent(new CustomEvent('aos:music', { detail: { playing: st.playing, current: cur } })); } catch (e) {}
  }

  /* Web Audio analyser — real spectrum when the CDN response is CORS-clean;
     a tainted source still plays but reads all-zeros → auto-fallback to fake. */
  let actx = null, analyser = null, panner = null, analyserDead = false, zeroFrames = 0;
  let shuffle = false, repeat = false;
  /* classic 10-band graphic EQ (Winamp bands) + preamp — real when the Web
     Audio chain is up, remembered and applied later otherwise */
  const EQ_FREQS = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000];
  let eqFilters = null, preampNode = null, eqOn = true;
  const eqGains = new Array(10).fill(0);
  let preampDb = 0;
  const dbLin = (db) => Math.pow(10, db / 20);
  function applyEq() {
    if (!eqFilters) return;
    for (let i = 0; i < eqFilters.length; i++) eqFilters[i].gain.value = eqOn ? eqGains[i] : 0;
    if (preampNode) preampNode.gain.value = eqOn ? dbLin(preampDb) : 1;
  }
  function ensureAnalyser() {
    if (actx || analyserDead) return;
    try {
      const AC = window.AudioContext || window.webkitAudioContext;
      actx = new AC();
      const srcNode = actx.createMediaElementSource(audio);
      analyser = actx.createAnalyser();
      analyser.fftSize = 128;
      analyser.smoothingTimeConstant = 0.72;
      preampNode = actx.createGain();
      eqFilters = EQ_FREQS.map((f, i) => {
        const q = actx.createBiquadFilter();
        q.type = i === 0 ? 'lowshelf' : i === EQ_FREQS.length - 1 ? 'highshelf' : 'peaking';
        q.frequency.value = f;
        if (q.Q) q.Q.value = 1.1;
        return q;
      });
      let node = srcNode;
      node.connect(preampNode); node = preampNode;
      eqFilters.forEach((f) => { node.connect(f); node = f; });
      node.connect(analyser);
      panner = actx.createStereoPanner ? actx.createStereoPanner() : null;
      if (panner) { analyser.connect(panner); panner.connect(actx.destination); }
      else analyser.connect(actx.destination);
      applyEq();
    } catch (e) { analyserDead = true; actx = null; analyser = null; }
  }
  let _bins = null;
  function getSpectrum(out) {  // fills out[] with 0..1; returns true if REAL
    if (!analyser || analyserDead) return false;
    if (!_bins) _bins = new Uint8Array(analyser.frequencyBinCount);
    analyser.getByteFrequencyData(_bins);
    let sum = 0;
    for (let i = 0; i < _bins.length; i++) sum += _bins[i];
    if (st.playing && !audio.muted && audio.currentTime > 0.6) {
      if (sum === 0) { if (++zeroFrames > 45) analyserDead = true; }  // tainted
      else zeroFrames = 0;
    }
    if (analyserDead || sum === 0) return false;
    const n = out.length, half = Math.floor(_bins.length * 0.75); // drop dead top
    for (let i = 0; i < n; i++) out[i] = _bins[Math.floor(i * half / n)] / 255;
    return true;
  }

  let token = 0, failStreak = 0;
  async function playTrack(pl, idx) {
    const list = D.playlists[pl].tracks;
    if (!list[idx]) return;
    const my = ++token;
    st.pl = pl; st.idx = idx; st.meta = null; st.resolving = true; st.error = false;
    emit();
    const meta = await resolve(list[idx]);
    if (my !== token) return;
    st.resolving = false;
    if (!meta) {
      st.error = true; failStreak++;
      emit();
      if (failStreak < list.length) next();   // skip unavailable, never loop
      return;
    }
    failStreak = 0;
    st.meta = meta;
    audio.src = meta.previewUrl;
    ensureAnalyser();
    if (actx && actx.state === 'suspended') { try { actx.resume(); } catch (e) {} }
    audio.muted = !!window.__atharvMuted;
    audio.play().catch(() => {});
    emit();
  }
  function toggle() {
    if (st.idx < 0) { playTrack(st.pl, 0); return; }
    if (st.playing) { audio.pause(); return; }
    ensureAnalyser();
    if (actx && actx.state === 'suspended') { try { actx.resume(); } catch (e) {} }
    audio.muted = !!window.__atharvMuted;
    if (audio.src) audio.play().catch(() => {});
    else playTrack(st.pl, Math.max(0, st.idx));
  }
  function next() {
    const n = D.playlists[st.pl].tracks.length;
    if (!n) return;
    if (shuffle && n > 1) {
      let i; do { i = Math.floor(Math.random() * n); } while (i === st.idx);
      playTrack(st.pl, i);
    } else playTrack(st.pl, (st.idx + 1) % n);
  }
  function prev() { const n = D.playlists[st.pl].tracks.length; if (n) playTrack(st.pl, (st.idx - 1 + n) % n); }
  function stop() { audio.pause(); try { audio.currentTime = 0; } catch (e) {} }
  function seek(frac) { if (audio.duration) { try { audio.currentTime = frac * audio.duration; } catch (e) {} } }
  function setVolume(v) { audio.volume = Math.min(1, Math.max(0, v)); }

  audio.addEventListener('play', () => { st.playing = true; emit(); });
  audio.addEventListener('pause', () => { st.playing = false; emit(); });
  audio.addEventListener('ended', () => {
    if (repeat && audio.src) { try { audio.currentTime = 0; } catch (e) {} audio.play().catch(() => {}); return; }
    next();
  });
  audio.addEventListener('error', () => {
    if (st.idx < 0 || !audio.src) return;
    st.error = true; failStreak++;
    emit();
    if (failStreak < D.playlists[st.pl].tracks.length) next();
  });
  /* master mute: pause instantly; unmute does NOT auto-resume */
  window.addEventListener('atharv-os:mute', (e) => {
    const m = !!(e.detail && e.detail.muted);
    audio.muted = m;
    if (m && st.playing) audio.pause();
  });
  audio.muted = !!window.__atharvMuted;

  return {
    audio: audio, playTrack: playTrack, toggle: toggle, next: next, prev: prev,
    stop: stop, seek: seek, setVolume: setVolume, getSpectrum: getSpectrum,
    setBalance: (p) => { if (panner) { try { panner.pan.value = Math.min(1, Math.max(-1, p)); } catch (e) {} } },
    setShuffle: (v) => { shuffle = !!v; }, setRepeat: (v) => { repeat = !!v; },
    setEq: (i, db) => { eqGains[i] = db; applyEq(); },
    setPreamp: (db) => { preampDb = db; applyEq(); },
    setEqOn: (v) => { eqOn = !!v; applyEq(); },
    eqGains: () => eqGains.slice(), eqFreqs: EQ_FREQS,
    state: () => st,
    subscribe: (f) => subs.add(f), unsubscribe: (f) => subs.delete(f)
  };
})();
window.AOS_MUSIC = AOS_MUSIC;

/* ============ Media Player — the MPX deck ============ */
/* vertical fader — EQ (chrome ball on dark slot) */
function MpxVSlider({ label, value, min, max, onChange }) {
  const ref = React.useRef(null);
  const frac = Math.min(1, Math.max(0, (value - min) / (max - min)));
  const set = (e) => {
    const el = ref.current; if (!el) return;
    const r = el.getBoundingClientRect();
    const f = Math.min(1, Math.max(0, 1 - (e.clientY - r.top) / r.height));
    onChange(min + f * (max - min));
  };
  const down = (e) => {
    e.preventDefault(); e.stopPropagation(); set(e);
    const mv = (ev) => set(ev);
    const up = () => { window.removeEventListener('pointermove', mv); window.removeEventListener('pointerup', up); };
    window.addEventListener('pointermove', mv);
    window.addEventListener('pointerup', up);
  };
  return (
    <div className="kj-vs" data-nodrag="1">
      <div className="kj-vs-track" ref={ref} onPointerDown={down} onDoubleClick={() => onChange((min + max) / 2)} title={label}>
        <i className="kj-ball" style={{ bottom: 'calc(' + frac * 100 + '% - 6px)' }}/>
      </div>
      <span className="kj-lbl">{label}</span>
    </div>
  );
}
/* horizontal chrome fader — volume (always visible) */
function MpxHSlider({ value, min, max, onChange, title }) {
  const ref = React.useRef(null);
  const frac = Math.min(1, Math.max(0, (value - min) / (max - min)));
  const set = (e) => {
    const el = ref.current; if (!el) return;
    const r = el.getBoundingClientRect();
    const f = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width));
    onChange(min + f * (max - min));
  };
  const down = (e) => {
    e.preventDefault(); e.stopPropagation(); set(e);
    const mv = (ev) => set(ev);
    const up = () => { window.removeEventListener('pointermove', mv); window.removeEventListener('pointerup', up); };
    window.addEventListener('pointermove', mv);
    window.addEventListener('pointerup', up);
  };
  return (
    <div className="kj-hs" ref={ref} onPointerDown={down} data-nodrag="1" title={title}>
      <i className="kj-hs-fill" style={{ width: frac * 100 + '%' }}/>
      <i className="kj-ball big" style={{ left: 'calc(' + frac * 100 + '% - 8px)' }}/>
    </div>
  );
}

/* Chrome hi-fi slab — angular faceted brushed metal, ice-blue glass.
   Chromeless window; [data-drag] metal moves it. */
function MediaPlayerContent({ onWinClose, onWinMinimize }) {
  const M = window.AOS_MUSIC;
  const [st, setSt] = React.useState(() => Object.assign({}, M.state()));
  const [time, setTime] = React.useState({ t: M.audio.currentTime || 0, d: M.audio.duration || 30, buf: 0 });
  const [vol, setVol] = React.useState(Math.round(M.audio.volume * 100));
  const [flags, setFlags] = React.useState({ shuffle: false, repeat: false });
  const [eq, setEq] = React.useState({ on: true, pre: 0, g: M.eqGains() });
  const [tray, setTray] = React.useState('closed');   // list | eq | closed
  const canvasRef = React.useRef(null);
  const seekRef = React.useRef(null);
  const reduced = React.useMemo(() => window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches, []);
  const click = () => { if (window.sfx && window.sfx.click) window.sfx.click(); };

  React.useEffect(() => {
    const f = (s) => setSt(Object.assign({}, s));
    M.subscribe(f);
    const tu = () => {
      const a = M.audio;
      let buf = 0;
      try { if (a.buffered.length) buf = a.buffered.end(a.buffered.length - 1); } catch (e) {}
      setTime({ t: a.currentTime || 0, d: a.duration || 30, buf: buf });
    };
    M.audio.addEventListener('timeupdate', tu);
    M.audio.addEventListener('progress', tu);
    M.audio.addEventListener('durationchange', tu);
    return () => {
      M.unsubscribe(f);
      M.audio.removeEventListener('timeupdate', tu);
      M.audio.removeEventListener('progress', tu);
      M.audio.removeEventListener('durationchange', tu);
    };
  }, []);

  /* ice-blue spectrum — ONE canvas + rAF, paused when hidden/tv-off */
  React.useEffect(() => {
    const cv = canvasRef.current;
    if (!cv) return;
    const ctx = cv.getContext('2d');
    const N = 18, CELLS = 8;
    const vals = new Float32Array(N), peaks = new Float32Array(N);
    let raf = 0, alive = true;
    const paint = () => {
      const W = cv.width, H = cv.height;
      ctx.clearRect(0, 0, W, H);
      const bw = Math.floor(W / N);
      for (let i = 0; i < N; i++) {
        const lit = Math.round(vals[i] * CELLS);
        for (let c = 0; c < CELLS; c++) {
          const y = H - 2 - c * 4;
          ctx.fillStyle = c >= lit ? 'rgba(150,210,255,0.08)' : (c < 6 ? '#7cc4ff' : '#eaf6ff');
          ctx.fillRect(i * bw + 1, y, bw - 3, 3);
        }
        if (vals[i] > peaks[i]) peaks[i] = vals[i]; else peaks[i] = Math.max(0, peaks[i] - 0.014);
        const py = H - 3 - Math.round(peaks[i] * CELLS) * 4;
        ctx.fillStyle = '#ffffff';
        ctx.fillRect(i * bw + 1, py, bw - 3, 2);
      }
    };
    const fake = () => {
      const t = performance.now() / 1000;
      const s = M.state();
      for (let i = 0; i < N; i++) {
        if (s.playing) {
          const a = Math.abs(Math.sin(t * (1.6 + i * 0.31) + i * 2.1));
          const b = Math.abs(Math.sin(t * 2.3 + i * 0.9));
          vals[i] = Math.max(0.04, a * (0.3 + 0.6 * b) * M.audio.volume + 0.05);
        } else vals[i] = Math.max(0, vals[i] - 0.05);
      }
    };
    const frame = () => {
      if (!alive) return;
      const hidden = document.hidden || cv.offsetParent === null ||
        document.documentElement.classList.contains('tv-off');
      if (hidden) { setTimeout(() => { if (alive) frame(); }, 500); return; }
      if (!M.getSpectrum(vals)) fake();
      paint();
      if (!reduced) raf = requestAnimationFrame(frame);
    };
    if (reduced) { for (let i = 0; i < N; i++) vals[i] = st.playing ? 0.4 + 0.3 * Math.abs(Math.sin(i)) : 0.05; paint(); }
    else frame();
    return () => { alive = false; cancelAnimationFrame(raf); };
  }, [reduced, st.playing, st.pl]);

  const [art, setArt] = React.useState(false);         // right-side art drawer
  const pl = D.playlists[st.pl];
  const tr = st.idx >= 0 ? pl.tracks[st.idx] : null;
  const fmt = (s) => { s = Math.max(0, Math.floor(s || 0)); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
  const spotifySearch = (t) => 'https://open.spotify.com/search/' + encodeURIComponent(t.artist + ' ' + t.title);
  const mtxt = (st.resolving ? 'resolving…  ' : '') +
    (tr ? (st.meta ? st.meta.artist + ' — ' + st.meta.title : tr.artist + ' — ' + tr.title) : pl.name) +
    '  ·  0:30 preview  ·  ';
  const pct = time.d ? Math.min(100, (time.t / time.d) * 100) : 0;
  const bufPct = time.d ? Math.min(100, (time.buf / time.d) * 100) : 0;

  const doSeek = (e) => {
    const el = seekRef.current; if (!el) return;
    const r = el.getBoundingClientRect();
    M.seek(Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)));
  };
  const seekDown = (e) => {
    e.preventDefault(); e.stopPropagation(); doSeek(e);
    const mv = (ev) => doSeek(ev);
    const up = () => { window.removeEventListener('pointermove', mv); window.removeEventListener('pointerup', up); };
    window.addEventListener('pointermove', mv);
    window.addEventListener('pointerup', up);
  };
  const onKey = (e) => {
    if (e.code !== 'Space') return;
    const tag = (e.target.tagName || '').toLowerCase();
    if (tag === 'input' || tag === 'button' || tag === 'a' || tag === 'textarea') return;
    e.preventDefault();
    M.toggle();
  };
  const toggleFlag = (k) => {
    click();
    setFlags((f) => {
      const n = Object.assign({}, f);
      n[k] = !f[k];
      if (k === 'shuffle') M.setShuffle(n.shuffle); else M.setRepeat(n.repeat);
      return n;
    });
  };
  const setBand = (i, v) => {
    setEq((s) => { const g = s.g.slice(); g[i] = v; M.setEq(i, v); return Object.assign({}, s, { g: g }); });
  };
  const EQ_LABELS = ['60', '170', '310', '600', '1k', '3k', '6k', '12k', '14k', '16k'];

  return (
    <div className="kj" tabIndex={0} onKeyDown={onKey}>
      <div className="kj-shadow">
        <div className="kj-body" data-drag="1">
          <div className="kj-face"/>
          <div className="kj-in">
            <div className="kj-toprow">
              <span className="kj-brand">devarv <b>audio</b></span>
              <div className="kj-caps" data-nodrag="1">
                <button className="kj-cap" title="Minimize" onClick={onWinMinimize}>–</button>
                <button className="kj-cap" title="Close" onClick={onWinClose}>✕</button>
              </div>
            </div>
            {/* smoked glass display */}
            <div className="kj-glass">
              <div className="kj-marquee"><span>{mtxt}{mtxt}</span></div>
              <div className="kj-glass-row">
                <div className="kj-time">{fmt(time.t)}<em>/ 0:{String(Math.floor(time.d || 30)).padStart(2, '0')}</em></div>
                <canvas ref={canvasRef} className="kj-viz" width="144" height="34"/>
              </div>
              <div className="kj-seek" ref={seekRef} onPointerDown={seekDown} data-nodrag="1" title="Seek">
                <i className="kj-seek-buf" style={{ width: bufPct + '%' }}/>
                <i className="kj-seek-fill" style={{ width: pct + '%' }}/>
              </div>
              <div className="kj-glass-foot">
                <span className="blink">preview</span>
                <span>{st.resolving ? 'resolving' : st.error ? 'skipped' : st.playing ? 'playing' : st.idx >= 0 ? 'paused' : 'stopped'}</span>
                <span className="r">{pl.id === 'mohave' ? '95.1 mohave' : '71.0 stairway'}</span>
              </div>
            </div>
            {/* transport + volume */}
            <div className="kj-ctl">
              <button className="kj-orb" title="Previous" onClick={() => { click(); M.prev(); }} dangerouslySetInnerHTML={{ __html: TP_GLYPHS.prev }}/>
              <button className="kj-orb main" title={st.playing ? 'Pause' : 'Play'} onClick={() => { click(); M.toggle(); }} dangerouslySetInnerHTML={{ __html: st.playing ? TP_GLYPHS.pause : TP_GLYPHS.play }}/>
              <button className="kj-orb" title="Stop" onClick={() => { click(); M.stop(); }} dangerouslySetInnerHTML={{ __html: TP_GLYPHS.stop }}/>
              <button className="kj-orb" title="Next" onClick={() => { click(); M.next(); }} dangerouslySetInnerHTML={{ __html: TP_GLYPHS.next }}/>
              <div className="kj-volwrap">
                <span className="kj-lbl">volume</span>
                <MpxHSlider value={vol} min={0} max={100} title={'Volume ' + vol + '%'}
                  onChange={(v) => { setVol(Math.round(v)); M.setVolume(v / 100); }}/>
              </div>
            </div>
            {/* slim option row */}
            <div className="kj-opts">
              {D.playlists.map((p, i) => (
                <button key={p.id} className={'kj-tab' + (i === st.pl ? ' on' : '')}
                  onClick={() => { click(); if (i !== st.pl || st.idx < 0) M.playTrack(i, 0); }} title={p.tagline}>
                  {p.id === 'mohave' ? 'mohave' : 'stairway'}
                </button>
              ))}
              <span className="kj-div"/>
              <button className={'kj-tab' + (flags.shuffle ? ' on' : '')} onClick={() => toggleFlag('shuffle')}>shuffle</button>
              <button className={'kj-tab' + (flags.repeat ? ' on' : '')} onClick={() => toggleFlag('repeat')}>repeat</button>
              <span className="kj-div"/>
              <button className={'kj-tab' + (tray === 'list' ? ' on' : '')} onClick={() => { click(); setTray(tray === 'list' ? 'closed' : 'list'); }}>playlist</button>
              <button className={'kj-tab' + (tray === 'eq' ? ' on' : '')} onClick={() => { click(); setTray(tray === 'eq' ? 'closed' : 'eq'); }}>eq</button>
              <button className={'kj-tab' + (art ? ' on' : '')} onClick={() => { click(); setArt(!art); }}>art</button>
              {tr ? <a className="kj-tab gold" href={spotifySearch(tr)} target="_blank" rel="noopener noreferrer">full song ↗</a> : null}
              {pl && pl.spotifyId ? <a className="kj-tab gold" href={'https://open.spotify.com/playlist/' + pl.spotifyId} target="_blank" rel="noopener noreferrer" title="the real playlist this cassette is named after">playlist ↗</a> : null}
            </div>
          </div>
        </div>
        {/* right-side album art drawer */}
        {art ? (
          <div className="kj-art" data-drag="1">
            <div className="kj-art-well">
              {st.meta && st.meta.artworkUrl
                ? <img src={st.meta.artworkUrl} alt="" draggable={false}/>
                : <span className="kj-art-ph">{st.resolving ? 'resolving…' : 'no signal'}</span>}
            </div>
            <span className="kj-art-cap">{st.meta ? st.meta.title : tr ? tr.title : pl.name}</span>
          </div>
        ) : null}
        {/* angular smoked-glass tray */}
        {tray !== 'closed' ? (
          <div className="kj-tray" data-drag="1">
            {tray === 'list' ? (
              <div className="kj-list" data-nodrag="1">
                {pl.tracks.map((t, i) => (
                  <div key={i} className={'kj-tr' + (i === st.idx ? ' cur' : '')}
                    onDoubleClick={() => { click(); M.playTrack(st.pl, i); }} title="Double-click to play">
                    <span className="kj-tr-n">{i === st.idx ? (st.playing ? '▶' : '❚❚') : String(i + 1).padStart(2, '0')}</span>
                    <span className="kj-tr-t">{t.title}<i> — {t.artist}</i></span>
                    <a className="kj-tr-full" href={spotifySearch(t)} target="_blank" rel="noopener noreferrer"
                      onClick={(e) => e.stopPropagation()} onDoubleClick={(e) => e.stopPropagation()}>full ↗</a>
                  </div>
                ))}
              </div>
            ) : (
              <div className="kj-eq">
                <MpxVSlider label="pre" value={eq.pre} min={-12} max={12}
                  onChange={(v) => setEq((s) => { M.setPreamp(v); return Object.assign({}, s, { pre: v }); })}/>
                {EQ_LABELS.map((l, i) => (
                  <MpxVSlider key={l} label={l} value={eq.g[i]} min={-12} max={12} onChange={(v) => setBand(i, v)}/>
                ))}
                <div className="kj-eq-btns">
                  <button className={'kj-tab' + (eq.on ? ' on' : '')} onClick={() => { click(); setEq((s) => { M.setEqOn(!s.on); return Object.assign({}, s, { on: !s.on }); }); }}>{eq.on ? 'eq on' : 'eq off'}</button>
                  <button className="kj-tab" onClick={() => { click(); setEq((s) => { for (let i = 0; i < 10; i++) M.setEq(i, 0); M.setPreamp(0); return Object.assign({}, s, { pre: 0, g: new Array(10).fill(0) }); }); }}>reset</button>
                </div>
              </div>
            )}
          </div>
        ) : null}
      </div>
    </div>
  );
}

/* ===== My Computer / File Explorer ===== */
function MyComputerContent({ openApp, openProject, iconOverrides = {}, folderIcons = {} }) {
  const [path, setPath] = React.useState('C:\\');
  const [view, setView] = React.useState('icons');

  const tree = {
    'C:\\': [
      { name: 'Projects', type: 'folder', target: 'C:\\Projects', folderKey: 'projects' },
      { name: 'Awards', type: 'folder', target: 'C:\\Awards', folderKey: 'awards' },
      { name: 'Photos', type: 'folder', target: 'C:\\Photos', folderKey: 'photos' },
      { name: 'Notes', type: 'folder', target: 'C:\\Notes', folderKey: 'notes' },
      { name: 'README.txt', type: 'file', icon: 'readme', action: () => openApp('readme') },
      { name: 'Resume.pdf', type: 'file', icon: 'pdf', action: () => openApp('resume') },
    ],
    'C:\\Projects': D.projects.map(p => ({
      name: p.name + '.exe', type: 'file',
      icon: iconOverrides[p.slug] || p.iconKey,
      color: p.themeColor,
      action: () => openProject(p.slug)
    })),
    'C:\\Awards': [{ name: 'Awards', type: 'folder', action: () => openApp('awards') }],
    'C:\\Photos': [{ name: 'Photos', type: 'folder', action: () => openApp('photos') }],
    'C:\\Notes': [{ name: 'Notes.txt', type: 'file', icon: 'notes', action: () => openApp('notepad') }],
  };
  const items = tree[path] || [];

  return (
    <div style={{display:'flex', flexDirection:'column', flex:1, minHeight:0}}>
      <div className="menubar">
        <button><span className="u">F</span>ile</button>
        <button><span className="u">E</span>dit</button>
        <button><span className="u">V</span>iew</button>
        <button><span className="u">H</span>elp</button>
      </div>
      <div className="tbtoolbar">
        <button className="tbbtn" disabled={path==='C:\\'} onClick={()=>setPath(p => {
          const up = p.split('\\').slice(0,-1).join('\\');
          return (!up || up === 'C:') ? 'C:\\' : up;
        })}>↑ Up</button>
        <div className="tbsep"/>
        <button className={`tbbtn ${view==='icons'?'active':''}`} onClick={()=>setView('icons')}>▦ Icons</button>
        <button className={`tbbtn ${view==='details'?'active':''}`} onClick={()=>setView('details')}>≡ Details</button>
        <div className="tbsep"/>
        <span style={{fontSize:11, alignSelf:'center'}}><b>Address:</b> {path}</span>
      </div>
      <div className="wbody white" style={{flex:1, padding:0}}>
        {view==='icons' ? (
          <div className="icogrid" style={{padding:12}}>
            {items.map((it,i) => (
              <div className="item" key={i} onDoubleClick={()=>{
                if (it.type==='folder' && it.target) setPath(it.target);
                else if (it.action) it.action();
                else if (it.target) setPath(it.target);
              }} onClick={()=>{
                if (it.type==='folder' && it.target) setPath(it.target);
                else if (it.action) it.action();
                else if (it.target) setPath(it.target);
              }}>
                <div className="ico" dangerouslySetInnerHTML={{__html:
                  it.type==='folder'
                    ? (it.folderKey && folderIcons[it.folderKey]
                        ? (ICO[folderIcons[it.folderKey]] ? ICO[folderIcons[it.folderKey]](32) : ICO.folder(32))
                        : ICO.folder(32))
                    : (ICO[it.icon] ? ICO[it.icon](32, it.color) : ICO.readme(32))
                }}/>
                <div className="lbl">{it.name}</div>
              </div>
            ))}
          </div>
        ) : (
          <table className="detailtable">
            <thead><tr><th>Name</th><th>Type</th><th>Size</th></tr></thead>
            <tbody>
              {items.map((it,i) => (
                <tr key={i} onDoubleClick={()=>{
                  if (it.type==='folder' && it.target) setPath(it.target);
                  else if (it.action) it.action();
                }}>
                  <td>📄 {it.name}</td>
                  <td>{it.type==='folder'?'File Folder':'Application'}</td>
                  <td>{Math.round(Math.random()*900+100)} KB</td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>
      <div className="statusbar">
        <div className="scell flex">{items.length} object(s)</div>
        <div className="scell">{path}</div>
      </div>
    </div>
  );
}

/* ===== README ===== */
function ReadmeContent() {
  return (
    <div className="notepad-body">
{`README.TXT — devarv's portfolio
=====================================

Hello, traveller.

This is ATHARV.OS — a portfolio dressed up as a 1995
operating system because the real one I grew up with
came on dusty hand-me-down PCs and 1.44MB floppies.

What is in here:
  - 3 solo-built live products + 14 more (My Computer)
  - Real awards: HackathonX 1st (2025), HACKAP 2nd (2024)
  - Live links to deployed systems
  - A Notepad with field notes from the mountains
  - A Media Player spinning 30-second jukebox previews
  - A Resume.pdf for the recruiter in a hurry

Easter eggs:
  - Try Win+R (Run dialog)
  - Look closely at the desktop. It tilts.
  - Drag a window. It lifts.
  - The mountains are real. So am I.

Contact:
  atharv5873@gmail.com
  github.com/Atharv5873

— Atharv (devarv), Manali, Himachal Pradesh
`}
    </div>
  );
}

/* ===== Recycle Bin ===== */
function RecycleBinContent() {
  React.useEffect(() => {
    if (window.sfx) window.sfx.recycle();
  }, []);
  return (
    <div className="wbody white">
      <div style={{padding:24, textAlign:'center', color:'#404040'}}>
        <div dangerouslySetInnerHTML={{__html: ICO.recycle(64)}} style={{display:'inline-block', marginBottom:12}}/>
        <h3 style={{fontSize:13, fontWeight:700, marginBottom:6}}>Recycle Bin is empty.</h3>
        <p style={{fontSize:11, lineHeight:1.5}}>
          (Things devarv tried that didn't work — they're all here, but the file system swallowed them.)<br/><br/>
          <i>Failed migrations, abandoned drafts, an n8n side project, a doomed attempt to learn Rust on a 7-hour bus ride to Kullu, and a YAML parser written in pure Bash.</i><br/><br/>
          They served their purpose. Onward.
        </p>
      </div>
    </div>
  );
}

/* ===== Terminal ===== */
function TerminalContent({ openApp, openProject, lineHistory, addLine, sudoMode }) {
  const inputRef = React.useRef();
  const bottomRef = React.useRef();
  const [val, setVal] = React.useState('');
  const [cmdHist, setCmdHist] = React.useState([]);
  const [histIdx, setHistIdx] = React.useState(-1);

  React.useEffect(() => {
    bottomRef.current?.scrollIntoView({behavior:'auto'});
    inputRef.current?.focus();
  }, [lineHistory]);

  const out = (text) => addLine({ type:'out', text });
  const err = (text) => addLine({ type:'err', text });

  // slug/name resolver for `open` and `cat`
  const projList = (D.projects || []);
  const findProj = (q) => projList.find(p => p.slug === q || p.name.toLowerCase() === q || p.name.toLowerCase().includes(q));
  const APP_ALIASES = { about:'about', me:'about', projects:'mycomputer', 'my computer':'mycomputer', mycomputer:'mycomputer',
    skills:'skills', experience:'experience', exp:'experience', awards:'awards', photos:'photos', notes:'notepad', notepad:'notepad',
    resume:'resume', cv:'resume', readme:'readme', network:'network', ie:'ie', browser:'ie', media:'mediaplayer', spotify:'mediaplayer',
    chat:'chat', ai:'chat', devarv:'chat', hiring:'hiring', hire:'hiring', solitaire:'solitaire', minesweeper:'minesweeper',
    mines:'minesweeper', doom:'doom', terminal:'terminal' };

  const run = (raw) => {
    const cmd = raw.trim();
    if (!cmd) return;
    addLine({ type:'cmd', text: cmd });
    setCmdHist(h => [...h, cmd]); setHistIdx(-1);
    const c = cmd.toLowerCase();
    const [verb, ...rest] = c.split(/\s+/);
    const arg = rest.join(' ');

    if (c==='help' || c==='?' || verb==='man') {
      out([
        'ATHARV.OS shell — available commands:',
        '  help              this list',
        '  whoami / id       who is logged in',
        '  ls [projects]     list files / projects',
        '  open <name>       open an app or project window   (e.g. `open hiring`, `open royalbankpacific`)',
        '  cat <file>        README.txt · resume · <project>',
        '  projects          summary of all projects',
        '  skills            top skills by category',
        '  experience        work history',
        '  status            live status of my deployed products',
        '  neofetch          system info',
        '  pwd · date · echo · uname -a · history · clear · exit',
        '  easter eggs:      sudo · make-coffee · matrix · konami · hire',
      ].join('\n'));
    }
    else if (c==='whoami' || c==='id') out(sudoMode ? 'root' : 'devarv — Atharv Sharma, Backend & AI Engineer');
    else if (verb==='ls' || verb==='dir') {
      if (arg.startsWith('project')) out(projList.map(p => p.slug).join('  '));
      else out('projects/  awards/  photos/  notes/  resume.pdf  README.txt  HIRING.TXT');
    }
    else if (verb==='open') {
      const p = findProj(arg);
      if (p && openProject) { out(`opening ${p.name} …`); openProject(p.slug); }
      else if (APP_ALIASES[arg]) { out(`opening ${arg} …`); openApp(APP_ALIASES[arg]); }
      else err(`open: '${arg||''}' not found — try \`ls projects\` or \`open hiring\``);
    }
    else if (verb==='cat') {
      if (arg==='readme.txt' || arg==='readme') { out('opening README.txt …'); openApp('readme'); }
      else if (arg==='resume' || arg==='resume.pdf' || arg==='cv') { out('opening Resume.pdf …'); openApp('resume'); }
      else { const p = findProj(arg); if (p) out(`${p.name} ${p.version||''}\n  ${p.tagline}\n  stack: ${p.stack.join(', ')}\n  ${p.headlineMetric||''}${p.live?'\n  live: '+p.live:''}${p.repo?'\n  repo: '+p.repo:''}`); else err(`cat: ${arg||''}: no such file`); }
    }
    else if (c==='projects') out(projList.map(p => `  ${p.name}${p.live?'  ['+p.live.replace(/^https?:\/\//,'').replace(/\/$/,'')+']':''}`).join('\n'));
    else if (c==='skills') out((D.skillCategories||[]).slice(0,4).map(cat => `  ${cat.name}: ${cat.skills.slice(0,4).map(s=>s.n).join(', ')}`).join('\n'));
    else if (c==='experience' || c==='exp') out((D.experience||[]).map(e => `  ${e.role} @ ${e.company} (${e.period})`).join('\n'));
    else if (c==='status' || c==='ping') { out('checking deployed products …'); openApp('systems'); }
    else if (c==='neofetch' || c==='screenfetch') out([
      "        .--.        devarv@atharv-os",
      "       |o_o |       ----------------",
      "       |:_/ |       OS:     ATHARV.OS 1.0 (2026)",
      "      //   \\ \\      Shell:  bash (in-browser)",
      "     (|     | )     Role:   Backend & AI Engineer",
      "    /'\\_   _/`\\     Stack:  Python · FastAPI · LangGraph",
      "    \\___)=(___/     Uptime: since 1995 🏔",
    ].join('\n'));
    else if (c==='pwd') out('/home/devarv');
    else if (verb==='echo') out(cmd.slice(5));
    else if (c==='date') out(new Date().toString());
    else if (c==='uname -a') out('ATHARV.OS 1.0 (build 2026.04) #1 SMP devarv-kernel x86_64 GNU/Mountain');
    else if (c==='history') out(cmdHist.map((h,i)=>`  ${i+1}  ${h}`).join('\n') || '  (empty)');
    else if (c==='sudo' || c==='sudo su') out('[sudo] devarv is not in the sudoers file. This incident will be reported. 😏 (try `make-coffee`)');
    else if (c==='make-coffee') out('☕ brewing… HTTP 418: I\'m a teapot. The mug is in another castle.');
    else if (c==='hire') out('contact:\n  email:    atharv5873@gmail.com\n  linkedin: linkedin.com/in/atharv-sharma-devarv\n  → run `open hiring` for the full pitch');
    else if (c==='matrix') { out('wake up, neo…'); window.dispatchEvent(new CustomEvent('atharv:matrix')); }
    else if (c==='konami') { out('↑↑↓↓←→←→BA — fireworks engaged.'); window.dispatchEvent(new CustomEvent('atharv:konami')); }
    else if (c==='clear' || c==='cls') window.dispatchEvent(new CustomEvent('atharv:clearterm'));
    else if (c==='exit' || c==='quit' || c==='logout') out('(close the window to exit, traveller)');
    else err(`bash: ${cmd.split(/\s+/)[0]}: command not found — type \`help\``);
    setVal('');
  };

  const handleSubmit = (e) => { e.preventDefault(); run(val); };
  const onKey = (e) => {
    if (e.key === 'ArrowUp') { e.preventDefault(); if (!cmdHist.length) return; const i = histIdx < 0 ? cmdHist.length-1 : Math.max(0, histIdx-1); setHistIdx(i); setVal(cmdHist[i]); }
    else if (e.key === 'ArrowDown') { e.preventDefault(); if (histIdx < 0) return; const i = histIdx+1; if (i >= cmdHist.length) { setHistIdx(-1); setVal(''); } else { setHistIdx(i); setVal(cmdHist[i]); } }
  };

  return (
    <div className="terminal" onClick={()=>inputRef.current?.focus()}>
      <div>ATHARV.OS [Version 1.0.2026]</div>
      <div>(c) 2026 devarv. All rights reserved. — type <b>help</b></div>
      <div style={{height:8}}/>
      {lineHistory.map((l, i) => {
        if (l.type==='cmd') return <div key={i}><span className="prompt">{sudoMode?'root':'devarv'}@atharv-os:~$</span> {l.text}</div>;
        if (l.type==='err') return <div key={i} style={{color:'#ff7777', whiteSpace:'pre-wrap'}}>{l.text}</div>;
        return <div key={i} style={{whiteSpace:'pre-wrap'}}>{l.text}</div>;
      })}
      <form onSubmit={handleSubmit}>
        <span className="prompt">{sudoMode?'root':'devarv'}@atharv-os:~$&nbsp;</span>
        <input ref={inputRef} value={val} onChange={e=>setVal(e.target.value)} onKeyDown={onKey} autoFocus/>
      </form>
      <div ref={bottomRef}/>
    </div>
  );
}

/* ===== Tip of the Day ===== */
function TipOfTheDay({ onClose }) {
  const tips = [
    "Drag any window — it lifts off the 3D plane in real time.",
    "Press Win+R to open the Run dialog. Try typing 'matrix'.",
    "Every project icon is real and shipped to production. Double-click to inspect.",
    "The mountains in Photos are not stock photos. They are placeholders for places I have actually been.",
    "Don't see a Tweaks panel? Toggle it from the toolbar — it's hiding above this OS.",
    "Older Hindi songs and `tail -f` on a production log. The two best soundtracks I know."
  ];
  const tip = tips[Math.floor(Date.now()/86400000) % tips.length];
  return (
    <div style={{display:'flex', flexDirection:'column', flex:1, minHeight:0}}>
      <div className="tip-of-the-day">
        <div className="icon">💡</div>
        <div className="text">
          <h3>Did you know...</h3>
          <p>{tip}</p>
        </div>
      </div>
      <div style={{padding:8, borderTop:'1px solid #fff', display:'flex', justifyContent:'flex-end', background:'#C0C0C0'}}>
        <button className="btn" onClick={onClose}>Close</button>
      </div>
    </div>
  );
}

Object.assign(window, {
  MediaPlayerContent, MyComputerContent, ReadmeContent,
  RecycleBinContent, TerminalContent, TipOfTheDay
});
