// PRE Web GUI — GPU hot-plug detection module
// Polls for GPU availability changes at runtime so PRE can react
// when an eGPU is connected or disconnected without restarting.

const EventEmitter = require('events');
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');

const IS_WIN = process.platform === 'win32';
const IS_MAC = process.platform === 'darwin';
const IS_LINUX = process.platform === 'linux';

const PRE_DIR = path.join(os.homedir(), '.pre');
const STATE_FILE = path.join(PRE_DIR, 'gpu-state.json');

function _run(cmd) {
  try { return execSync(cmd, { encoding: 'utf-8', timeout: 10000 }).trim(); } catch { return ''; }
}

/**
 * Detect TinyGPU driver loaded via systemextensionsctl (macOS only).
 * Mirrors install.sh:125-127.
 */
function _detectTinyGpuDriver() {
  if (!IS_MAC) return false;
  const out = _run('systemextensionsctl list 2>/dev/null');
  return /tinygpu/i.test(out);
}

/**
 * Check for TinyGPU.app (macOS only).
 * Mirrors install.sh:130-133.
 */
function _detectTinyGpuApp() {
  if (!IS_MAC) return false;
  return fs.existsSync('/Applications/TinyGPU.app') ||
         fs.existsSync(path.join(os.homedir(), 'Applications', 'TinyGPU.app'));
}

/**
 * Scan Thunderbolt bus for GPU devices (macOS only).
 * Mirrors install.sh:136-137.
 * @returns {{ name: string, vendor: string }|null}
 */
function _detectThunderboltGpu() {
  if (!IS_MAC) return null;
  const out = _run('system_profiler SPThunderboltDataType 2>/dev/null');
  // Find any line containing GPU vendor/product keywords (mirrors install.sh grep)
  const lines = out.split('\n');
  for (const line of lines) {
    if (/NVIDIA|GeForce|RTX|Radeon|RX /i.test(line)) {
      // Strip "key: " prefix if present (mirrors install.sh sed)
      const name = line.replace(/^.*:\s*/, '').trim();
      if (!name) continue;
      const vendor = /nvidia|geforce|rtx/i.test(name) ? 'nvidia' : 'amd';
      return { name, vendor };
    }
  }
  return null;
}

/**
 * Query NVIDIA GPU via Docker (macOS TinyGPU path).
 * Mirrors install.sh:153-166.
 * @returns {{ name: string, vram_gb: number }|null}
 */
function _detectViaDocker() {
  if (IS_WIN || IS_LINUX) return null; // Linux/Windows use nvidia-smi directly
  // Only try Docker if it's available and running
  const dockerOk = _run('docker info 2>/dev/null');
  if (!dockerOk) return null;
  const out = _run('docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu24.04 nvidia-smi --query-gpu=name,memory.total --format=csv,noheader 2>/dev/null');
  if (!out) return null;
  const parts = out.split(',');
  const name = (parts[0] || '').trim();
  const mibMatch = out.match(/(\d+)\s*MiB/);
  const vram_gb = mibMatch ? Math.floor(parseInt(mibMatch[1], 10) / 1024) : 0;
  return name ? { name, vram_gb } : null;
}

/**
 * Query NVIDIA GPU directly via nvidia-smi (Linux/Windows).
 * Mirrors pre-server.sh:131-140 (Linux) and platform.js:433-446.
 * @returns {{ name: string, vram_gb: number, driver: string }|null}
 */
function _detectNvidiaSmi() {
  const cmd = IS_WIN
    ? 'nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader 2>NUL'
    : 'nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader 2>/dev/null';
  const out = _run(cmd);
  if (!out) return null;
  const parts = out.split(',').map(s => s.trim());
  const name = parts[0] || '';
  const memStr = parts[1] || '';
  const driver = parts[2] || '';
  const mibMatch = memStr.match(/(\d+)/);
  const vram_gb = mibMatch ? Math.floor(parseInt(mibMatch[1], 10) / 1024) : 0;
  return name ? { name, vram_gb, driver } : null;
}

/**
 * Run full GPU detection for the current platform.
 * @returns {Object} GPU state
 */
