// ─── Files screen ─── Real file ops against /api/instance/files*
function FilesScreen({ setScreen, toast }) {
  const [path, setPath] = React.useState('/');
  const [entries, setEntries] = React.useState([]);
  const [loading, setLoading] = React.useState(false);
  const [selected, setSelected] = React.useState(null);
  const [uploadLoading, setUploadLoading] = React.useState(false);
  const [maxMB, setMaxMB] = React.useState(20);
  const fileInput = React.useRef(null);

  React.useEffect(() => {
    apiFetch('/api/auth/config').then(r => r.json()).then(d => {
      const n = Number(d.file_upload_max_mb);
      if (n > 0) setMaxMB(n);
    }).catch(() => {});
  }, []);

  async function load(p) {
    setLoading(true);
    try {
      const data = await apiRequest(`/api/instance/files?path=${encodeURIComponent(p)}`);
      setPath(data.path || '/');
      const sorted = (data.entries || []).slice().sort((a, b) => {
        if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
        return a.name.localeCompare(b.name);
      });
      setEntries(sorted);
      setSelected(null);
    } catch (ex) {
      toast(errText(ex.code || ex.message), 'error');
      setEntries([]);
    } finally {
      setLoading(false);
    }
  }

  React.useEffect(() => { load('/'); }, []); // eslint-disable-line

  function entryPath(e) {
    const base = path === '/' ? '' : path;
    return `${base}/${e.name}`;
  }

  function clickEntry(e) {
    setSelected(e.name);
    if (e.is_dir) load(entryPath(e));
  }

  function navigateUp() {
    const parts = path.split('/').filter(Boolean);
    parts.pop();
    load('/' + parts.join('/'));
  }

  function crumbs() {
    const parts = path.split('/').filter(Boolean);
    const out = [{ name: 'workspace', p: '/' }];
    let acc = '';
    for (const pt of parts) { acc += '/' + pt; out.push({ name: pt, p: acc }); }
    return out;
  }

  function download(e) {
    const url = `/api/instance/files/download?path=${encodeURIComponent(entryPath(e))}`;
    const a = document.createElement('a');
    a.href = url;
    a.download = e.name;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  }

  async function remove(e) {
    const msg = e.is_dir ? `确定删除目录 ${e.name} 及其所有内容吗？` : `确定删除 ${e.name} 吗？`;
    if (!confirm(msg)) return;
    try {
      const res = await apiFetch(`/api/instance/files?path=${encodeURIComponent(entryPath(e))}`, { method: 'DELETE' });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        throw new Error(d.error || 'err_file_op_failed');
      }
      toast('已删除');
      load(path);
    } catch (ex) {
      toast(errText(ex.message), 'error');
    }
  }

  async function upload(ev) {
    const file = ev.target.files?.[0];
    if (!file) return;
    ev.target.value = '';
    if (file.size > maxMB * 1024 * 1024) {
      toast(`文件超过 ${maxMB}MB 限制`, 'error');
      return;
    }
    setUploadLoading(true);
    try {
      const fd = new FormData();
      fd.append('file', file);
      const res = await apiFetch(`/api/instance/files/upload?path=${encodeURIComponent(path)}`, {
        method: 'POST',
        body: fd,
        headers: { 'Content-Type': null },
      });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        throw new Error(d.error || 'err_upload_failed');
      }
      toast('上传成功');
      load(path);
    } catch (ex) {
      toast(errText(ex.message), 'error');
    } finally {
      setUploadLoading(false);
    }
  }

  async function mkdir() {
    const raw = prompt('输入新目录名称');
    if (raw === null) return;
    const name = raw.trim();
    if (!name || name.includes('/')) {
      toast('目录名不能为空，且不能包含 /', 'error');
      return;
    }
    try {
      const res = await apiFetch(`/api/instance/files/dirs?path=${encodeURIComponent(path)}`, {
        method: 'POST',
        body: JSON.stringify({ name }),
      });
      if (!res.ok) {
        const d = await res.json().catch(() => ({}));
        throw new Error(d.error || 'err_file_op_failed');
      }
      toast('目录已创建');
      load(path);
    } catch (ex) {
      toast(errText(ex.message), 'error');
    }
  }

  const usedMB = entries.filter(e => !e.is_dir).reduce((s, e) => s + (e.size || 0), 0) / (1024 * 1024);

  return (
    <div style={{ padding: '24px', maxWidth: 1200, margin: '0 auto' }}>
      <button className="btn btn--sm btn--ghost mb-3" onClick={() => setScreen('dashboard')}>← 返回 Dashboard</button>
      <div className="win">
        <div className="win__bar">
          <div className="dots"><i/><i/><i/></div>
          <div className="title">file_browser</div>
          <div className="meta">/workspace · 本目录 {usedMB.toFixed(1)}M · 上传上限 {maxMB}M</div>
        </div>
        <div className="win__body" style={{ padding: 16 }}>
          <div className="row mb-3" style={{ gap: 8 }}>
            <div className="breadcrumb flex-1">
              ~ {crumbs().map((c, i) => (
                <React.Fragment key={c.p}>
                  {i > 0 && <span className="sep"> / </span>}
                  <a onClick={(e) => { e.preventDefault(); load(c.p); }} style={{ cursor: 'pointer', color: i === crumbs().length - 1 ? 'inherit' : 'var(--amber)', textDecoration: 'none' }}>
                    {c.name}
                  </a>
                </React.Fragment>
              ))}
              <span className="caret" style={{ color: 'var(--amber)' }}/>
            </div>
            <input type="file" ref={fileInput} onChange={upload} style={{ display: 'none' }}/>
            <button className="btn btn--sm" onClick={() => fileInput.current?.click()} disabled={uploadLoading}>
              {uploadLoading ? '上传中...' : '↑ 上传'}
            </button>
            <button className="btn btn--sm" onClick={mkdir}>+ 新建</button>
            <button className="btn btn--sm btn--lime" onClick={() => load(path)}>⟳</button>
          </div>

          <div className="panel panel--flat" style={{ overflow: 'hidden' }}>
            <div className="fb">
              <div className="fb__row" style={{ background: 'var(--paper-2)', fontWeight: 600, fontFamily: 'var(--font-display)', fontSize: 10, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--ink-mute)', cursor: 'default' }}>
                <span></span>
                <span>名称</span>
                <span>类型</span>
                <span>大小</span>
                <span>修改时间</span>
              </div>
              {path !== '/' && (
                <div className="fb__row folder" onClick={navigateUp}>
                  <span className="icon"/>
                  <span>..</span>
                  <span className="muted small">上级目录</span>
                  <span></span>
                  <span></span>
                </div>
              )}
              {loading && (
                <div className="fb__row" style={{ cursor: 'default', color: 'var(--ink-mute)' }}>
                  <span></span>
                  <span>加载中...</span>
                  <span></span>
                  <span></span>
                  <span></span>
                </div>
              )}
              {!loading && entries.length === 0 && (
                <div className="fb__row" style={{ cursor: 'default', color: 'var(--ink-mute)' }}>
                  <span></span>
                  <span>{'// 空目录'}</span>
                  <span></span>
                  <span></span>
                  <span></span>
                </div>
              )}
              {!loading && entries.map(e => (
                <div
                  key={e.name}
                  className={'fb__row ' + (e.is_dir ? 'folder' : 'file') + (selected === e.name ? ' selected' : '')}
                  onClick={() => clickEntry(e)}
                  onDoubleClick={() => !e.is_dir && download(e)}
                >
                  <span className="icon"/>
                  <span>{e.name}</span>
                  <span className="muted small">{e.is_dir ? '目录' : kindFromName(e.name)}</span>
                  <span className="muted small">{e.is_dir ? '—' : formatSize(e.size)}</span>
                  <span className="muted small">{relTime(e.mod_time)}
                    {!e.is_dir && (
                      <span className="row" style={{ gap: 4, marginLeft: 8, display: 'inline-flex' }} onClick={ev => ev.stopPropagation()}>
                        <button className="btn btn--sm btn--ghost" onClick={() => download(e)} style={{ padding: '2px 6px', fontSize: 9 }}>↓</button>
                        <button className="btn btn--sm btn--ghost" onClick={() => remove(e)} style={{ padding: '2px 6px', fontSize: 9 }}>🗑</button>
                      </span>
                    )}
                    {e.is_dir && (
                      <span onClick={ev => ev.stopPropagation()} style={{ marginLeft: 8, display: 'inline-flex' }}>
                        <button className="btn btn--sm btn--ghost" onClick={() => remove(e)} style={{ padding: '2px 6px', fontSize: 9 }}>🗑</button>
                      </span>
                    )}
                  </span>
                </div>
              ))}
            </div>
          </div>

          <div className="row mt-3 tiny mono muted" style={{ justifyContent: 'space-between' }}>
            <span>{entries.length} 项{selected ? ` · 已选 ${selected}` : ''}</span>
            <span>{'>'} 双击文件下载 · 上传上限 {maxMB} MB</span>
          </div>
        </div>
      </div>
    </div>
  );
}
window.FilesScreen = FilesScreen;

