/**
 * Navigator Code — Web Server
 *
 * Запускает HTTP-сервер со статикой (фронтенд) и WebSocket,
 * который проксирует сообщения к CLI-процессу в режиме --acp.
 *
 * Архитектура:
 *   Browser (WebSocket) ←→ This Server ←→ CLI child process (stdin/stdout NDJSON)
 */

import { createServer } from 'node:http';
import { spawn, execSync, spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import { existsSync, createReadStream, readdirSync, copyFileSync, statSync, rmSync, cpSync } from 'node:fs';
import { extname, basename } from 'node:path';
import { homedir } from 'node:os';
import { WebSocketServer } from 'ws';

const __dirname = dirname(fileURLToPath(import.meta.url));
// В dev: packages/webapp/src/ → ../public
// В dist (бандл): app/webapp/ → public (рядом)
const PUBLIC_DIR_DIST = join(__dirname, 'public');
const PUBLIC_DIR_DEV = join(__dirname, '..', 'public');
const PUBLIC_DIR = existsSync(PUBLIC_DIR_DIST) ? PUBLIC_DIR_DIST : PUBLIC_DIR_DEV;

// Определяем путь к CLI
// В dev: packages/webapp/src/ → ../../.. → dist/cli.js
// В dist: app/webapp/ → .. → dist/cli.js
// Проверяем оба варианта
const CLI_PATH_DEV = resolve(__dirname, '..', '..', '..', 'dist', 'cli.js');
const CLI_PATH_DIST = resolve(__dirname, '..', 'dist', 'cli.js');
const CLI_PATH = existsSync(CLI_PATH_DIST) ? CLI_PATH_DIST : CLI_PATH_DEV;

const PORT = parseInt(process.env.PORT || '3100', 10);
const HOST = process.env.HOST || '0.0.0.0';

// ────────────────────────────────────────────
// MIME-типы для статики
// ────────────────────────────────────────────
const MIME = {
  '.html': 'text/html; charset=utf-8',
  '.js': 'application/javascript; charset=utf-8',
  '.css': 'text/css; charset=utf-8',
  '.json': 'application/json; charset=utf-8',
  '.png': 'image/png',
  '.svg': 'image/svg+xml',
  '.ico': 'image/x-icon',
};

// ────────────────────────────────────────────
// HTTP-сервер: отдаёт статику из public/
// ────────────────────────────────────────────
const httpServer = createServer((req, res) => {
  let filePath = req.url === '/' ? '/index.html' : req.url;
  // Убираем query string
  filePath = filePath.split('?')[0];
  // Корень после отрезания query (например /?path=...) -> index.html
  if (filePath === '/' || filePath === '') filePath = '/index.html';
  const fullPath = join(PUBLIC_DIR, filePath);

  // API: листинг каталогов для файлового браузера (ДО проверки public/)
  if (filePath.startsWith('/api/browse')) {
    const url = new URL(req.url, `http://${req.headers.host}`);
    let dir = url.searchParams.get('path') || homedir();
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    try {
      dir = resolve(dir);
      const entries = readdirSync(dir, { withFileTypes: true });
      const dirs = entries
        .filter(e => e.isDirectory() && !e.name.startsWith('.'))
        .map(e => ({ name: e.name, path: join(dir, e.name) }))
        .sort((a, b) => a.name.localeCompare(b.name));
      const files = entries
        .filter(e => e.isFile() && !e.name.startsWith('.'))
        .map(e => ({ name: e.name, path: join(dir, e.name) }))
        .sort((a, b) => a.name.localeCompare(b.name));
      res.writeHead(200);
      res.end(JSON.stringify({ current: dir, parent: dirname(dir), dirs, files }));
    } catch (err) {
      res.writeHead(200);
      res.end(JSON.stringify({ current: dir, parent: dirname(dir), dirs: [], error: err.message }));
    }
    return;
  }

  // API: создать копию файла/папки рядом с оригиналом.
  // Файл foo.txt → foo (копия).txt; если такая уже есть — (копия 2), (копия 3)...
  // Папка bar → bar (копия); рекурсивно копирует содержимое.
  if (filePath === '/api/copy') {
    const url = new URL(req.url, `http://${req.headers.host}`);
    const target = url.searchParams.get('path') || '';
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    if (!target) {
      res.writeHead(400);
      res.end(JSON.stringify({ error: 'path required' }));
      return;
    }
    try {
      const src = resolve(target);
      if (!existsSync(src)) {
        res.writeHead(404);
        res.end(JSON.stringify({ error: 'not found', path: src }));
        return;
      }
      const parent = dirname(src);
      const ext = extname(src);
      const base = basename(src, ext);
      // Подбираем имя: сначала "base (копия)<ext>", потом " 2", " 3" и т.д.
      let dst = join(parent, `${base} (копия)${ext}`);
      let n = 2;
      while (existsSync(dst)) {
        dst = join(parent, `${base} (копия ${n})${ext}`);
        n++;
        if (n > 999) throw new Error('too many copies'); // защита от бесконечного цикла
      }
      const stats = statSync(src);
      if (stats.isDirectory()) {
        // Рекурсивная копия директории — fs.cpSync доступен с Node 16.7+.
        // У нас Node >=18 (по engines), так что ок.
        cpSync(src, dst, { recursive: true });
      } else {
        copyFileSync(src, dst);
      }
      res.writeHead(200);
      res.end(JSON.stringify({ ok: true, from: src, to: dst }));
    } catch (err) {
      res.writeHead(500);
      res.end(JSON.stringify({ error: err.message }));
    }
    return;
  }

  // API: удалить файл или папку. Для папок — рекурсивно.
  // НЕ поддерживает trash/recycle bin — удаление окончательное.
  // UI-сторона обязана спрашивать подтверждение перед вызовом.
  if (filePath === '/api/delete') {
    const url = new URL(req.url, `http://${req.headers.host}`);
    const target = url.searchParams.get('path') || '';
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    if (!target) {
      res.writeHead(400);
      res.end(JSON.stringify({ error: 'path required' }));
      return;
    }
    try {
      const p = resolve(target);
      if (!existsSync(p)) {
        res.writeHead(404);
        res.end(JSON.stringify({ error: 'not found', path: p }));
        return;
      }
      // Дополнительная защита: не даём удалить корень диска / HOME.
      const forbidden = new Set([
        resolve('/'),
        resolve(homedir()),
        process.platform === 'win32' ? 'C:\\' : null,
      ].filter(Boolean));
      if (forbidden.has(p)) {
        res.writeHead(403);
        res.end(JSON.stringify({ error: 'refusing to delete system root', path: p }));
        return;
      }
      rmSync(p, { recursive: true, force: true });
      res.writeHead(200);
      res.end(JSON.stringify({ ok: true, path: p }));
    } catch (err) {
      res.writeHead(500);
      res.end(JSON.stringify({ error: err.message }));
    }
    return;
  }

  // API: открыть файл в OS-дефолтном приложении. Работает как двойной
  // клик в проводнике: Windows `start ""`, macOS `open`, Linux `xdg-open`.
  // Безопасности ради путь не проверяем на containment — пользователь сам
  // через UI выбирает файлы из своего каталога, не из внешних источников.
  if (filePath === '/api/open') {
    const url = new URL(req.url, `http://${req.headers.host}`);
    const target = url.searchParams.get('path') || '';
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    if (!target) {
      res.writeHead(400);
      res.end(JSON.stringify({ error: 'path required' }));
      return;
    }
    try {
      const resolved = resolve(target);
      if (!existsSync(resolved)) {
        res.writeHead(404);
        res.end(JSON.stringify({ error: 'not found', path: resolved }));
        return;
      }
      if (process.platform === 'win32') {
        // `start ""` с пустой «title» — чтобы start корректно принял
        // путь с кавычками как аргумент. detached чтобы не наследовать
        // время жизни от процесса сервера.
        const c = spawn('cmd.exe', ['/c', 'start', '""', resolved], {
          detached: true,
          stdio: 'ignore',
          windowsHide: true,
        });
        c.unref();
      } else if (process.platform === 'darwin') {
        const c = spawn('open', [resolved], { detached: true, stdio: 'ignore' });
        c.unref();
      } else {
        const c = spawn('xdg-open', [resolved], { detached: true, stdio: 'ignore' });
        c.unref();
      }
      res.writeHead(200);
      res.end(JSON.stringify({ ok: true, path: resolved }));
    } catch (err) {
      res.writeHead(500);
      res.end(JSON.stringify({ error: err.message }));
    }
    return;
  }

  // API: список дисков (Windows)
  if (filePath === '/api/drives') {
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    if (process.platform === 'win32') {
      try {
        // fsutil — мгновенное получение дисков на Windows (не зависает)
        const out = spawnSync('cmd.exe', ['/c', 'fsutil fsinfo drives'], {
          encoding: 'utf-8', timeout: 3000,
        });
        // Вывод: "Drives: C:\ D:\ "
        const match = (out.stdout || '').match(/[A-Z]:\\/g);
        const drives = match || ['C:\\'];
        res.writeHead(200);
        res.end(JSON.stringify({ drives }));
      } catch {
        res.writeHead(200);
        res.end(JSON.stringify({ drives: ['C:\\'] }));
      }
    } else {
      res.writeHead(200);
      res.end(JSON.stringify({ drives: ['/'] }));
    }
    return;
  }

  // API: открыть системный диалог выбора папки (только Windows)
  if (filePath === '/api/select-folder') {
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    try {
      if (process.platform === 'win32') {
        // PowerShell COM-объект Shell.Application для открытия системного диалога
        // Используем Base64 чтобы избежать проблем с кавычками и кириллицей
        const psScript = `$shell = New-Object -ComObject Shell.Application; $folder = $shell.BrowseForFolder(0, 'Choose working folder', 0x0001); if ($folder -ne $null) { $folder.Self.Path } else { '' }`;
        const encoded = Buffer.from(psScript, 'utf16le').toString('base64');
        const { stdout, stderr, status } = spawnSync('powershell.exe', ['-NoProfile', '-EncodedCommand', encoded], {
          encoding: 'utf-8',
          timeout: 60000,
        });
        if (status !== 0) throw new Error(stderr || `PowerShell exited with code ${status}`);
        const selectedPath = (stdout || '').trim();

        if (selectedPath && existsSync(selectedPath)) {
          res.writeHead(200);
          res.end(JSON.stringify({ path: selectedPath }));
        } else if (selectedPath === '') {
          // Пользователь отменил выбор
          res.writeHead(200);
          res.end(JSON.stringify({ path: null }));
        } else {
          res.writeHead(400);
          res.end(JSON.stringify({ error: 'Выбранный путь не существует' }));
        }
      } else {
        // На Linux/macOS можно использовать zenity/kdialog/macOS AppleScript
        res.writeHead(501);
        res.end(JSON.stringify({ error: 'Системный диалог пока поддерживается только на Windows' }));
      }
    } catch (err) {
      res.writeHead(500);
      res.end(JSON.stringify({ error: err.message }));
    }
    return;
  }

  // Безопасность — не выходить за пределы public/
  if (!fullPath.startsWith(PUBLIC_DIR)) {
    res.writeHead(403);
    res.end('Forbidden');
    return;
  }

  if (!existsSync(fullPath)) {
    // SPA fallback — отдаём index.html
    const indexPath = join(PUBLIC_DIR, 'index.html');
    if (existsSync(indexPath)) {
      // no-cache для HTML: после апдейта webapp (новый index.html в dist)
      // браузер иначе может отдавать старую версию из своего кеша неделями,
      // и пользователь сидит на старом JS, думая что фикс не сработал.
      res.writeHead(200, {
        'Content-Type': 'text/html; charset=utf-8',
        'Cache-Control': 'no-cache, no-store, must-revalidate',
        Pragma: 'no-cache',
        Expires: '0',
      });
      createReadStream(indexPath).pipe(res);
    } else {
      res.writeHead(404);
      res.end('Not Found');
    }
    return;
  }

  const ext = filePath.substring(filePath.lastIndexOf('.'));
  const contentType = MIME[ext] || 'application/octet-stream';
  // HTML всегда раздаём без кеша (см. комментарий выше про index.html).
  // Для остальных ресурсов — короткий кеш (1 час), чтобы не гонять лишних
  // запросов, но при обновлении не было многонедельных stale-копий.
  const headers = { 'Content-Type': contentType };
  if (ext === '.html') {
    headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
    headers['Pragma'] = 'no-cache';
    headers['Expires'] = '0';
  } else {
    headers['Cache-Control'] = 'public, max-age=3600';
  }
  res.writeHead(200, headers);
  createReadStream(fullPath).pipe(res);
});

// ────────────────────────────────────────────
// WebSocket-сервер: мост между браузером и CLI
// ────────────────────────────────────────────
const wss = new WebSocketServer({ server: httpServer, path: '/ws' });

// Heartbeat: пингуем клиентов, чтобы детектировать мёртвые соединения
// (idle-таймауты proxy/NAT/tcp-стека молча рвут WS при длинных командах).
const HEARTBEAT_INTERVAL_MS = 20000;
const heartbeatTimer = setInterval(() => {
  for (const ws of wss.clients) {
    if (ws.isAlive === false) {
      console.log('[webapp] WS без pong — терминируем');
      try { ws.terminate(); } catch {}
      continue;
    }
    ws.isAlive = false;
    try { ws.ping(); } catch {}
    // Application-level ping — JSON-RPC нотификация, которую
    // клиент видит в onmessage и сбрасывает watchdog. Нужно потому,
    // что во время долгих shell-команд агент молчит 30–120+ сек,
    // и браузерный watchdog ложно считает WS мёртвым.
    // (browser WebSocket API не даёт доступа к TCP pong-фреймам.)
    if (ws.readyState === ws.OPEN) {
      try { ws.send('{"jsonrpc":"2.0","method":"navigator/ping"}'); } catch {}
    }
  }
}, HEARTBEAT_INTERVAL_MS);
wss.on('close', () => clearInterval(heartbeatTimer));

wss.on('connection', (ws, req) => {
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });

  // Определяем рабочую директорию (из query или cwd)
  const url = new URL(req.url, `http://${req.headers.host}`);
  // Если cwd не передан — используем домашнюю папку (не каталог сервера)
  const cwd = url.searchParams.get('cwd') || homedir();

  const resolvedCwd = resolve(cwd);
  console.log(`[webapp] Новое подключение, cwd: ${resolvedCwd}`);

  // Отправляем клиенту информацию о сервере
  ws.send(JSON.stringify({
    jsonrpc: '2.0',
    method: 'server/info',
    params: { cwd: resolvedCwd },
  }));

  // Запускаем CLI-процесс в режиме ACP
  const child = spawn(process.execPath, [CLI_PATH, '--acp'], {
    cwd: resolvedCwd,
    stdio: ['pipe', 'pipe', 'pipe'],
    env: {
      ...process.env,
      // Не наследуем интерактивные настройки
      TERM: 'dumb',
    },
  });

  let alive = true;

  // CLI stdout → WebSocket (NDJSON → отдельные JSON-сообщения)
  let buffer = '';
  child.stdout.on('data', (chunk) => {
    buffer += chunk.toString();
    const lines = buffer.split('\n');
    // Последний элемент — неполная строка, сохраняем
    buffer = lines.pop() || '';
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed && ws.readyState === ws.OPEN) {
        ws.send(trimmed);
      }
    }
  });

  // CLI stderr → лог в консоль сервера
  child.stderr.on('data', (chunk) => {
    const text = chunk.toString().trim();
    if (text) {
      console.error(`[cli stderr] ${text}`);
    }
  });

  // CLI завершился
  child.on('close', (code) => {
    console.log(`[webapp] CLI процесс завершён, код: ${code}`);
    alive = false;
    if (ws.readyState === ws.OPEN) {
      ws.close(1000, 'CLI process exited');
    }
  });

  child.on('error', (err) => {
    console.error(`[webapp] Ошибка CLI процесса:`, err.message);
    alive = false;
    if (ws.readyState === ws.OPEN) {
      ws.send(JSON.stringify({
        jsonrpc: '2.0',
        method: 'error',
        params: { message: `CLI process error: ${err.message}` },
      }));
      ws.close(1011, 'CLI process error');
    }
  });

  // WebSocket → CLI stdin (NDJSON)
  ws.on('message', (data) => {
    if (!alive) return;
    const msg = data.toString();
    try {
      // Проверяем что это валидный JSON
      JSON.parse(msg);
      child.stdin.write(msg + '\n');
    } catch {
      console.error('[webapp] Невалидный JSON от клиента:', msg.substring(0, 200));
    }
  });

  // Клиент отключился — убиваем CLI-процесс
  ws.on('close', () => {
    console.log('[webapp] Клиент отключился');
    if (alive) {
      child.kill('SIGTERM');
      // Даём 3 секунды на graceful shutdown, потом SIGKILL
      setTimeout(() => {
        if (alive) {
          child.kill('SIGKILL');
        }
      }, 3000);
    }
  });
});

