// PRE Web GUI — llama.cpp server lifecycle manager
// Manages starting, stopping, and health-checking llama-server.

const { spawn, execSync } = require('child_process');
const http = require('http');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { IS_WIN, IS_MAC, IS_LINUX } = require('./platform');
const { resolveModelPath } = require('./llamacpp-model');

const PRE_DIR = path.join(os.homedir(), '.pre');
const LOG_FILE = path.join(PRE_DIR, 'llamacpp.log');
const MAX_LOG_SIZE = 1024 * 1024; // 1MB

let _process = null;
let _startTime = null;
let _config = null;

/**
 * Find the llama-server binary on this platform.
 * Checks PATH, then common installation locations.
 * @returns {string|null}
 */
function getBinaryPath() {
  // Check PATH first
  try {
    const cmd = IS_WIN ? 'where llama-server.exe' : 'which llama-server';
    const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000 }).trim();
    if (result) return result.split('\n')[0].trim();
  } catch {}

  // Common locations
  const candidates = IS_WIN
    ? [
        path.join(process.env.LOCALAPPDATA || '', 'Programs', 'llama.cpp', 'llama-server.exe'),
        path.join(process.env.PROGRAMFILES || '', 'llama.cpp', 'llama-server.exe'),
      ]
    : IS_MAC
    ? [
        '/usr/local/bin/llama-server',
        '/opt/homebrew/bin/llama-server',
        path.join(os.homedir(), '.local', 'bin', 'llama-server'),
      ]
    : [
        '/usr/local/bin/llama-server',
        path.join(os.homedir(), '.local', 'bin', 'llama-server'),
        '/usr/bin/llama-server',
      ];

  for (const p of candidates) {
    try { if (fs.existsSync(p)) return p; } catch {}
  }

  return null;
}

/**
 * Build command-line arguments for llama-server from config.
 * @param {Object} config
 * @returns {string[]}
 */
function _buildArgs(config) {
  const modelPath = resolveModelPath(config.model_path);
  if (!modelPath) throw new Error(`Model not found: ${config.model_path}`);

  const contextFile = path.join(PRE_DIR, 'context');
  let ctxSize = config.context_size || 131072;
  try { ctxSize = parseInt(fs.readFileSync(contextFile, 'utf-8').trim(), 10) || ctxSize; } catch {}

  const args = [
    '--model', modelPath,
    '--port', String(config.port || 8080),
    '--ctx-size', String(ctxSize),
    '--n-gpu-layers', String(config.n_gpu_layers ?? -1),
    '--threads', String(config.threads || Math.max(1, Math.floor(os.cpus().length / 2))),
    '--parallel', String(config.parallel || 1),
  ];

  // Flash Attention: llama.cpp b9110+ defaults to 'auto' which enables FA when
  // the backend supports it (CUDA, Metal on supported chips). Only disable if
  // explicitly set to false in config.
  if (config.flash_attn === false) args.push('--flash-attn', 'off');
  if (config.cache_type_k) args.push('--cache-type-k', config.cache_type_k);
  if (config.cache_type_v) args.push('--cache-type-v', config.cache_type_v);

  // Speculative decoding: draft model
  if (config.draft_model_path) {
    const draftPath = resolveModelPath(config.draft_model_path);
    if (draftPath) {
      args.push('--spec-draft-model', draftPath);
      args.push('--spec-draft-n-max', String(config.draft_max || 16));
    }
  }

  // KV cache persistence slot
  if (config.slot_save_path) {
    args.push('--slot-save-path', config.slot_save_path);
  }

  return args;
}

/**
 * Start the llama-server process.
 * @param {Object} config - Server configuration
 * @returns {Promise<{pid: number, port: number}>}
 */
