#!/usr/bin/env node
/**
 * Node fallback for Sync.exe — OTA apply/check on Windows.
 * Usage: node sync-apply.js check|apply [--channel dev|stable]
 */
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
const { execSync } = require('child_process');
const { waitForHealth, getHealthCheckPorts, probeHealthOnce } = require('./health-probe');

const MANIFEST_BASE =
  'https://pos.siraya.food/downloads/printer/channels';
const INSTALL_ROOT = path.join(
  process.env.ProgramFiles || 'C:\\Program Files',
  'Siraya',
  'Printer',
);
const STAGE_DIR = path.join(process.env.TEMP || 'C:\\Temp', 'SirayaPrinter', 'stage');
const SERVICE = 'SirayaPrinterService';
const TASK_NAME = 'SirayaPrinterAgent';

const DATA_DIR = path.join(
  process.env.ProgramData || 'C:\\ProgramData',
  'Siraya',
  'Printer',
);

function readPersistenceMode() {
  try {
    const raw = fs.readFileSync(path.join(DATA_DIR, 'persistence.json'), 'utf8');
    const parsed = JSON.parse(raw);
    return parsed.mode === 'scheduled-task' ? 'scheduled-task' : 'service';
  } catch {
    return 'service';
  }
}

function stopAgent() {
  const ps1 = path.join(INSTALL_ROOT, 'installer', 'windows', 'stop-agent.ps1');
  if (fs.existsSync(ps1)) {
    try {
      execSync(
        `powershell -NoProfile -ExecutionPolicy Bypass -File "${ps1}"`,
        { stdio: 'ignore', shell: true },
      );
    } catch {
      /* ignore */
    }
  }
  try {
    execSync(`schtasks /End /TN ${TASK_NAME}`, { stdio: 'ignore', shell: true });
  } catch {
    /* ignore */
  }
  try {
    execSync(`net stop ${SERVICE}`, { stdio: 'ignore', shell: true });
  } catch {
    /* ignore */
  }
  try {
    execSync(
      'powershell -NoProfile -Command "Get-Process -Name node -ErrorAction SilentlyContinue | ForEach-Object { $c = (Get-CimInstance Win32_Process -Filter (\'ProcessId=\' + $_.Id)).CommandLine; if ($c -match \'Siraya[\\\\/]Printer\') { Stop-Process -Id $_.Id -Force } }"',
      { stdio: 'ignore', shell: true },
    );
  } catch {
    /* ignore */
  }
}

function startAgent() {
  const mode = readPersistenceMode();
  if (mode === 'scheduled-task') {
    try {
      execSync(`schtasks /Run /TN ${TASK_NAME}`, { stdio: 'ignore', shell: true });
      return 'task';
    } catch {
      /* fall through */
    }
  } else {
    try {
      execSync(`net start ${SERVICE}`, { stdio: 'inherit', shell: true });
      return 'service';
    } catch {
      /* try scheduled task or bat */
    }
  }
  try {
    execSync(`schtasks /Run /TN ${TASK_NAME}`, { stdio: 'ignore', shell: true });
    return 'task';
  } catch {
    /* ignore */
  }
  const bat = path.join(INSTALL_ROOT, 'start-agent.bat');
  if (fs.existsSync(bat)) {
    execSync(`start "" /B "${bat}"`, { stdio: 'ignore', shell: true });
    return 'bat';
  }
  throw new Error('Could not restart print agent (service, task, or start-agent.bat)');
}

function sha256File(filePath) {
  return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
}