function kindFromName(name) {
  const i = name.lastIndexOf('.');
  if (i < 0) return '文件';
  const ext = name.slice(i + 1).toLowerCase();
  return ext.toUpperCase();
}
function formatSize(bytes) {
  if (!bytes) return '0';
  const units = ['B', 'KB', 'MB', 'GB'];
  let i = 0, s = bytes;
  while (s >= 1024 && i < units.length - 1) { s /= 1024; i++; }
  return s.toFixed(i > 0 ? 1 : 0) + ' ' + units[i];
}
function relTime(iso) {
  if (!iso) return '—';
  const dt = new Date(iso);
  if (Number.isNaN(dt.getTime())) return '—';
  const diff = Date.now() - dt.getTime();
  if (diff < 60_000) return '刚刚';
  if (diff < 3_600_000) return Math.floor(diff / 60_000) + 'm 前';
  if (diff < 86_400_000) return Math.floor(diff / 3_600_000) + 'h 前';
  if (diff < 7 * 86_400_000) return Math.floor(diff / 86_400_000) + 'd 前';
  const pad = n => String(n).padStart(2, '0');
  return `${pad(dt.getMonth()+1)}-${pad(dt.getDate())}`;
}

// ─── Terminal screen ─── xterm.js + binary WebSocket, mirrors web/hosting-ui-v4/src/components/Terminal.vue
// Protocol: [tag][payload]
//   0x00: bi-directional data (stdin client→server, stdout server→client)
//   0x01: resize (client→server) — [0x01, cols_hi, cols_lo, rows_hi, rows_lo] big-endian
//   0x7e (~): info message (server→client)
//   0x7f (DEL): error/exit (server→client), body is "code:message"
const TERM_ERROR_MESSAGES = {
  err_instance_not_found: '请先创建云端电脑',
  err_instance_not_ready: '实例未运行，请先启动',
  err_container_failed: '终端启动失败，请重试',
  session_replaced: '已在其他位置打开此终端',
  idle_timeout: '空闲超时，终端已关闭',
  container_stopped: '容器已停止',
};