function detect() {
  const base = {
    available: false,
    vendor: null,
    name: null,
    vram_gb: 0,
    backend: null,
    driver: null,
    lastCheck: Date.now(),
  };

  if (IS_MAC) {
    // macOS: check TinyGPU (eGPU) or Apple Silicon Metal
    const tinyGpuLoaded = _detectTinyGpuDriver();
    const tinyGpuApp = _detectTinyGpuApp();
    const tbGpu = _detectThunderboltGpu();

    if (tinyGpuLoaded || tinyGpuApp || tbGpu) {
      base.driver = 'tinygpu';
      base.vendor = tbGpu?.vendor || 'nvidia';
      base.name = tbGpu?.name || 'eGPU (TinyGPU)';
      base.backend = base.vendor === 'nvidia' ? 'cuda' : 'rocm';

      // Try to get VRAM via Docker nvidia-smi
      if (base.vendor === 'nvidia') {
        const docker = _detectViaDocker();
        if (docker) {
          if (docker.name) base.name = docker.name;
          base.vram_gb = docker.vram_gb;
        }
      }

      base.available = true;
      return base;
    }

    // No eGPU — Apple Silicon has Metal but that's the primary GPU, not an "eGPU"
    return base;
  }

  if (IS_LINUX || IS_WIN) {
    // Linux/Windows: direct nvidia-smi
    const nv = _detectNvidiaSmi();
    if (nv) {
      base.available = true;
      base.vendor = 'nvidia';
      base.name = nv.name;
      base.vram_gb = nv.vram_gb;
      base.backend = 'cuda';
      base.driver = 'native';
      return base;
    }
    return base;
  }

  return base;
}

// ── GpuDetector class ────────────────────────────────────────────────────

class GpuDetector extends EventEmitter {
  constructor() {
    super();
    this._interval = null;
    this._state = null;
    this._pollMs = 30000;
  }

  /**
   * Start polling for GPU changes.
   * First poll runs immediately.
   * @param {number} [intervalMs=30000]
   */
  start(intervalMs) {
    if (this._interval) return;
    this._pollMs = intervalMs || 30000;

    // Load previous state if available
    this._state = _loadState();

    // Immediate first check
    this._poll();

    this._interval = setInterval(() => this._poll(), this._pollMs);
  }

  /**
   * Stop polling.
   */
  stop() {
    if (this._interval) {
      clearInterval(this._interval);
      this._interval = null;
    }
  }

  /**
   * Get current GPU state synchronously.
   * @returns {Object|null}
   */
  getState() {
    return this._state;
  }

  /**
   * Force an immediate check and return the result.
   * @returns {Object}
   */
  forceCheck() {
    this._poll();
    return this._state;
  }

  /**
   * Internal poll — runs detection and emits events on state changes.
   */
  _poll() {
    const prev = this._state;
    let next;
    try {
      next = detect();
    } catch (err) {
      console.error('[gpu-detect] Detection error:', err.message);
      return;
    }

    this._state = next;

    // Determine what changed
    if (!prev) {
      // First poll — emit current state
      if (next.available) this.emit('gpu-available', next);
      _saveState(next);
      return;
    }

    if (!prev.available && next.available) {
      console.log(`[gpu-detect] GPU connected: ${next.name} (${next.vram_gb}GB VRAM)`);
      this.emit('gpu-available', next);
      _saveState(next);
    } else if (prev.available && !next.available) {
      console.log('[gpu-detect] GPU disconnected');
      this.emit('gpu-unavailable', next);
      _saveState(next);
    } else if (prev.available && next.available) {
      // Check for GPU swap (different name or VRAM)
      if (prev.name !== next.name || prev.vram_gb !== next.vram_gb) {
        console.log(`[gpu-detect] GPU changed: ${next.name} (${next.vram_gb}GB VRAM)`);
        this.emit('gpu-changed', next);
        _saveState(next);
      }
    }
  }
}

// ── State persistence ────────────────────────────────────────────────────

function _saveState(state) {
  try {
    fs.mkdirSync(PRE_DIR, { recursive: true });
    fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
  } catch {}
}

function _loadState() {
  try {
    return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
  } catch {
    return null;
  }
}

// ── Singleton ────────────────────────────────────────────────────────────

const gpuDetector = new GpuDetector();

module.exports = {
  gpuDetector,
  GpuDetector,
  detect,
  // Exported for testing
  _detectTinyGpuDriver,
  _detectTinyGpuApp,
  _detectThunderboltGpu,
  _detectViaDocker,
  _detectNvidiaSmi,
  _loadState,
  _saveState,
};