function toInstallPath(manifestPath) {
  const n = manifestPath.replace(/\//g, path.sep);
  if (n.startsWith(`app${path.sep}`)) {
    return path.join(INSTALL_ROOT, n.slice('app'.length + 1));
  }
  return path.join(INSTALL_ROOT, n);
}

function toStagePath(manifestPath) {
  return path.join(STAGE_DIR, manifestPath.replace(/\//g, path.sep));
}

function getJson(url) {
  return new Promise((resolve, reject) => {
    const lib = url.startsWith('https') ? https : http;
    lib
      .get(url, (res) => {
        let data = '';
        res.on('data', (c) => (data += c));
        res.on('end', () => {
          if (res.statusCode !== 200) {
            reject(new Error(`HTTP ${res.statusCode} for ${url}`));
            return;
          }
          resolve(JSON.parse(data));
        });
      })
      .on('error', reject);
  });
}

function download(url, dest) {
  return new Promise((resolve, reject) => {
    const lib = url.startsWith('https') ? https : http;
    lib.get(url, (res) => {
      if (res.statusCode !== 200) {
        reject(new Error(`HTTP ${res.statusCode} for ${url}`));
        return;
      }
      fs.mkdirSync(path.dirname(dest), { recursive: true });
      const file = fs.createWriteStream(dest);
      res.pipe(file);
      file.on('finish', () => file.close(resolve));
      file.on('error', reject);
    }).on('error', reject);
  });
}

function getInstalledVersion() {
  const ptr = path.join(INSTALL_ROOT, 'current-version.txt');
  if (fs.existsSync(ptr)) return fs.readFileSync(ptr, 'utf8').trim();
  try {
    const pkg = JSON.parse(
      fs.readFileSync(path.join(INSTALL_ROOT, 'package.json'), 'utf8'),
    );
    return pkg.version || 'unknown';
  } catch {
    return 'unknown';
  }
}

function changedFiles(manifest) {
  return manifest.files.filter((f) => {
    const local = toInstallPath(f.path);
    return !fs.existsSync(local) || sha256File(local) !== f.sha256;
  });
}

async function healthOk() {
  for (const port of getHealthCheckPorts(DATA_DIR)) {
    const ok = await probeHealthOnce('127.0.0.1', port, 2000);
    if (ok) return ok;
  }
  return null;
}

async function waitHealth(sec = 90) {
  return waitForHealth({ dataDir: DATA_DIR, timeoutSec: sec });
}

async function apply(channel) {
  if (!fs.existsSync(INSTALL_ROOT)) {
    throw new Error(`Not installed at ${INSTALL_ROOT}. Run Install.cmd first.`);
  }
  const manifest = await getJson(`${MANIFEST_BASE}/${channel}/manifest.json`);
  const changed = changedFiles(manifest);
  if (changed.length === 0) {
    console.log('Already up to date.');
    const port = await healthOk();
    if (port) {
      execSync(`start http://127.0.0.1:${port}/`, { stdio: 'ignore', shell: true });
    }
    return;
  }
  console.log(`Syncing ${changed.length} file(s) → v${manifest.version}`);
  if (fs.existsSync(STAGE_DIR)) fs.rmSync(STAGE_DIR, { recursive: true, force: true });
  fs.mkdirSync(STAGE_DIR, { recursive: true });

  for (const file of changed) {
    const url = `${manifest.baseUrl.replace(/\/$/, '')}/${file.path}`;
    const stagePath = toStagePath(file.path);
    console.log(`Download  ${file.path}`);
    await download(url, stagePath);
    if (sha256File(stagePath) !== file.sha256) {
      throw new Error(`Hash mismatch: ${file.path}`);
    }
  }

  try {
    stopAgent();
  } catch {
    /* ignore */
  }
  await new Promise((r) => setTimeout(r, 2000));

  for (const file of changed) {
    const installPath = toInstallPath(file.path);
    fs.mkdirSync(path.dirname(installPath), { recursive: true });
    fs.copyFileSync(toStagePath(file.path), installPath);
  }

  if (changed.some((f) => f.path.endsWith('package-lock.json'))) {
    const npm = path.join(INSTALL_ROOT, 'runtime', 'npm.cmd');
    if (fs.existsSync(path.join(INSTALL_ROOT, 'node_modules'))) {
      console.log('Skipping npm ci — node_modules present');
    } else if (fs.existsSync(npm)) {
      execSync(`"${npm}" ci --omit=dev`, { cwd: INSTALL_ROOT, stdio: 'inherit' });
    }
  }

  fs.writeFileSync(
    path.join(INSTALL_ROOT, 'current-version.txt'),
    manifest.version,
  );
  const mode = startAgent();
  console.log(`Restarted agent via ${mode}`);

  const port = await waitHealth(90);
  if (!port) throw new Error('Health check failed after update');
  console.log(`Updated → v${manifest.version} at http://127.0.0.1:${port}/`);
  execSync(`start http://127.0.0.1:${port}/`, { stdio: 'ignore', shell: true });
}

async function check(channel) {
  const manifest = await getJson(`${MANIFEST_BASE}/${channel}/manifest.json`);
  const changed = changedFiles(manifest);
  console.log(`Installed: ${getInstalledVersion()}`);
  console.log(`Remote:    ${manifest.version} (${channel})`);
  console.log(`Changes:   ${changed.length} of ${manifest.files.length}`);
}

async function main() {
  const cmd = process.argv[2] || 'check';
  let channel = 'stable';
  for (let i = 3; i < process.argv.length; i++) {
    if (process.argv[i] === '--channel' && process.argv[i + 1]) channel = process.argv[++i];
  }
  if (cmd === 'check') await check(channel);
  else if (cmd === 'apply') await apply(channel);
  else {
    console.error('Usage: node sync-apply.js check|apply [--channel dev|stable]');
    process.exit(2);
  }
}

main().catch((e) => {
  console.error(`ERROR: ${e.message}`);
  process.exit(1);
});