// ────────────────────────────────────────────
// Запуск (с автоматическим освобождением порта)
// ────────────────────────────────────────────
function startServer() {
  httpServer.listen(PORT, HOST, () => {
    console.log('');
    console.log('  ╔══════════════════════════════════════╗');
    console.log('  ║   Navigator Code — Web Interface  ║');
    console.log('  ╠══════════════════════════════════════╣');
    console.log(`  ║   http://localhost:${PORT}              ║`);
    console.log('  ╚══════════════════════════════════════╝');
    console.log('');
    console.log(`  CLI: ${CLI_PATH}`);
    console.log(`  CWD: ${process.cwd()}`);
    console.log('');
  });

  httpServer.on('error', (err) => {
    if (err.code === 'EADDRINUSE') {
      console.log(`  Порт ${PORT} занят — освобождаю...`);
      // На Windows: убить процесс на порту
      try {
        if (process.platform === 'win32') {
          const result = execSync(`netstat -ano | findstr :${PORT} | findstr LISTENING`, { encoding: 'utf-8' });
          const lines = result.trim().split('\n');
          for (const line of lines) {
            const pid = line.trim().split(/\s+/).pop();
            if (pid && pid !== '0') {
              try { execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' }); } catch {}
            }
          }
        } else {
          execSync(`fuser -k ${PORT}/tcp 2>/dev/null || true`, { stdio: 'ignore' });
        }
        console.log('  Порт освобождён. Перезапуск...');
        setTimeout(() => httpServer.listen(PORT, HOST), 1500);
      } catch {
        console.error(`  Не удалось освободить порт ${PORT}. Закройте процесс вручную.`);
        process.exit(1);
      }
    } else {
      throw err;
    }
  });
}

startServer();