async function start(config) {
  if (_process) {
    throw new Error('llama-server is already running (pid ' + _process.pid + ')');
  }

  const binary = config.binary_path || getBinaryPath();
  if (!binary) {
    throw new Error('llama-server not found. Install via: brew install llama.cpp (macOS) or download from github.com/ggml-org/llama.cpp/releases');
  }

  const args = _buildArgs(config);
  const port = config.port || 8080;

  console.log(`[llamacpp] Starting: ${binary} ${args.join(' ')}`);

  // Rotate log if too large
  try {
    const stat = fs.statSync(LOG_FILE);
    if (stat.size > MAX_LOG_SIZE) fs.unlinkSync(LOG_FILE);
  } catch {}

  const logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });

  _process = spawn(binary, args, {
    stdio: ['ignore', 'pipe', 'pipe'],
    detached: false,
  });

  _process.stdout.pipe(logStream);
  _process.stderr.pipe(logStream);

  _process.on('exit', (code, signal) => {
    console.log(`[llamacpp] Process exited: code=${code} signal=${signal}`);
    _process = null;
    _startTime = null;
    logStream.end();
  });

  _process.on('error', (err) => {
    console.log(`[llamacpp] Process error: ${err.message}`);
    _process = null;
    _startTime = null;
    logStream.end();
  });

  _config = config;
  _startTime = Date.now();

  // Wait for server to become healthy (model loading takes 30-120s)
  const timeout = config.startup_timeout || 180000; // 3 minutes
  const start = Date.now();
  while (Date.now() - start < timeout) {
    if (!_process) throw new Error('llama-server process died during startup');
    const healthy = await _healthPing(port);
    if (healthy) {
      console.log(`[llamacpp] Server ready on port ${port} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
      return { pid: _process.pid, port };
    }
    await new Promise(r => setTimeout(r, 500));
  }

  // Timeout — kill and report
  stop();
  throw new Error(`llama-server failed to start within ${timeout / 1000}s. Check ${LOG_FILE}`);
}

/**
 * Stop the llama-server process.
 */
function stop() {
  if (!_process) return;
  console.log(`[llamacpp] Stopping server (pid ${_process.pid})`);
  try {
    if (IS_WIN) {
      execSync(`taskkill /F /PID ${_process.pid}`, { timeout: 5000 });
    } else {
      _process.kill('SIGTERM');
    }
  } catch {}
  _process = null;
  _startTime = null;
  _config = null;
}

/**
 * Restart with new or existing config.
 * @param {Object} [config] - New config, or reuse existing
 */
async function restart(config) {
  stop();
  await new Promise(r => setTimeout(r, 1000));
  return start(config || _config);
}

/**
 * Ping the health endpoint.
 * @param {number} port
 * @returns {Promise<boolean>}
 */
function _healthPing(port) {
  return new Promise((resolve) => {
    const req = http.request({
      hostname: '127.0.0.1',
      port,
      path: '/health',
      method: 'GET',
      timeout: 2000,
    }, (res) => {
      let data = '';
      res.on('data', chunk => { data += chunk; });
      res.on('end', () => {
        try {
          const json = JSON.parse(data);
          resolve(json.status === 'ok');
        } catch {
          resolve(false);
        }
      });
    });
    req.on('error', () => resolve(false));
    req.on('timeout', () => { req.destroy(); resolve(false); });
    req.end();
  });
}

/**
 * Check if the server is running and healthy.
 * @returns {Promise<boolean>}
 */
async function isRunning() {
  if (!_process) return false;
  const port = _config?.port || 8080;
  return _healthPing(port);
}

/**
 * Get server status.
 * @returns {Promise<Object>}
 */
async function getStatus() {
  const running = await isRunning();
  return {
    running,
    pid: _process?.pid || null,
    port: _config?.port || 8080,
    model: _config?.model_path || null,
    draftModel: _config?.draft_model_path || null,
    uptime: _startTime ? Math.round((Date.now() - _startTime) / 1000) : 0,
    binaryPath: _config?.binary_path || getBinaryPath(),
  };
}

/**
 * Graceful shutdown — call on server exit.
 */
function shutdown() {
  stop();
}

module.exports = { start, stop, restart, isRunning, getStatus, getBinaryPath, shutdown };