function TermScreen({ setScreen }) {
  const hostRef = React.useRef(null);
  const termRef = React.useRef(null);
  const wsRef = React.useRef(null);
  const fitRef = React.useRef(null);
  const [status, setStatus] = React.useState({ text: '', kind: '' });

  function buildWsURL(cols, rows) {
    const u = new URL('/api/instance/terminal', window.location.origin);
    u.protocol = u.protocol === 'https:' ? 'wss:' : 'ws:';
    u.searchParams.set('cols', String(cols));
    u.searchParams.set('rows', String(rows));
    return u.toString();
  }

  function sendResize(cols, rows) {
    const ws = wsRef.current;
    if (!ws || ws.readyState !== WebSocket.OPEN) return;
    const frame = new Uint8Array(5);
    frame[0] = 0x01;
    const dv = new DataView(frame.buffer);
    dv.setUint16(1, cols, false);
    dv.setUint16(3, rows, false);
    ws.send(frame);
  }

  function connect() {
    if (!window.Terminal || !window.FitAddon) {
      setStatus({ text: 'xterm 未加载', kind: 'error' });
      return;
    }
    setStatus({ text: '', kind: '' });

    const term = new window.Terminal({
      cursorBlink: true,
      fontFamily: '"JetBrains Mono", ui-monospace, Menlo, monospace',
      fontSize: 13,
      convertEol: false,
      theme: { background: '#0e0e14', foreground: '#d8ffcd', cursor: '#ff5b7a' },
    });
    const FitCtor = window.FitAddon.FitAddon || window.FitAddon;
    const fit = new FitCtor();
    term.loadAddon(fit);
    term.open(hostRef.current);
    setTimeout(() => { try { fit.fit(); } catch {} }, 0);
    termRef.current = term;
    fitRef.current = fit;

    const ws = new WebSocket(buildWsURL(term.cols, term.rows));
    ws.binaryType = 'arraybuffer';
    wsRef.current = ws;

    ws.onopen = () => sendResize(term.cols, term.rows);
    ws.onmessage = (ev) => {
      const view = new Uint8Array(ev.data);
      if (view.length === 0) return;
      const tag = view[0];
      const payload = view.subarray(1);
      if (tag === 0x00) {
        term.write(payload);
      } else if (tag === 0x7e) {
        setStatus({ text: new TextDecoder().decode(payload), kind: 'info' });
      } else if (tag === 0x7f) {
        const body = new TextDecoder().decode(payload);
        const code = body.split(':', 1)[0];
        setStatus({ text: TERM_ERROR_MESSAGES[code] || body, kind: 'error' });
      }
    };
    ws.onclose = () => {
      setStatus(s => s.kind ? s : { text: '会话已结束', kind: 'error' });
    };
    ws.onerror = () => {
      setStatus(s => s.kind ? s : { text: '连接失败', kind: 'error' });
    };

    term.onData((data) => {
      const w = wsRef.current;
      if (!w || w.readyState !== WebSocket.OPEN) return;
      const bytes = new TextEncoder().encode(data);
      const frame = new Uint8Array(bytes.length + 1);
      frame[0] = 0x00;
      frame.set(bytes, 1);
      w.send(frame);
    });
  }

  function disconnect() {
    const ws = wsRef.current;
    if (ws) { try { ws.close(); } catch {} wsRef.current = null; }
    const term = termRef.current;
    if (term) { term.dispose(); termRef.current = null; }
    fitRef.current = null;
  }

  function reconnect() {
    disconnect();
    connect();
  }

  React.useEffect(() => {
    connect();
    let resizeTimer = null;
    const onResize = () => {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(() => {
        const fit = fitRef.current;
        const term = termRef.current;
        if (!fit || !term) return;
        try { fit.fit(); } catch {}
        sendResize(term.cols, term.rows);
      }, 150);
    };
    window.addEventListener('resize', onResize);
    return () => {
      window.removeEventListener('resize', onResize);
      clearTimeout(resizeTimer);
      disconnect();
    };
  }, []); // eslint-disable-line

  return (
    <div style={{ padding: '24px', maxWidth: 1200, margin: '0 auto' }}>
      <div className="row mb-3" style={{ gap: 8 }}>
        <button className="btn btn--sm btn--ghost" onClick={() => setScreen('dashboard')}>← 返回 Dashboard</button>
        {status.kind === 'error' && (
          <button className="btn btn--sm btn--primary" onClick={reconnect}>↻ 重连</button>
        )}
      </div>
      <div className="win">
        <div className="win__bar">
          <div className="dots"><i/><i/><i/></div>
          <div className="title">pty · /bin/bash</div>
          <div className="meta">{status.kind === 'error' ? 'disconnected' : 'connected'}</div>
        </div>
        <div className="win__body" style={{ padding: 0 }}>
          <div ref={hostRef} style={{
            background: '#0e0e14',
            padding: 12,
            minHeight: 460,
            position: 'relative',
          }}/>
          {status.text && (
            <div
              className="tiny mono"
              style={{
                padding: '8px 14px',
                borderTop: '1.5px solid var(--line)',
                background: status.kind === 'error' ? 'var(--shrimp-l)' : 'var(--paper-2)',
                color: status.kind === 'error' ? 'var(--shrimp-d)' : 'var(--amber-d)',
              }}
            >
              {status.kind === 'error' ? '⚠ ' : 'ⓘ '}{status.text}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}
window.TermScreen = TermScreen;
