'use strict';

const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
const crypto = require('crypto');
const { spawn } = require('child_process');
const lock = require('./lock');
const reporter = require('./reporter');
const { readAgentCredentials, authHeaders, maskedCredentialSummary } = require('./agent-credentials');
const runtimeDelta = require('./runtime-delta');
const runtimeSlots = require('./runtime-slots');

const PROGRAMDATA = process.env.ProgramData || 'C:\\ProgramData';
const DATA_DIR = process.env.ARKHON_UPDATER_DATA_DIR || path.join(PROGRAMDATA, 'ARKHON', 'Updater');
const DOWNLOADS_DIR = path.join(DATA_DIR, 'downloads');
const BACKUPS_DIR = path.join(DATA_DIR, 'backups');
const AGENT_DATA_DIR = process.env.ARKHON_AGENT_DATA_DIR || path.join(PROGRAMDATA, 'ARKHON', 'Agent');
const LOGS_DIR = path.join(DATA_DIR, 'logs');
const LOG_FILE = path.join(LOGS_DIR, 'updater-jobs.log');
const AGENT_SERVICE_CANDIDATES = (process.env.ARKHON_AGENT_SERVICE_CANDIDATES || 'ArkhonMonitorConnector,ARKHON Agent')
  .split(',')
  .map((name) => name.trim())
  .filter(Boolean);
const UPDATER_SERVICE_NAME = process.env.ARKHON_UPDATER_SERVICE_NAME || 'ARKHON Updater';
const CURRENT_ARCH = process.arch === 'ia32' ? 'ia32' : 'x64';

for (const dir of [DATA_DIR, DOWNLOADS_DIR, BACKUPS_DIR, LOGS_DIR]) fs.mkdirSync(dir, { recursive: true });

function log(job, message, extra = {}) {
  const line = JSON.stringify({
    at: reporter.now(),
    jobId: job && job.id,
    status: job && job.status,
    message,
    ...extra,
  });
  try { fs.appendFileSync(LOG_FILE, `${line}\n`, 'utf8'); } catch (_) {}
}

function setStatus(job, saveJob, status, patch = {}) {
  job.status = status;
  job.updatedAt = reporter.now();
  Object.assign(job, patch);
  saveJob(job);
  log(job, status, patch.result ? { result: patch.result } : {});
}

function cleanVersion(value) {
  return String(value || '0.0.0').trim().replace(/^v/i, '');
}

function compareVersions(a, b) {
  const pa = cleanVersion(a).split('.').map((n) => parseInt(n, 10) || 0);
  const pb = cleanVersion(b).split('.').map((n) => parseInt(n, 10) || 0);
  for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
    const da = pa[i] || 0;
    const db = pb[i] || 0;
    if (da > db) return 1;
    if (da < db) return -1;
  }
  return 0;
}

function readJson(filePath, fallback = null) {
  try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (_) { return fallback; }
}

function installedVersion() {
  const configuredAppRoot = String(process.env.ARKHON_APP_ROOT || '').trim();
  const candidates = [
    configuredAppRoot ? path.join(configuredAppRoot, 'package.json') : null,
    path.join(path.dirname(__dirname), 'app', 'package.json'),
    path.join(path.dirname(__dirname), 'arkhon-app', 'package.json'),
    path.join(process.cwd(), '..', 'app', 'package.json'),
  ].filter(Boolean);
  for (const filePath of candidates) {
    const pkg = readJson(filePath);
    if (pkg && pkg.version) return cleanVersion(pkg.version);
  }
  return '0.0.0';
}

function appRoot() {
  return process.env.ARKHON_APP_ROOT || path.join(PROGRAMDATA, 'ARKHON', 'app');
}

function stamp() {
  return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
}

function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function fetchJson(url, timeoutMs = 8000) {
  return new Promise((resolve, reject) => {
    let parsed;
    try { parsed = new URL(url); } catch (_) { return reject(new Error('health_check_url_invalid')); }
    const proto = parsed.protocol === 'https:' ? https : http;
    const req = proto.get(parsed, {
      headers: { 'User-Agent': 'ARKHON-Updater-Service', Accept: 'application/json' },
    }, (res) => {
      let body = '';
      res.setEncoding('utf8');
      res.on('data', (chunk) => { body += chunk; });
      res.on('end', () => {
        if (res.statusCode < 200 || res.statusCode >= 300) {
          return reject(Object.assign(new Error(`HTTP ${res.statusCode}`), { statusCode: res.statusCode }));
        }
        try {
          resolve(body ? JSON.parse(body) : {});
        } catch (error) {
          reject(Object.assign(new Error('health_check_invalid_json'), { cause: error }));
        }
      });
    });
    req.on('error', reject);
    req.setTimeout(timeoutMs, () => req.destroy(new Error('health_check_timeout')));
  });
}

function createApplyBackup(job, currentVersion) {
  const source = appRoot();
  const backupRoot = path.join(BACKUPS_DIR, `${cleanVersion(currentVersion)}-${stamp()}`);
  const appBackup = path.join(backupRoot, 'app');
  fs.mkdirSync(backupRoot, { recursive: true });
  if (fs.existsSync(source)) {
    fs.cpSync(source, appBackup, {
      recursive: true,
      force: true,
      errorOnExist: false,
      filter: (sourcePath) => {
        const base = path.basename(sourcePath).toLowerCase();
        return !['logs', 'coverage', 'dist', 'releases'].includes(base);
      },
    });
  }
  fs.writeFileSync(path.join(backupRoot, 'backup-manifest.json'), `${JSON.stringify({
    jobId: job.id,
    createdAt: reporter.now(),
    source,
    appBackup,
    version: currentVersion,
  }, null, 2)}\n`, 'utf8');
  return { ok: true, path: backupRoot, appBackup, source, version: currentVersion };
}

function restoreApplyBackup(backup) {
  if (!backup || !backup.appBackup || !backup.source || !fs.existsSync(backup.appBackup)) {
    const error = new Error('rollback_backup_missing');
    error.code = 'rollback_backup_missing';
    throw error;
  }
  fs.mkdirSync(path.dirname(backup.source), { recursive: true });
  fs.cpSync(backup.appBackup, backup.source, { recursive: true, force: true, errorOnExist: false });
  return { ok: true, restoredFrom: backup.appBackup, restoredTo: backup.source };
}

function rollbackIfChanged(job, saveJob, backup, previousVersion) {
  job.rollback = { attempted: false };
  if (!backup || cleanVersion(installedVersion()) === cleanVersion(previousVersion)) return job.rollback;
  setStatus(job, saveJob, 'rollback_started');
  job.rollback.attempted = true;
  try {
    const rollback = restoreApplyBackup(backup);
    job.rollback = { attempted: true, ok: true, ...rollback };
    setStatus(job, saveJob, 'rollback_completed');
  } catch (rollbackError) {
    job.rollback = { attempted: true, ok: false, detail: reporter.serializeError(rollbackError) };
    setStatus(job, saveJob, 'rollback_failed');
  }
  return job.rollback;
}

