// PRE Web GUI — Escalation router
//
// One place that knows how to route "use the bigger GPU" requests.
//   macOS:        tinygrad subprocess hosts a Llama/Qwen/GLM model on an eGPU.
//   Windows/Linux: a second Ollama model registered alongside pre-gemma4.
//
// Escalation is an explicit, per-message override. The user toggles it from the
// chat input or types /escalate to trigger one request through the alt runtime;
// the standard router (fast/standard/frontier) is bypassed when this fires.

const http = require('http');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { EventEmitter } = require('events');
const { IS_MAC } = require('./platform');
const { gpuDetector } = require('./gpu-detect');
const { OLLAMA_PORT } = require('./constants');

const CONNECTIONS_FILE = path.join(os.homedir(), '.pre', 'connections.json');

function _loadConnections() {
  try { return JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf-8')); }
  catch { return {}; }
}

const events = new EventEmitter();
let _lastStatus = null;
let _probeTimer = null;

/**
 * Read the escalation config from connections.json.
 * Returns null if escalation is not configured or disabled.
 *
 * Shape under `_provider.escalation`:
 *   {
 *     "enabled": true,
 *     "runtime": "tinygrad" | "ollama",  // optional; defaults by platform
 *     "label": "GLM-4-32B Q4_K_M",       // shown in UI
 *
 *     // tinygrad runtime (macOS):
 *     "model_path": "/path/to/model.gguf",
 *     "tokenizer_name": "...",
 *     "port": 8091,
 *     "base_url": "http://127.0.0.1:8091",
 *     "context_size": 8192,
 *
 *     // ollama runtime (Win/Linux):
 *     "model_name": "pre-escalate"
 *   }
 */
function getEscalationConfig() {
  const data = _loadConnections();
  const cfg = data?._provider?.escalation;
  if (!cfg || cfg.enabled === false) return null;

  const runtime = cfg.runtime || (IS_MAC ? 'tinygrad' : 'ollama');

  if (runtime === 'tinygrad') {
    if (!cfg.model_path) return null;
    const port = parseInt(cfg.port, 10) || 8091;
    return {
      runtime: 'tinygrad',
      model_path: cfg.model_path,
      tokenizer_name: cfg.tokenizer_name || 'auto',
      port,
      base_url: cfg.base_url || `http://127.0.0.1:${port}`,
      context_size: parseInt(cfg.context_size, 10) || 8192,
      label: cfg.label || path.basename(cfg.model_path),
    };
  }

  if (runtime === 'ollama') {
    if (!cfg.model_name) return null;
    return {
      runtime: 'ollama',
      model_name: cfg.model_name,
      label: cfg.label || cfg.model_name,
    };
  }

  return null;
}

function _probeTinygrad(baseUrl) {
  return new Promise((resolve) => {
    let u;
    try { u = new URL('/health', baseUrl); } catch { return resolve(null); }
    const req = http.request({
      hostname: u.hostname,
      port: u.port,
      path: u.pathname,
      method: 'GET',
      timeout: 2000,
    }, (res) => {
      let data = '';
      res.on('data', (c) => { data += c; });
      res.on('end', () => {
        if (res.statusCode !== 200) return resolve(null);
        try { resolve(JSON.parse(data)); } catch { resolve({ ok: true }); }
      });
    });
    req.on('error', () => resolve(null));
    req.on('timeout', () => { req.destroy(); resolve(null); });
    req.end();
  });
}

function _probeOllamaModel(modelName) {
  return new Promise((resolve) => {
    const req = http.request({
      hostname: '127.0.0.1',
      port: OLLAMA_PORT,
      path: '/api/tags',
      method: 'GET',
      timeout: 2000,
    }, (res) => {
      let data = '';
      res.on('data', (c) => { data += c; });
      res.on('end', () => {
        try {
          const json = JSON.parse(data);
          const models = json.models || [];
          // Match against both `name` and `model` fields and ignore ":latest" suffix
          const norm = (s) => String(s || '').replace(/:latest$/, '');
          const target = norm(modelName);
          const found = models.some((m) => norm(m.name) === target || norm(m.model) === target);
          resolve(found);
        } catch { resolve(false); }
      });
    });
    req.on('error', () => resolve(false));
    req.on('timeout', () => { req.destroy(); resolve(false); });
    req.end();
  });
}

/**
 * Compute the current escalation status. Returns:
 *   { available, runtime, model, label, reason }
 * `available` is true only if the runtime is reachable and the model is loaded.
 */
async function getStatus() {
  const cfg = getEscalationConfig();
  if (!cfg) {
    return { available: false, runtime: null, model: null, label: null, reason: 'not configured' };
  }

  if (cfg.runtime === 'tinygrad') {
    if (!IS_MAC) {
      return { available: false, runtime: 'tinygrad', model: cfg.model_path, label: cfg.label, reason: 'tinygrad runtime is macOS-only' };
    }
    const gpu = gpuDetector.getState();
    if (!gpu.available) {
      return { available: false, runtime: 'tinygrad', model: cfg.model_path, label: cfg.label, reason: 'eGPU not detected' };
    }
    const health = await _probeTinygrad(cfg.base_url);
    if (!health) {
      return { available: false, runtime: 'tinygrad', model: cfg.model_path, label: cfg.label, reason: 'tinygrad server not running' };
    }
    return { available: true, runtime: 'tinygrad', model: cfg.model_path, label: cfg.label, reason: 'tinygrad ready' };
  }

  if (cfg.runtime === 'ollama') {
    const has = await _probeOllamaModel(cfg.model_name);
    if (!has) {
      return { available: false, runtime: 'ollama', model: cfg.model_name, label: cfg.label, reason: `Ollama model "${cfg.model_name}" not pulled` };
    }
    const gpu = gpuDetector.getState();
    const reason = gpu.available ? 'eGPU + escalation model ready' : 'escalation model ready (CPU/iGPU)';
    return { available: true, runtime: 'ollama', model: cfg.model_name, label: cfg.label, reason };
  }

  return { available: false, runtime: null, model: null, label: null, reason: 'unknown runtime' };
}

/**
 * Probe runtime health and emit a `status` event if it changed.
 */
async function probe() {
  const s = await getStatus();
  const changed = !_lastStatus || JSON.stringify(_lastStatus) !== JSON.stringify(s);
  _lastStatus = s;
  if (changed) events.emit('status', s);
  return s;
}

function getLastStatus() {
  return _lastStatus;
}

/**
 * Return a provider-like object that `streamChat` can use to dispatch one
 * request through the escalation runtime. Returns null if unavailable.
 */
function getStreamChatProvider() {
  const cfg = getEscalationConfig();
  if (!cfg) return null;

  if (cfg.runtime === 'tinygrad') {
    // tinygrad-server.py exposes /v1/chat/completions. The OpenAI streamer
    // appends /chat/completions to base_url, so we include /v1 here.
    return {
      type: 'openai',
      base_url: cfg.base_url.replace(/\/+$/, '') + '/v1',
      api_key: '',
      model: 'local',
      max_tokens: 4096,
      _escalation: true,
      _label: cfg.label,
    };
  }

  if (cfg.runtime === 'ollama') {
    return {
      type: 'ollama',
      model: cfg.model_name,
      _escalation: true,
      _label: cfg.label,
    };
  }

  return null;
}

/**
 * Start a recurring probe. Emits `status` events when state changes.
 * @param {number} intervalMs default 30000 — aligned with gpu-detect cadence
 */
function startProbing(intervalMs = 30000) {
  if (_probeTimer) return;
  probe().catch(() => {});
  _probeTimer = setInterval(() => { probe().catch(() => {}); }, intervalMs);
  if (_probeTimer.unref) _probeTimer.unref();
  // Re-probe on GPU hot-plug to surface availability quickly
  gpuDetector.on('gpu-available', () => { probe().catch(() => {}); });
  gpuDetector.on('gpu-unavailable', () => { probe().catch(() => {}); });
}

function stopProbing() {
  if (_probeTimer) { clearInterval(_probeTimer); _probeTimer = null; }
}

module.exports = {
  getEscalationConfig,
  getStatus,
  getStreamChatProvider,
  getLastStatus,
  probe,
  startProbing,
  stopProbing,
  events,
};