function runCommand(command, args = [], options = {}) {
  return new Promise((resolve, reject) => {
    const child = spawn(command, args, {
      cwd: options.cwd || undefined,
      env: options.env || process.env,
      windowsHide: true,
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    let stdout = '';
    let stderr = '';
    let settled = false;
    const timeoutMs = Number(options.timeoutMs || 0);
    const timer = timeoutMs > 0 ? setTimeout(() => {
      if (settled) return;
      settled = true;
      try { child.kill('SIGKILL'); } catch (_) {}
      const err = new Error(options.timeoutError || 'command_timeout');
      err.stdout = stdout;
      err.stderr = stderr;
      reject(err);
    }, timeoutMs) : null;

    child.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
    child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
    child.on('error', (error) => {
      if (settled) return;
      settled = true;
      if (timer) clearTimeout(timer);
      error.stdout = stdout;
      error.stderr = stderr;
      reject(error);
    });
    child.on('close', (exitCode, signal) => {
      if (settled) return;
      settled = true;
      if (timer) clearTimeout(timer);
      resolve({ exitCode, signal, stdout, stderr });
    });
  });
}

async function waitForInstaller(job, installerPath) {
  if (job.request.dryRun || job.request.apply === false) {
    return { ok: true, skipped: true, reason: 'dry_run', installerPath };
  }

  const args = Array.isArray(job.request.installerArgs) && job.request.installerArgs.length
    ? job.request.installerArgs
    : ['/S'];
  const command = job.request.installerCommand || installerPath;
  const timeoutMs = Number(job.request.installerTimeoutMs || process.env.ARKHON_INSTALLER_TIMEOUT_MS || 15 * 60 * 1000);
  const result = await runCommand(command, args, {
    timeoutMs,
    timeoutError: 'installer_timeout',
  });
  if (result.exitCode !== 0) {
    const err = new Error('installer_exit_failed');
    err.details = { exitCode: result.exitCode, signal: result.signal, stdout: result.stdout, stderr: result.stderr };
    throw err;
  }
  return { ok: true, installerPath, command, args, exitCode: result.exitCode };
}

async function waitForExpectedVersion(expectedVersion, timeoutMs) {
  const deadline = Date.now() + timeoutMs;
  let lastVersion = installedVersion();
  while (Date.now() <= deadline) {
    lastVersion = installedVersion();
    if (cleanVersion(lastVersion) === cleanVersion(expectedVersion)) {
      return { ok: true, version: lastVersion };
    }
    await wait(1000);
  }
  const err = new Error('version_not_updated');
  err.details = { expectedVersion: cleanVersion(expectedVersion), installedVersion: cleanVersion(lastVersion) };
  throw err;
}

async function assertServiceRunning(name) {
  if (process.platform !== 'win32') return { name, skipped: true, reason: 'non_windows' };
  const deadline = Date.now() + Number(process.env.ARKHON_SERVICE_START_WAIT_MS || 60000);
  let lastResult = null;
  while (Date.now() <= deadline) {
    lastResult = await runCommand('sc.exe', ['query', name], { timeoutMs: 10000, timeoutError: 'service_check_timeout' });
    if (lastResult.exitCode === 0 && /\bRUNNING\b/i.test(lastResult.stdout)) return { name, running: true };
    await wait(2000);
  }
  const err = new Error('services_not_running');
  err.details = {
    service: name,
    exitCode: lastResult?.exitCode,
    stdout: lastResult?.stdout,
    stderr: lastResult?.stderr,
  };
  throw err;
}

async function queryWindowsService(name) {
  if (process.platform !== 'win32') return { name, exists: true, skipped: true, reason: 'non_windows' };
  const result = await runCommand('sc.exe', ['query', name], { timeoutMs: 10000, timeoutError: 'service_check_timeout' });
  return {
    name,
    exists: result.exitCode === 0 && !/does not exist|no existe/i.test(`${result.stdout}\n${result.stderr}`),
    running: result.exitCode === 0 && /\bRUNNING\b/i.test(result.stdout),
    exitCode: result.exitCode,
    stdout: result.stdout,
    stderr: result.stderr,
  };
}

async function resolveWindowsService(candidates) {
  if (process.platform !== 'win32') return candidates[0];
  const checked = [];
  for (const name of candidates) {
    const status = await queryWindowsService(name);
    checked.push(status);
    if (status.exists) return name;
  }
  const err = new Error('service_not_found');
  err.details = { candidates, checked };
  throw err;
}

async function fetchJsonWithRetry(url, timeoutMs = 8000, waitMs = 60000) {
  const deadline = Date.now() + waitMs;
  let lastError = null;
  while (Date.now() <= deadline) {
    try {
      return await fetchJson(url, timeoutMs);
    } catch (error) {
      lastError = error;
      await wait(2000);
    }
  }
  throw lastError || new Error('health_check_failed');
}

async function validateInstall(job, manifest) {
  const expectedVersion = cleanVersion(manifest.version);
  const timeoutMs = Number(job.request.validationTimeoutMs || process.env.ARKHON_INSTALL_VALIDATION_TIMEOUT_MS || 300000);
  const version = await waitForExpectedVersion(expectedVersion, timeoutMs);
  const requiredServices = Array.isArray(job.request.requiredServices)
    ? job.request.requiredServices
    : [await resolveWindowsService(AGENT_SERVICE_CANDIDATES), UPDATER_SERVICE_NAME];
  const services = [];
  for (const name of requiredServices) services.push(await assertServiceRunning(name));

  const agentHealthUrl = job.request.agentHealthUrl || process.env.ARKHON_AGENT_HEALTH_URL || 'http://127.0.0.1:3001/api/health';
  const updaterStatusUrl = job.request.updaterStatusUrl || process.env.ARKHON_UPDATER_STATUS_URL || 'http://127.0.0.1:34567/updater/status';
  const health = await fetchJsonWithRetry(agentHealthUrl, 8000, timeoutMs);
  if (health.ok !== true || cleanVersion(health.version) !== expectedVersion) {
    const err = new Error('health_check_failed');
    err.details = { url: agentHealthUrl, expectedVersion, response: health };
    throw err;
  }
  const updater = await fetchJsonWithRetry(updaterStatusUrl, 8000, timeoutMs);
  if (updater.ok !== true) {
    const err = new Error('updater_status_failed');
    err.details = { url: updaterStatusUrl, response: updater };
    throw err;
  }

  const criticalConfigPaths = Array.isArray(job.request.criticalConfigPaths)
    ? job.request.criticalConfigPaths
    : [];
  const missingConfig = criticalConfigPaths.filter((filePath) => !fs.existsSync(filePath));
  if (missingConfig.length) {
    const err = new Error('critical_config_missing');
    err.details = { missingConfig };
    throw err;
  }

  return { ok: true, version: version.version, services, health, updater, criticalConfigChecked: criticalConfigPaths };
}

function fetchText(url) {
  return new Promise((resolve, reject) => {
    let parsed;
    try { parsed = new URL(url); } catch (_) { return reject(new Error('update_feed_url_invalid')); }
    const proto = parsed.protocol === 'https:' ? https : http;
    const req = proto.get(parsed, {
      headers: {
        'User-Agent': 'ARKHON-Updater-Service',
        Accept: 'application/json, text/plain, application/x-yaml, */*',
      },
    }, (res) => {
      if ([301, 302, 307, 308].includes(res.statusCode)) {
        res.resume();
        const nextUrl = new URL(res.headers.location, parsed).toString();
        return fetchText(nextUrl).then(resolve).catch(reject);
      }
      let body = '';
      res.setEncoding('utf8');
      res.on('data', (chunk) => { body += chunk; });
      res.on('end', () => {
        if (res.statusCode < 200 || res.statusCode >= 300) return reject(new Error(`HTTP ${res.statusCode}`));
        resolve(body);
      });
    });
    req.on('error', reject);
    req.setTimeout(15000, () => req.destroy(new Error('update_check_offline')));
  });
}

function parseLatestYml(text) {
  const out = {};
  for (const line of String(text || '').split(/\r?\n/)) {
    const match = line.match(/^([a-zA-Z_][\w-]*):\s*(.*)$/);
    if (match) out[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, '');
  }
  const fileMatch = String(text || '').match(/^\s*-\s+url:\s*(.+)$/m);
  if (fileMatch && !out.path) out.path = fileMatch[1].trim().replace(/^['"]|['"]$/g, '');
  return out;
}

async function fetchManifest(feedUrl) {
  const text = await fetchText(feedUrl);
  let manifest;
  try {
    manifest = JSON.parse(text);
  } catch (_) {
    const yml = parseLatestYml(text);
    const base = feedUrl.replace(/\/[^/?#]+(?:[?#].*)?$/, '');
    manifest = {
      version: yml.version,
      download_url: yml.path ? `${base}/${yml.path}` : yml.url,
      sha256: yml.sha512 ? null : yml.sha256,
      channel: yml.channel,
      notes: yml.notes || yml.notas,
    };
  }
  return normalizeManifest(manifest, feedUrl);
}

function normalizeManifest(manifest, feedUrl) {
  const base = feedUrl.replace(/\/[^/?#]+(?:[?#].*)?$/, '');
  const downloadUrl = manifest.download_url || manifest.downloadUrl || manifest.url ||
    (manifest.path ? `${base}/${manifest.path}` : '');
  return {
    version: cleanVersion(manifest.version || manifest.tag_name),
    type: manifest.type || (String(downloadUrl).toLowerCase().endsWith('.zip') ? 'runtime-zip' : 'installer-exe'),
    fileName: manifest.fileName || manifest.file_name || (downloadUrl ? path.basename(new URL(downloadUrl).pathname) : ''),
    download_url: downloadUrl,
    sha256: manifest.sha256 || null,
    size: manifest.size || null,
    arch: manifest.arch || manifest.architecture || null,
    minUpdaterVersion: manifest.minUpdaterVersion || manifest.min_updater_version || null,
    channel: manifest.channel || process.env.ARKHON_UPDATE_CHANNEL || 'stable',
    notes: manifest.notas || manifest.notes || '',
    source: feedUrl,
    deltas: Array.isArray(manifest.deltas) ? manifest.deltas : [],
  };
}

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

function sha256FileAsync(filePath) {
  return new Promise((resolve, reject) => {
    const hash = crypto.createHash('sha256');
    const stream = fs.createReadStream(filePath);
    stream.on('data', chunk => hash.update(chunk));
    stream.on('error', reject);
    stream.on('end', () => resolve(hash.digest('hex')));
  });
}

function downloadFile(url, destPath, options = {}) {
  return new Promise((resolve, reject) => {
    let parsed;
    try { parsed = new URL(url); } catch (_) { return reject(new Error('update_download_url_invalid')); }
    const redirects = Number(options.redirects || 0);
    if (redirects > 5) return reject(new Error('update_download_too_many_redirects'));
    const partPath = options.partPath || `${destPath}.part`;
    fs.mkdirSync(path.dirname(destPath), { recursive: true });
    const existingBytes = fs.existsSync(partPath) ? fs.statSync(partPath).size : 0;
    const proto = parsed.protocol === 'https:' ? https : http;
    const headers = { 'User-Agent': 'ARKHON-Updater-Service' };
    if (existingBytes > 0) headers.Range = `bytes=${existingBytes}-`;
    const req = proto.get(parsed, { headers }, (res) => {
      if ([301, 302, 307, 308].includes(res.statusCode)) {
        res.resume();
        const nextUrl = new URL(res.headers.location, parsed).toString();
        return downloadFile(nextUrl, destPath, { ...options, redirects: redirects + 1, partPath }).then(resolve).catch(reject);
      }
      if (res.statusCode === 416 && existingBytes > 0) {
        const total = Number(String(res.headers['content-range'] || '').match(/\*\/(\d+)/)?.[1] || 0);
        res.resume();
        if (total === existingBytes) {
          try { fs.unlinkSync(destPath); } catch (_) {}
          fs.renameSync(partPath, destPath);
          return resolve({ path: destPath, bytes: total, resumed: true, alreadyComplete: true });
        }
        try { fs.unlinkSync(partPath); } catch (_) {}
        return downloadFile(url, destPath, { ...options, redirects: 0, partPath }).then(resolve).catch(reject);
      }
      if (res.statusCode !== 200 && res.statusCode !== 206) {
        res.resume();
        return reject(new Error(`update_download_failed_http_${res.statusCode}`));
      }
      const resumed = res.statusCode === 206 && existingBytes > 0;
      if (res.statusCode === 206) {
        const rangeStart = Number(String(res.headers['content-range'] || '').match(/^bytes (\d+)-/)?.[1]);
        if (!Number.isFinite(rangeStart) || rangeStart !== existingBytes) {
          res.resume();
          return reject(new Error('update_download_invalid_content_range'));
        }
      }
      const file = fs.createWriteStream(partPath, { flags: resumed ? 'a' : 'w' });
      let receivedBytes = resumed ? existingBytes : 0;
      let lastReportedAt = 0;
      res.on('data', (chunk) => {
        receivedBytes += chunk.length;
        if (typeof options.onProgress === 'function' && Date.now() - lastReportedAt >= 1000) {
          lastReportedAt = Date.now();
          options.onProgress({ receivedBytes, resumed, totalBytes: Number(res.headers['content-length'] || 0) + (resumed ? existingBytes : 0) });
        }
      });
      res.pipe(file);
      file.on('finish', () => file.close(() => {
        try {
          try { fs.unlinkSync(destPath); } catch (_) {}
          fs.renameSync(partPath, destPath);
          resolve({ path: destPath, bytes: receivedBytes, resumed });
        } catch (error) {
          reject(error);
        }
      }));
      file.on('error', (err) => {
        res.destroy();
        reject(err);
      });
    });
    req.on('error', reject);
    req.setTimeout(Number(options.timeoutMs || process.env.ARKHON_UPDATE_DOWNLOAD_TIMEOUT_MS || 120000), () => req.destroy(new Error('update_download_failed_timeout')));
  });
}

function activationSnapshot() {
  const candidates = [
    path.join(process.cwd(), 'activation.json'),
    path.join(AGENT_DATA_DIR, 'config', 'activation.json'),
    path.join(AGENT_DATA_DIR, 'activation.json'),
  ];
  for (const filePath of candidates) {
    const data = readJson(filePath);
    if (!data) continue;
    return {
      path: filePath,
      hasKey: !!data.key,
      plan: data.plan || null,
      cliente_id: data.cliente_id || data.client_id || null,
      hwid: data.hwid || null,
      expires_at: data.expires_at || null,
    };
  }
  return { path: null, hasKey: false };
}

function readEnvFile(filePath) {
  const out = {};
  try {
    for (const line of fs.readFileSync(filePath, 'utf8').split(/\r?\n/)) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('#')) continue;
      const idx = trimmed.indexOf('=');
      if (idx > 0) out[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1).trim();
    }
  } catch (_) {}
  return out;
}

function localLicenseKey() {
  if (process.env.LICENSE_KEY) return process.env.LICENSE_KEY;
  const activationCandidates = [
    path.join(process.cwd(), 'activation.json'),
    path.join(appRoot(), 'activation.json'),
    path.join(AGENT_DATA_DIR, 'config', 'activation.json'),
    path.join(AGENT_DATA_DIR, 'activation.json'),
  ];
  for (const filePath of activationCandidates) {
    const data = readJson(filePath);
    if (data?.key) return data.key;
  }
  return readEnvFile(path.join(appRoot(), '.env')).LICENSE_KEY || '';
}

function validateLocalLicenseForUpdate(snapshot) {
  const key = localLicenseKey();
  if (!key) return { ok: false, reason: 'local_license_key_missing', activation: snapshot };
  try {
    const validator = require(path.join(appRoot(), 'src', 'license', 'licenseValidator.js'));
    const state = validator.validateKey(key);
    const moduleAllowed = state.valid === true && !['demo', 'trial'].includes(String(state.plan || '').toLowerCase());
    return {
      ok: state.valid === true && moduleAllowed,
      skippedCloud: true,
      reason: state.valid === true ? (moduleAllowed ? null : 'update_not_allowed') : state.reason,
      plan: state.plan || null,
      modules: state.modules || [],
      activation: snapshot,
    };
  } catch (error) {
    return { ok: false, reason: 'local_license_validation_failed', error: error.message, activation: snapshot };
  }
}

function updaterRoot() {
  return process.env.ARKHON_UPDATER_ROOT || path.join(PROGRAMDATA, 'ARKHON', 'arkhon-updater');
}

function serviceDir() {
  return path.join(appRoot(), 'service');
}

function canonicalRuntimeExecutable() {
  return process.env.ARKHON_RUNTIME_EXECUTABLE ||
    path.join(process.env.ProgramFiles || 'C:\\Program Files', 'ARKHON Monitor', 'ARKHON Monitor.exe');
}

function shouldPreserveRuntimePath(relativePath) {
  const normalized = String(relativePath || '').replace(/\\/g, '/').toLowerCase();
  const base = path.basename(normalized);
  if (!normalized) return false;
  if (base === '.env' || base === 'activation.json' || base === 'connector.json' || base === 'connector-secret.enc' || base === 'entitlement-cache.enc') return true;
  if (base.endsWith('.local.json') || base.endsWith('.log')) return true;
  return normalized === 'config' || normalized.startsWith('config/') ||
    normalized === 'logs' || normalized.startsWith('logs/') ||
    normalized === 'diagnostics' || normalized.startsWith('diagnostics/') ||
    normalized === 'releases' || normalized.startsWith('releases/') ||
    normalized === 'coverage' || normalized.startsWith('coverage/') ||
    normalized === 'dist' || normalized.startsWith('dist/');
}

function isServiceWrapperPath(relativePath) {
  const normalized = String(relativePath || '').replace(/\\/g, '/').toLowerCase();
  return normalized === 'winsw-agent.exe' ||
    normalized === 'winsw-updater.exe' ||
    normalized === 'service/winsw-agent.exe' ||
    normalized === 'service/winsw-updater.exe';
}

function isServiceXmlPath(relativePath) {
  const normalized = String(relativePath || '').replace(/\\/g, '/').toLowerCase();
  return normalized === 'winsw-agent.xml' ||
    normalized === 'winsw-updater.xml' ||
    normalized === 'service/winsw-agent.xml' ||
    normalized === 'service/winsw-updater.xml';
}

function listFilesRecursive(baseDir, prefix = '') {
  const files = [];
  if (!fs.existsSync(baseDir)) return files;
  for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
    const relative = prefix ? path.join(prefix, entry.name) : entry.name;
    const full = path.join(baseDir, entry.name);
    if (entry.isDirectory()) files.push(...listFilesRecursive(full, relative));
    else files.push(relative);
  }
  return files;
}

function copyDirectorySafe(source, destination, options = {}) {
  fs.mkdirSync(destination, { recursive: true });
  const warnings = [];
  let copied = 0;
  let skipped = 0;
  const files = listFilesRecursive(source);
  const progressEvery = Number(options.progressEvery || 1000);
  for (const relative of files) {
    if (options.preserveRuntimePaths && shouldPreserveRuntimePath(relative)) {
      skipped += 1;
      continue;
    }
    if (options.protectServiceWrappers && isServiceWrapperPath(relative)) {
      skipped += 1;
      warnings.push({ type: 'service_wrapper_skipped', file: relative });
      continue;
    }
    if (options.serviceXmlOnly && !isServiceXmlPath(relative)) {
      skipped += 1;
      continue;
    }
    const sourcePath = path.join(source, relative);
    const destinationPath = path.join(destination, relative);
    try {
      fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
      fs.copyFileSync(sourcePath, destinationPath);
      copied += 1;
      if (options.job && options.saveJob && progressEvery > 0 && copied % progressEvery === 0) {
        log(options.job, options.progressMessage || 'replace_progress', { copied, skipped });
      }
    } catch (error) {
      const warning = { type: 'locked_file_skipped', file: relative, error: error.message, code: error.code || null };
      warnings.push(warning);
      skipped += 1;
      if (!options.tolerateLockedFiles || isServiceXmlPath(relative)) {
        const err = new Error(options.errorMessage || 'runtime_copy_failed');
        err.details = { file: relative, warning, copied, skipped };
        throw err;
      }
    }
  }
  return { ok: true, copied, skipped, warnings };
}

function copyRuntimeOverlay(source, destination, manifest = {}, context = {}) {
  const allowServiceWrapperUpdate = manifest.allowServiceWrapperUpdate === true;
  return copyDirectorySafe(source, destination, {
    preserveRuntimePaths: true,
    protectServiceWrappers: !allowServiceWrapperUpdate,
    tolerateLockedFiles: !!context.tolerateLockedFiles,
    job: context.job,
    saveJob: context.saveJob,
    progressMessage: 'replace_progress',
  });
}

function createRuntimeZipBackup(job, currentVersion) {
  const backupRoot = path.join(BACKUPS_DIR, `${cleanVersion(currentVersion)}-${stamp()}`);
  const appBackup = path.join(backupRoot, 'arkhon-app');
  const updaterBackup = path.join(backupRoot, 'arkhon-updater');
  const serviceBackup = path.join(backupRoot, 'service');
  fs.mkdirSync(backupRoot, { recursive: true });
  if (fs.existsSync(appRoot())) fs.cpSync(appRoot(), appBackup, { recursive: true, force: true, errorOnExist: false });
  if (fs.existsSync(updaterRoot())) fs.cpSync(updaterRoot(), updaterBackup, { recursive: true, force: true, errorOnExist: false });
  if (fs.existsSync(serviceDir())) fs.cpSync(serviceDir(), serviceBackup, { recursive: true, force: true, errorOnExist: false });
  fs.writeFileSync(path.join(backupRoot, 'backup-manifest.json'), `${JSON.stringify({
    jobId: job.id,
    createdAt: reporter.now(),
    type: 'runtime-zip',
    version: currentVersion,
    appBackup,
    updaterBackup,
    serviceBackup,
    targets: { app: appRoot(), updater: updaterRoot(), service: serviceDir() },
  }, null, 2)}\n`, 'utf8');
  return { ok: true, path: backupRoot, appBackup, updaterBackup, serviceBackup, version: currentVersion };
}

function restoreRuntimeZipBackup(backup) {
  if (!backup || !backup.path) {
    const error = new Error('rollback_backup_missing');
    error.code = 'rollback_backup_missing';
    throw error;
  }
  const parts = [];
  if (backup.appBackup && fs.existsSync(backup.appBackup)) {
    parts.push({ name: 'app', ...copyDirectorySafe(backup.appBackup, appRoot(), { protectServiceWrappers: true, tolerateLockedFiles: true, errorMessage: 'rollback_app_restore_failed' }) });
  }
  if (backup.updaterBackup && fs.existsSync(backup.updaterBackup)) {
    parts.push({ name: 'updater', ...copyDirectorySafe(backup.updaterBackup, updaterRoot(), { protectServiceWrappers: true, tolerateLockedFiles: true, errorMessage: 'rollback_updater_restore_failed' }) });
  }
  if (backup.serviceBackup && fs.existsSync(backup.serviceBackup)) {
    parts.push({ name: 'service_xml', ...copyDirectorySafe(backup.serviceBackup, serviceDir(), { protectServiceWrappers: true, serviceXmlOnly: true, tolerateLockedFiles: true, errorMessage: 'rollback_service_restore_failed' }) });
  }
  const warnings = parts.flatMap((part) => part.warnings || []);
  return { ok: true, restoredFrom: backup.path, parts, warnings };
}

async function rollbackRuntimeZip(job, saveJob, backup) {
  setStatus(job, saveJob, 'rollback_started');
  try {
    const rollback = restoreRuntimeZipBackup(backup);
    job.rollback = { attempted: true, ok: true, ...rollback };
    setStatus(job, saveJob, rollback.warnings && rollback.warnings.length ? 'rollback_completed_with_warnings' : 'rollback_completed');
    return job.rollback;
  } catch (rollbackError) {
    job.rollback = { attempted: true, ok: false, detail: reporter.serializeError(rollbackError) };
    setStatus(job, saveJob, 'rollback_failed');
    return job.rollback;
  }
}

async function stopWindowsService(name) {
  if (process.platform !== 'win32') return { name, skipped: true, reason: 'non_windows' };
  const result = await runCommand('sc.exe', ['stop', name], { timeoutMs: 30000, timeoutError: 'service_stop_timeout' });
  return { name, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
}

async function startWindowsService(name) {
  if (process.platform !== 'win32') return { name, skipped: true, reason: 'non_windows' };
  const result = await runCommand('sc.exe', ['start', name], { timeoutMs: 30000, timeoutError: 'service_start_timeout' });
  if (result.exitCode !== 0 && !/already been started|ya se ha iniciado/i.test(result.stdout)) {
    const err = new Error('service_start_failed');
    err.details = { service: name, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
    throw err;
  }
  return { name, exitCode: result.exitCode, stdout: result.stdout };
}

async function stopAgentService() {
  const name = await resolveWindowsService(AGENT_SERVICE_CANDIDATES);
  return stopWindowsService(name);
}

async function startAgentService() {
  const name = await resolveWindowsService(AGENT_SERVICE_CANDIDATES);
  return startWindowsService(name);
}

async function waitForPortFree(port, timeoutMs = 30000) {
  if (process.platform !== 'win32') return { port, skipped: true, reason: 'non_windows' };
  const deadline = Date.now() + timeoutMs;
  let lastPids = [];
  while (Date.now() <= deadline) {
    const result = await runCommand('netstat.exe', ['-ano'], { timeoutMs: 10000, timeoutError: 'netstat_timeout' });
    const pids = [];
    const re = new RegExp(`^\\s*TCP\\s+\\S*[:.]${port}\\s+\\S+\\s+LISTENING\\s+(\\d+)\\s*$`, 'gim');
    let match;
    while ((match = re.exec(result.stdout))) pids.push(match[1]);
    lastPids = [...new Set(pids)];
    if (!lastPids.length) return { port, free: true };
    await wait(1000);
  }
  const terminated = await terminateCanonicalRuntimeProcesses(lastPids);
  if (terminated.length) {
    await wait(2000);
    const retry = await runCommand('netstat.exe', ['-ano'], { timeoutMs: 10000, timeoutError: 'netstat_timeout' });
    if (!new RegExp(`^\\s*TCP\\s+\\S*[:.]${port}\\s+\\S+\\s+LISTENING\\s+\\d+\\s*$`, 'im').test(retry.stdout)) {
      return { port, free: true, terminated };
    }
  }
  const err = new Error('port_not_released');
  err.details = { port, pids: lastPids, terminated };
  throw err;
}

async function describeWindowsProcess(pid) {
  const script = [
    `$p=Get-CimInstance Win32_Process -Filter "ProcessId=${Number(pid) || 0}" -ErrorAction SilentlyContinue`,
    'if($p){ [pscustomobject]@{ ProcessId=$p.ProcessId; ParentProcessId=$p.ParentProcessId; ExecutablePath=$p.ExecutablePath; CommandLine=$p.CommandLine } | ConvertTo-Json -Compress }',
  ].join('; ');
  const result = await runCommand('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], {
    timeoutMs: 10000,
    timeoutError: 'process_inspect_timeout',
  });
  if (result.exitCode !== 0 || !result.stdout.trim()) return null;
  try { return JSON.parse(result.stdout); } catch (_) { return null; }
}

function sameWindowsPath(a, b) {
  return String(a || '').replace(/\//g, '\\').toLowerCase() === String(b || '').replace(/\//g, '\\').toLowerCase();
}

async function terminateCanonicalRuntimeProcesses(pids) {
  const terminated = [];
  const runtimeExe = canonicalRuntimeExecutable();
  const appIndex = path.join(appRoot(), 'index.js');
  for (const pid of [...new Set(pids)].filter(Boolean)) {
    const proc = await describeWindowsProcess(pid).catch(() => null);
    if (!proc) continue;
    const commandLine = String(proc.CommandLine || '');
    const isRuntime = sameWindowsPath(proc.ExecutablePath, runtimeExe);
    const isAgentEntrypoint = commandLine.toLowerCase().includes(appIndex.toLowerCase());
    if (!isRuntime || !isAgentEntrypoint) continue;
    const result = await runCommand('taskkill.exe', ['/PID', String(pid), '/T', '/F'], {
      timeoutMs: 15000,
      timeoutError: 'agent_child_kill_timeout',
    });
    terminated.push({ pid: Number(pid), executablePath: proc.ExecutablePath, exitCode: result.exitCode });
  }
  return terminated;
}

async function extractZip(zipPath, destination, job = null) {
  fs.rmSync(destination, { recursive: true, force: true });
  fs.mkdirSync(destination, { recursive: true });
  log(job, 'extraction_started', { zipPath, destination });
  const timeoutMs = Number(process.env.ARKHON_RUNTIME_ZIP_EXTRACT_TIMEOUT_MS || 15 * 60 * 1000);
  const result = await runCommand('tar.exe', ['-xf', zipPath, '-C', destination], {
    timeoutMs,
    timeoutError: 'runtime_zip_extract_timeout',
  });
  if (result.exitCode !== 0) {
    const err = new Error('runtime_zip_extract_failed');
    err.details = result;
    throw err;
  }
  const files = listFilesRecursive(destination).length;
  log(job, 'extraction_completed', { zipPath, destination, files });
  return { ok: true, destination, files };
}

async function verifySha256Sums(extractDir, context = {}) {
  const sumsPath = path.join(extractDir, 'sha256sums.json');
  const sums = readJson(sumsPath);
  if (!sums || typeof sums !== 'object') throw new Error('runtime_zip_sha256sums_missing');
  const mismatches = [];
  const entries = Object.entries(sums);
  const progressEvery = Number(context.progressEvery || 500);
  let checked = 0;
  for (const [relativePath, expected] of entries) {
    const filePath = path.join(extractDir, relativePath);
    if (!fs.existsSync(filePath)) {
      mismatches.push({ file: relativePath, expected, actual: 'missing' });
      checked += 1;
      continue;
    }
    const actual = await sha256FileAsync(filePath);
    if (String(actual).toLowerCase() !== String(expected).toLowerCase()) mismatches.push({ file: relativePath, expected, actual });
    checked += 1;
    if (context.job && context.saveJob && progressEvery > 0 && checked % progressEvery === 0) {
      context.job.verifyProgress = { checked, total: entries.length };
      context.saveJob(context.job);
      log(context.job, 'staging_verify_progress', { checked, total: entries.length });
      await wait(0);
    }
  }
  if (mismatches.length) {
    const err = new Error('runtime_zip_sha256_failed');
    err.details = { mismatches };
    throw err;
  }
  if (context.job && context.saveJob) {
    context.job.verifyProgress = { checked, total: entries.length };
    context.saveJob(context.job);
  }
  return { ok: true, checked: entries.length };
}

function serviceXmlNeedsUpdate(xmlPath, definition) {
  if (!fs.existsSync(xmlPath)) return true;
  const current = fs.readFileSync(xmlPath, 'utf8');
  return !current.includes(canonicalRuntimeExecutable()) ||
    !current.includes(definition.script) ||
    !current.includes(definition.workingDirectory);
}

function writeWinSwXml(xmlPath, definition) {
  fs.mkdirSync(path.dirname(xmlPath), { recursive: true });
  const xmlEscape = (value) => String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  const envXml = Object.entries(definition.env || {})
    .map(([name, value]) => `  <env name="${xmlEscape(name)}" value="${xmlEscape(value)}"/>`)
    .join('\n');
  fs.writeFileSync(xmlPath, [
    '<service>',
    `  <id>${xmlEscape(definition.name)}</id>`,
    `  <name>${xmlEscape(definition.name)}</name>`,
    `  <description>${xmlEscape(definition.description)}</description>`,
    `  <executable>${xmlEscape(canonicalRuntimeExecutable())}</executable>`,
    `  <arguments>--max-old-space-size=512 "${xmlEscape(definition.script)}"</arguments>`,
    `  <workingdirectory>${xmlEscape(definition.workingDirectory)}</workingdirectory>`,
    `  <logpath>${xmlEscape(definition.logDir)}</logpath>`,
    '  <log mode="roll-by-size-time">',
    '    <sizeThreshold>10485760</sizeThreshold>',
    '    <pattern>yyyyMMdd</pattern>',
    '    <autoRollAtTime>00:00:00</autoRollAtTime>',
    '    <zipOlderThanNumDays>7</zipOlderThanNumDays>',
    '  </log>',
    '  <stoptimeout>30sec</stoptimeout>',
    '  <onfailure action="restart" delay="5 sec"/>',
    '  <onfailure action="restart" delay="10 sec"/>',
    '  <onfailure action="restart" delay="30 sec"/>',
    envXml,
    '</service>',
    '',
  ].join('\n'), 'utf8');
}

function updateServiceXml() {
  const agentLogDir = path.join(AGENT_DATA_DIR, 'logs');
  const updaterLogDir = path.join(DATA_DIR, 'logs');
  const agentDefinition = {
    name: 'ARKHON Agent',
    description: 'ARKHON Monitor Connector - servicio local offline-first',
    script: path.join(appRoot(), 'index.js'),
    workingDirectory: appRoot(),
    logDir: agentLogDir,
    env: {
      NODE_ENV: 'production',
      ARKHON_SERVICE: '1',
      CONFIG_PATH: path.join(AGENT_DATA_DIR, 'config'),
      ARKHON_AGENT_DATA_DIR: AGENT_DATA_DIR,
      ARKHON_AGENT_LOG_DIR: agentLogDir,
      ELECTRON_RUN_AS_NODE: '1',
    },
  };
  const updaterDefinition = {
    name: 'ARKHON Updater',
    description: 'ARKHON Monitor Updater - supervisor local de actualizaciones',
    script: path.join(updaterRoot(), 'index.js'),
    workingDirectory: updaterRoot(),
    logDir: updaterLogDir,
    env: {
      NODE_ENV: 'production',
      ARKHON_UPDATER_SERVICE: '1',
      ARKHON_UPDATER_HOST: '127.0.0.1',
      ARKHON_UPDATER_PORT: '34567',
      ARKHON_AGENT_DATA_DIR: AGENT_DATA_DIR,
      ARKHON_UPDATER_DATA_DIR: DATA_DIR,
      ARKHON_UPDATER_LOG_DIR: updaterLogDir,
      CONFIG_PATH: path.join(AGENT_DATA_DIR, 'config'),
      ELECTRON_RUN_AS_NODE: '1',
    },
  };
  const agentXml = path.join(serviceDir(), 'winsw-agent.xml');
  const updaterXml = path.join(serviceDir(), 'winsw-updater.xml');
  const updated = [];
  if (serviceXmlNeedsUpdate(agentXml, agentDefinition)) {
    writeWinSwXml(agentXml, agentDefinition);
    updated.push(agentXml);
  }
  if (serviceXmlNeedsUpdate(updaterXml, updaterDefinition)) {
    writeWinSwXml(updaterXml, updaterDefinition);
    updated.push(updaterXml);
  }
  return { ok: true, runtime: canonicalRuntimeExecutable(), serviceDir: serviceDir(), updated };
}

function runtimeRoots(app = appRoot()) {
  return { app, updater: updaterRoot(), service: path.join(app, 'service') };
}

function filterDeltaManifest(deltaManifest, prefix) {
  return {
    ...deltaManifest,
    changed: deltaManifest.changed.filter((entry) => String(entry.path).replace(/\\/g, '/').startsWith(prefix)),
    deletions: deltaManifest.deletions.filter((relative) => String(relative).replace(/\\/g, '/').startsWith(prefix)),
  };
}

async function verifyRuntimeTargetIndex(targetIndex, roots, context = {}) {
  const entries = Object.entries(runtimeDelta.runtimeIndex(targetIndex));
  const mismatches = [];
  let checked = 0;
  for (const [relative, expected] of entries) {
    const resolved = runtimeDelta.resolveRuntimePath(relative, roots);
    if (!fs.existsSync(resolved.target)) {
      mismatches.push({ file: relative, expected, actual: 'missing' });
    } else {
      const actual = await sha256FileAsync(resolved.target);
      if (String(actual).toLowerCase() !== String(expected).toLowerCase()) mismatches.push({ file: relative, expected, actual });
    }
    checked += 1;
    if (context.job && context.saveJob && checked % 500 === 0) {
      context.job.targetVerifyProgress = { checked, total: entries.length };
      context.saveJob(context.job);
      await wait(0);
    }
  }
  if (mismatches.length) throw Object.assign(new Error('runtime_target_index_mismatch'), { details: { mismatches: mismatches.slice(0, 100) } });
  return { ok: true, checked };
}

function removeUnexpectedRootFiles(rootPath, targetIndex, prefix) {
  const expected = new Set(Object.keys(runtimeDelta.runtimeIndex(targetIndex))
    .filter((relative) => relative.replace(/\\/g, '/').startsWith(prefix))
    .map((relative) => relative.replace(/\\/g, '/').slice(prefix.length).toLowerCase()));
  let removed = 0;
  for (const relative of listFilesRecursive(rootPath)) {
    const normalized = relative.replace(/\\/g, '/');
    if (expected.has(normalized.toLowerCase()) || shouldPreserveRuntimePath(normalized) || isServiceWrapperPath(normalized)) continue;
    fs.rmSync(path.join(rootPath, relative), { force: true });
    removed += 1;
  }
  return removed;
}

async function applyFullRuntimeAb(job, saveJob, manifest, zipPath, actualSha, current, extractDir, packageManifest, staging) {
  const targetIndex = runtimeDelta.runtimeIndex(readJson(path.join(extractDir, 'sha256sums.json'), {}));
  setStatus(job, saveJob, 'preparing_inactive_slot');
  const candidate = runtimeSlots.prepareCandidate(DATA_DIR, appRoot(), manifest.version);
  const appCopy = copyRuntimeOverlay(path.join(extractDir, 'arkhon-app'), candidate.candidate, packageManifest, { job, saveJob });
  const removedApp = removeUnexpectedRootFiles(candidate.candidate, targetIndex, 'arkhon-app/');
  const candidateIndex = Object.fromEntries(Object.entries(targetIndex).filter(([relative]) => relative.replace(/\\/g, '/').startsWith('arkhon-app/')));
  await verifyRuntimeTargetIndex(candidateIndex, runtimeRoots(candidate.candidate), { job, saveJob });

  const updaterAffected = [...new Set([
    ...listFilesRecursive(updaterRoot()).map((relative) => `arkhon-updater/${relative.replace(/\\/g, '/')}`),
    ...Object.keys(targetIndex).filter((relative) => relative.replace(/\\/g, '/').startsWith('arkhon-updater/')),
  ])];
  const updaterBackup = runtimeDelta.createSelectiveBackup(
    path.join(BACKUPS_DIR, `${cleanVersion(current)}-${stamp()}-ab`),
    { changed: updaterAffected.map((relative) => ({ path: relative })), deletions: [] },
    runtimeRoots()
  );
  job.backup = updaterBackup;
  saveJob(job);
  let slotState = null;
  try {
    setStatus(job, saveJob, 'stopping_agent');
    await stopAgentService().catch((error) => ({ error: error.message, details: error.details }));
    await waitForPortFree(3001);
    setStatus(job, saveJob, 'switching_runtime_slot');
    slotState = runtimeSlots.activateCandidate(DATA_DIR, appRoot(), candidate.candidate, current, manifest.version);
    const updaterCopy = copyRuntimeOverlay(path.join(extractDir, 'arkhon-updater'), updaterRoot(), packageManifest, { job, saveJob, tolerateLockedFiles: true });
    const removedUpdater = removeUnexpectedRootFiles(updaterRoot(), targetIndex, 'arkhon-updater/');
    const serviceXml = updateServiceXml();
    setStatus(job, saveJob, 'starting_agent');
    await startAgentService();
    setStatus(job, saveJob, 'health_check');
    const validation = await validateInstall(job, manifest);
    const healthySlot = runtimeSlots.markHealthy(DATA_DIR, slotState);
    const installed = runtimeDelta.writeInstalledIndex(DATA_DIR, manifest.version, CURRENT_ARCH, targetIndex);
    return {
      ok: true,
      packageType: 'runtime-zip',
      zipPath,
      sha256: actualSha,
      backup: updaterBackup,
      validation,
      staging,
      replace: { app: { mode: 'ab-slot', ...appCopy, removed: removedApp }, updater: { ...updaterCopy, removed: removedUpdater }, serviceXml },
      slot: healthySlot,
      installedIndex: { version: installed.version, indexSha256: installed.indexSha256 },
      selfUpdateRestartDeferred: true,
    };
  } catch (error) {
    if (slotState) {
      try {
        await stopAgentService().catch(() => {});
        await waitForPortFree(3001).catch(() => {});
        runtimeSlots.rollbackSlot(DATA_DIR, slotState);
      } catch (rollbackError) {
        error.details = { ...(error.details || {}), slotRollback: reporter.serializeError(rollbackError) };
      }
    }
    try { runtimeDelta.restoreSelectiveBackup(updaterBackup, runtimeRoots()); } catch (rollbackError) {
      error.details = { ...(error.details || {}), selectiveRollback: reporter.serializeError(rollbackError) };
    }
    try { await startAgentService(); } catch (_) {}
    throw error;
  }
}

async function applyRuntimeDelta(job, saveJob, manifest, zipPath, actualSha, current, extractDir, packageManifest, staging) {
  const deltaManifest = readJson(path.join(extractDir, packageManifest.deltaManifest || 'delta-manifest.json'));
  const targetIndex = readJson(path.join(extractDir, packageManifest.targetIndex || 'target-sha256sums.json'));
  const installedIndex = runtimeDelta.readInstalledIndex(DATA_DIR);
  runtimeDelta.validateDeltaManifest(deltaManifest, packageManifest, current, CURRENT_ARCH, installedIndex);
  if (!targetIndex || deltaManifest.targetIndexSha256 !== runtimeDelta.indexDigest(targetIndex)) {
    throw new Error('runtime_delta_target_index_invalid');
  }
  if (job.request.dryRun || job.request.apply === false) {
    return { ok: true, skipped: true, reason: 'dry_run', packageType: 'runtime-delta', zipPath, sha256: actualSha, staging };
  }

  setStatus(job, saveJob, 'preparing_inactive_slot');
  const candidate = runtimeSlots.prepareCandidate(DATA_DIR, appRoot(), manifest.version);
  const appDelta = filterDeltaManifest(deltaManifest, 'arkhon-app/');
  runtimeDelta.applyDelta(extractDir, appDelta, runtimeRoots(candidate.candidate), shouldPreserveRuntimePath);
  const candidateIndex = Object.fromEntries(Object.entries(targetIndex).filter(([relative]) => relative.replace(/\\/g, '/').startsWith('arkhon-app/')));
  await verifyRuntimeTargetIndex(candidateIndex, runtimeRoots(candidate.candidate), { job, saveJob });

  const nonAppDelta = {
    ...deltaManifest,
    changed: deltaManifest.changed.filter((entry) => !String(entry.path).replace(/\\/g, '/').startsWith('arkhon-app/')),
    deletions: deltaManifest.deletions.filter((relative) => !String(relative).replace(/\\/g, '/').startsWith('arkhon-app/')),
  };
  const backupRoot = path.join(BACKUPS_DIR, `${cleanVersion(current)}-${stamp()}-delta`);
  const selectiveBackup = runtimeDelta.createSelectiveBackup(backupRoot, nonAppDelta, runtimeRoots());
  job.backup = selectiveBackup;
  saveJob(job);
  let slotState = null;
  try {
    setStatus(job, saveJob, 'stopping_agent');
    await stopAgentService().catch((error) => ({ error: error.message, details: error.details }));
    await waitForPortFree(3001);
    setStatus(job, saveJob, 'switching_runtime_slot');
    slotState = runtimeSlots.activateCandidate(DATA_DIR, appRoot(), candidate.candidate, current, manifest.version);
    const nonAppApply = runtimeDelta.applyDelta(extractDir, nonAppDelta, runtimeRoots(), shouldPreserveRuntimePath);
    const serviceXml = updateServiceXml();
    setStatus(job, saveJob, 'starting_agent');
    await startAgentService();
    setStatus(job, saveJob, 'health_check');
    const validation = await validateInstall(job, manifest);
    const healthySlot = runtimeSlots.markHealthy(DATA_DIR, slotState);
    const installed = runtimeDelta.writeInstalledIndex(DATA_DIR, manifest.version, CURRENT_ARCH, targetIndex);
    return {
      ok: true,
      packageType: 'runtime-delta',
      zipPath,
      sha256: actualSha,
      backup: selectiveBackup,
      validation,
      staging,
      replace: { app: { mode: 'ab-slot', ...candidate }, nonApp: nonAppApply, serviceXml },
      slot: healthySlot,
      installedIndex: { version: installed.version, indexSha256: installed.indexSha256 },
    };
  } catch (error) {
    if (slotState) {
      try {
        await stopAgentService().catch(() => {});
        await waitForPortFree(3001).catch(() => {});
        runtimeSlots.rollbackSlot(DATA_DIR, slotState);
      } catch (rollbackError) {
        error.details = { ...(error.details || {}), slotRollback: reporter.serializeError(rollbackError) };
      }
    }
    try { runtimeDelta.restoreSelectiveBackup(selectiveBackup, runtimeRoots()); } catch (rollbackError) {
      error.details = { ...(error.details || {}), selectiveRollback: reporter.serializeError(rollbackError) };
    }
    try { await startAgentService(); } catch (_) {}
    throw error;
  }
}

async function applyRuntimeZip(job, saveJob, manifest, zipPath, actualSha, current) {
  const extractDir = path.join(DATA_DIR, 'extracted', `${cleanVersion(manifest.version)}-${stamp()}`);
  setStatus(job, saveJob, 'extracting_to_staging');
  const extraction = await extractZip(zipPath, extractDir, job);
  const packageManifest = readJson(path.join(extractDir, 'manifest.json'));
  if (!packageManifest || cleanVersion(packageManifest.version) !== cleanVersion(manifest.version)) {
    const err = new Error('runtime_zip_manifest_invalid');
    err.details = { expectedVersion: manifest.version, packageManifest };
    throw err;
  }
  if (Number(packageManifest.schemaVersion || 1) >= 2) {
    const componentManifest = readJson(path.join(extractDir, packageManifest.componentManifest || 'component-manifest.json'));
    const componentIds = Array.isArray(componentManifest?.components) ? componentManifest.components.map((component) => component.id) : [];
    if (!componentManifest || cleanVersion(componentManifest.version) !== cleanVersion(manifest.version) || componentManifest.arch !== CURRENT_ARCH || !componentIds.includes('app') || !componentIds.includes('updater')) {
      const err = new Error('runtime_component_manifest_invalid');
      err.details = { expectedVersion: manifest.version, expectedArch: CURRENT_ARCH, componentManifest };
      throw err;
    }
    job.componentManifest = componentManifest;
    saveJob(job);
  }

  setStatus(job, saveJob, 'verifying_staging_sha256sums');
  log(job, 'staging_verify_started', { extractDir });
  const stagingValidation = await verifySha256Sums(extractDir, { job, saveJob });
  log(job, 'staging_verify_completed', { extractDir, checked: stagingValidation.checked });

  const staging = { extractDir, extraction, validation: stagingValidation };
  if (packageManifest.packageMode === 'delta') {
    return applyRuntimeDelta(job, saveJob, manifest, zipPath, actualSha, current, extractDir, packageManifest, staging);
  }

  if (job.request.dryRun || job.request.apply === false) {
    return {
      ok: true,
      skipped: true,
      reason: 'dry_run',
      packageType: 'runtime-zip',
      zipPath,
      sha256: actualSha,
      staging,
    };
  }

  if (packageManifest.activationMode === 'ab') {
    return applyFullRuntimeAb(job, saveJob, manifest, zipPath, actualSha, current, extractDir, packageManifest, staging);
  }

  setStatus(job, saveJob, 'preparing_backup');
  const backup = createRuntimeZipBackup(job, current);
  job.backup = backup;
  saveJob(job);

  try {
    setStatus(job, saveJob, 'stopping_agent');
    await stopAgentService().catch((error) => ({ error: error.message, details: error.details }));
    await waitForPortFree(3001);

    setStatus(job, saveJob, 'replacing_runtime');
    log(job, 'replace_started', { target: 'arkhon-app' });
    const appCopy = copyRuntimeOverlay(path.join(extractDir, 'arkhon-app'), appRoot(), packageManifest, { job, saveJob });
    log(job, 'replace_completed', { target: 'arkhon-app', copied: appCopy.copied, skipped: appCopy.skipped, warnings: appCopy.warnings });
    log(job, 'replace_started', { target: 'arkhon-updater' });
    const updaterCopy = copyRuntimeOverlay(path.join(extractDir, 'arkhon-updater'), updaterRoot(), packageManifest, { job, saveJob, tolerateLockedFiles: true });
    log(job, 'replace_completed', { target: 'arkhon-updater', copied: updaterCopy.copied, skipped: updaterCopy.skipped, warnings: updaterCopy.warnings });
    const pendingSelfUpdate = updaterCopy.warnings && updaterCopy.warnings.length ? {
      required: true,
      reason: 'updater_files_locked_or_skipped',
      warnings: updaterCopy.warnings,
    } : null;
    if (pendingSelfUpdate) {
      fs.writeFileSync(path.join(DATA_DIR, 'pending-self-update.json'), `${JSON.stringify({
        createdAt: reporter.now(),
        jobId: job.id,
        version: manifest.version,
        ...pendingSelfUpdate,
      }, null, 2)}\n`, 'utf8');
    }

    setStatus(job, saveJob, 'updating_service_xml_if_needed');
    const serviceXml = updateServiceXml();

    setStatus(job, saveJob, 'starting_agent');
    await startAgentService();
    // Self-update note: the updater process keeps running current code and new files are used after service restart.

    setStatus(job, saveJob, 'health_check');
    const validation = await validateInstall(job, manifest);
    const targetIndex = runtimeDelta.runtimeIndex(readJson(path.join(extractDir, 'sha256sums.json'), {}));
    const installedIndex = runtimeDelta.writeInstalledIndex(DATA_DIR, manifest.version, CURRENT_ARCH, targetIndex);
    return { ok: true, packageType: 'runtime-zip', zipPath, sha256: actualSha, backup, validation, staging, replace: { app: appCopy, updater: updaterCopy, serviceXml, pendingSelfUpdate }, installedIndex: { version: installedIndex.version, indexSha256: installedIndex.indexSha256 }, selfUpdateRestartDeferred: true };
  } catch (error) {
    const rollback = await rollbackRuntimeZip(job, saveJob, backup);
    try { await startAgentService(); } catch (_) {}
    error.details = { ...(error.details || {}), backup, rollback };
    throw error;
  }
}

function updateActionRequiresLicense(job) {
  return job.request.dryRun || job.request.apply !== false;
}

async function validateLicense(job, manifest) {
  const credentials = readAgentCredentials();
  const licenseUrl = String(
    job.request.licenseUrl ||
    process.env.ARKHON_UPDATE_LICENSE_URL ||
    process.env.ARKHON_LICENSE_STATUS_URL ||
    (credentials.cloudUrl ? `${credentials.cloudUrl}/api/license/validate` : '') ||
    ''
  ).trim();
  const snapshot = activationSnapshot();
  const mustValidate = updateActionRequiresLicense(job);

  if (!licenseUrl) {
    if (!mustValidate) {
      return {
        ok: true,
        skipped: true,
        reason: 'license_validation_endpoint_not_configured',
        activation: snapshot,
      };
    }
    const err = new Error('license_validation_endpoint_not_configured');
    err.details = { credentials: maskedCredentialSummary(credentials), activation: snapshot };
    throw err;
  }

  const canFallbackAgentKey = !!(credentials.agentId && credentials.agentKey);
  if (!credentials.canSign && !canFallbackAgentKey && mustValidate) {
    const local = validateLocalLicenseForUpdate(snapshot);
    if (local.ok) return local;
    const err = new Error('license_validation_credentials_missing');
    err.details = { credentials: maskedCredentialSummary(credentials), activation: snapshot, localLicense: local };
    throw err;
  }

  const payload = JSON.stringify({
    product: 'arkhon-monitor',
    target_version: manifest.version,
    channel: manifest.channel,
    version: installedVersion(),
    installed_version: installedVersion(),
    hwid: credentials.hwid || snapshot.hwid || null,
    deviceId: credentials.deviceId || null,
    license_present: snapshot.hasKey,
    cliente_id: snapshot.cliente_id || null,
    plan: snapshot.plan || null,
    agentId: credentials.canSign ? undefined : credentials.agentId || null,
    agentKey: credentials.canSign ? undefined : credentials.agentKey || null,
    tenantId: credentials.tenantId || null,
    branchId: credentials.branchId || null,
    request: {
      module: 'updater',
      action: 'apply_update',
    },
  });

  const parsed = new URL(licenseUrl);
  const proto = parsed.protocol === 'https:' ? https : http;
  const headers = credentials.canSign
    ? authHeaders(credentials, { method: 'POST', path: parsed.pathname, body: payload })
    : {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(payload),
        'User-Agent': 'ARKHON-Updater-Service',
      };
  return new Promise((resolve, reject) => {
    const req = proto.request(parsed, {
      method: 'POST',
      headers,
    }, (res) => {
      let body = '';
      res.setEncoding('utf8');
      res.on('data', (chunk) => { body += chunk; });
      res.on('end', () => {
        let parsedBody = {};
        try { parsedBody = body ? JSON.parse(body) : {}; } catch (_) {}
        if (res.statusCode < 200 || res.statusCode >= 300) {
          const reason = parsedBody.reason || parsedBody.error || 'update_blocked_by_cloud';
          return reject(Object.assign(new Error(reason), { details: parsedBody, statusCode: res.statusCode }));
        }
        if (parsedBody.valid === false || parsedBody.allowed === false || parsedBody.ok === false) {
          const reason = parsedBody.reason || parsedBody.error || 'update_blocked_by_license';
          return reject(Object.assign(new Error(reason), { details: parsedBody, statusCode: res.statusCode }));
        }
        resolve({ ok: true, statusCode: res.statusCode, response: parsedBody, activation: snapshot, credentials: maskedCredentialSummary(credentials) });
      });
    });
    req.on('error', (error) => reject(Object.assign(new Error('update_validation_failed'), { cause: error })));
    req.setTimeout(15000, () => req.destroy(new Error('update_validation_failed_timeout')));
    req.write(payload);
    req.end();
  });
}

async function processUpdateJob(job, saveJob) {
  setStatus(job, saveJob, 'checking');
  const feedUrl = String(job.request.feedUrl || process.env.ARKHON_UPDATE_FEED_URL || 'https://cloud.arkhonmonitor.com/updates/monitor/windows/latest.json').trim();
  let manifest = job.request.manifest || await fetchManifest(feedUrl);
  manifest.type = manifest.type || (String(manifest.download_url || '').toLowerCase().endsWith('.zip') ? 'runtime-zip' : 'installer-exe');
  const current = cleanVersion(job.request.installedVersion || installedVersion());
  manifest = runtimeDelta.selectRuntimeArtifact(manifest, current, CURRENT_ARCH, runtimeDelta.readInstalledIndex(DATA_DIR));
  job.manifest = manifest;
  saveJob(job);

  if (manifest.arch && manifest.arch !== CURRENT_ARCH) {
    const err = new Error('runtime_arch_mismatch');
    err.details = { manifestArch: manifest.arch, currentArch: CURRENT_ARCH };
    throw err;
  }
  if (manifest.version && compareVersions(manifest.version, current) <= 0 && !job.request.force) {
    setStatus(job, saveJob, 'completed', {
      result: { ok: true, status: 'no_update', installedVersion: current, availableVersion: manifest.version },
    });
    return job;
  }

  setStatus(job, saveJob, 'available');
  if (!manifest.download_url) throw new Error('update_download_url_missing');
  if (manifest.type === 'installer-exe' && job.request.apply !== false && !job.request.dryRun && process.env.ARKHON_UPDATE_LEGACY_FALLBACK !== 'true' && job.request.allowInstallerExe !== true) {
    throw new Error('installer_exe_auto_update_disabled');
  }
  if (!['runtime-zip', 'runtime-delta', 'installer-exe'].includes(manifest.type)) {
    throw new Error('update_manifest_type_unsupported');
  }

  setStatus(job, saveJob, 'validating_license');
  const license = await validateLicense(job, manifest);
  job.licenseValidation = license;
  saveJob(job);

  setStatus(job, saveJob, 'downloading');
  const ext = ['runtime-zip', 'runtime-delta'].includes(manifest.type) ? '.zip' : (/\.exe(?:$|\?)/i.test(manifest.download_url) ? '.exe' : '.bin');
  const targetName = ['runtime-zip', 'runtime-delta'].includes(manifest.type)
    ? (manifest.fileName || `ARKHON-Runtime-Update-${manifest.version || 'latest'}${ext}`)
    : `ARKHON-Monitor-Setup-${manifest.version || 'latest'}${ext}`;
  const target = path.join(DOWNLOADS_DIR, targetName);
  let reusedVerifiedDownload = false;
  if (manifest.sha256 && fs.existsSync(target)) {
    const cachedSha = await sha256FileAsync(target);
    reusedVerifiedDownload = cachedSha.toLowerCase() === String(manifest.sha256).toLowerCase();
    if (!reusedVerifiedDownload) {
      try { fs.unlinkSync(target); } catch (_) {}
    }
  }
  if (!reusedVerifiedDownload) {
    await downloadFile(manifest.download_url, target, {
      onProgress: ({ receivedBytes, totalBytes, resumed }) => log(job, 'download_progress', { receivedBytes, totalBytes, resumed }),
    });
  } else {
    log(job, 'download_reused_verified', { target, sha256: manifest.sha256 });
  }

  setStatus(job, saveJob, 'verifying');
  const actualSha = sha256File(target);
  if (manifest.sha256 && actualSha.toLowerCase() !== String(manifest.sha256).toLowerCase()) {
    try { fs.unlinkSync(target); } catch (_) {}
    const err = new Error('update_integrity_failed');
    err.expected = manifest.sha256;
    err.actual = actualSha;
    throw err;
  }

  if (['runtime-zip', 'runtime-delta'].includes(manifest.type)) {
    const applied = await applyRuntimeZip(job, saveJob, manifest, target, actualSha, current);
    if (applied.skipped) {
      setStatus(job, saveJob, 'completed', {
        result: {
          ok: true,
          status: 'downloaded_verified',
          version: manifest.version,
          packageType: manifest.type,
          zipPath: target,
          sha256: actualSha,
          apply: applied,
        },
      });
      return job;
    }
    setStatus(job, saveJob, 'health_check');
    setStatus(job, saveJob, 'completed', {
      result: {
        ok: true,
        status: 'installed_verified',
        version: manifest.version,
        packageType: manifest.type,
        zipPath: target,
        sha256: actualSha,
        backup: applied.backup,
        validation: applied.validation,
        selfUpdateRestartDeferred: applied.selfUpdateRestartDeferred,
      },
    });
    return job;
  }

  let backup = null;
  if (!job.request.dryRun && job.request.apply !== false) {
    setStatus(job, saveJob, 'preparing_backup');
    backup = createApplyBackup(job, current);
    job.backup = backup;
    saveJob(job);
  }

  setStatus(job, saveJob, appliedState(job));
  if (!job.request.dryRun && job.request.apply !== false) setStatus(job, saveJob, 'waiting_installer');
  let applied;
  try {
    applied = await waitForInstaller(job, target);
  } catch (error) {
    const rollback = rollbackIfChanged(job, saveJob, backup, current);
    error.details = { ...(error.details || {}), backup, rollback };
    throw error;
  }
  if (applied.skipped) {
    setStatus(job, saveJob, 'completed', {
      result: {
        ok: true,
        status: 'downloaded_verified',
        version: manifest.version,
        installerPath: target,
        sha256: actualSha,
        apply: applied,
      },
    });
    return job;
  }

  setStatus(job, saveJob, 'validating_install');
  try {
    const validation = await validateInstall(job, manifest);
    setStatus(job, saveJob, 'completed', {
      result: {
        ok: true,
        status: 'installed_verified',
        version: manifest.version,
        installerPath: target,
        sha256: actualSha,
        backup,
        apply: applied,
        validation,
      },
    });
  } catch (error) {
    const serialized = reporter.serializeError(error);
    const rollback = rollbackIfChanged(job, saveJob, backup, current);
    const failed = new Error(serialized.message || 'version_not_updated');
    failed.details = { validation: serialized, backup, rollback };
    throw failed;
  }
  return job;
}

function appliedState(job) {
  return job.request.dryRun || job.request.apply === false ? 'applying' : 'launching_installer';
}

async function processJob(job, saveJob) {
  if (!lock.acquire()) {
    job.status = 'blocked';
    job.updatedAt = reporter.now();
    job.result = { ok: false, error: 'updater_busy' };
    saveJob(job);
    return job;
  }

  try {
    return await processUpdateJob(job, saveJob);
  } catch (error) {
    const serialized = reporter.serializeError(error);
    const code = serialized.message || serialized.code || 'update_apply_failed';
    const mapped = /integrity/i.test(code) ? 'update_integrity_failed'
      : /download/i.test(code) ? 'update_download_failed'
      : /license|plan|hwid|cloud|validation/i.test(code) ? code
      : /installer_exe_auto_update_disabled|update_manifest_type_unsupported|runtime_zip|runtime_component_manifest_invalid|port_not_released|service_start_failed|installer_exit_failed|installer_timeout|version_not_updated|health_check_failed|services_not_running|critical_config_missing|updater_status_failed|rollback_failed|rollback_backup_missing/i.test(code) ? code
      : 'update_apply_failed';
    job.status = 'failed';
    job.updatedAt = reporter.now();
    job.result = { ok: false, error: mapped, detail: serialized };
    if (error.expected || error.actual) job.result.sha256 = { expected: error.expected, actual: error.actual };
    saveJob(job);
    log(job, 'failed', { result: job.result });
    return job;
  } finally {
    lock.release();
  }
}

module.exports = {
  processJob,
  fetchManifest,
  compareVersions,
  installedVersion,
  normalizeManifest,
  downloadFile,
};
