'use strict';

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

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

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

function writeJsonAtomic(filePath, value) {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
  fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
  fs.renameSync(temporary, filePath);
}

function canonicalIndex(index = {}) {
  return Object.fromEntries(Object.entries(index).sort(([left], [right]) => left.localeCompare(right)));
}

function runtimeIndex(index = {}) {
  return Object.fromEntries(Object.entries(index || {}).filter(([relative]) => {
    const normalized = String(relative).replace(/\\/g, '/');
    return normalized.startsWith('arkhon-app/') || normalized.startsWith('arkhon-updater/') || normalized.startsWith('service/');
  }));
}

function indexDigest(index = {}) {
  return crypto.createHash('sha256').update(JSON.stringify(canonicalIndex(index))).digest('hex');
}

function installedIndexPath(dataDir) {
  return path.join(dataDir, 'installed-runtime-index.json');
}

function readInstalledIndex(dataDir) {
  return readJson(installedIndexPath(dataDir));
}

function writeInstalledIndex(dataDir, version, arch, files) {
  const document = {
    schemaVersion: 1,
    version: cleanVersion(version),
    arch,
    indexSha256: indexDigest(files),
    files: canonicalIndex(files),
    writtenAt: new Date().toISOString(),
  };
  writeJsonAtomic(installedIndexPath(dataDir), document);
  return document;
}

function validInstalledIndex(document, version, arch) {
  return !!document
    && cleanVersion(document.version) === cleanVersion(version)
    && document.arch === arch
    && document.files && typeof document.files === 'object'
    && document.indexSha256 === indexDigest(document.files);
}

function selectRuntimeArtifact(manifest, installedVersion, arch, installedIndex) {
  if (!manifest || manifest.type !== 'runtime-zip' || !validInstalledIndex(installedIndex, installedVersion, arch)) return manifest;
  const delta = (Array.isArray(manifest.deltas) ? manifest.deltas : []).find((entry) =>
    cleanVersion(entry.base_version || entry.baseVersion) === cleanVersion(installedVersion)
      && cleanVersion(entry.target_version || entry.targetVersion || manifest.version) === cleanVersion(manifest.version)
      && (!entry.arch || entry.arch === arch));
  if (!delta) return manifest;
  return {
    ...manifest,
    ...delta,
    version: manifest.version,
    type: 'runtime-delta',
    packageMode: 'delta',
    baseVersion: cleanVersion(installedVersion),
    download_url: delta.download_url || delta.downloadUrl,
    fileName: delta.fileName || delta.file_name,
    sha256: delta.sha256,
    size: delta.size,
    fullFallback: {
      type: manifest.type,
      download_url: manifest.download_url,
      fileName: manifest.fileName,
      sha256: manifest.sha256,
      size: manifest.size,
    },
  };
}

function safeRelative(relativePath) {
  const normalized = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
  if (!normalized || normalized.startsWith('/') || normalized.includes('../') || /^[a-zA-Z]:/.test(normalized)) {
    const error = new Error('runtime_delta_path_invalid');
    error.details = { path: relativePath };
    throw error;
  }
  return normalized;
}

function resolveRuntimePath(relativePath, roots) {
  const normalized = safeRelative(relativePath);
  const mappings = [
    ['arkhon-app/', roots.app],
    ['arkhon-updater/', roots.updater],
    ['service/', roots.service],
  ];
  const mapping = mappings.find(([prefix]) => normalized.startsWith(prefix));
  if (!mapping) throw Object.assign(new Error('runtime_delta_root_invalid'), { details: { path: relativePath } });
  const child = normalized.slice(mapping[0].length);
  const target = path.resolve(mapping[1], child);
  const root = path.resolve(mapping[1]);
  if (target !== root && !target.startsWith(`${root}${path.sep}`)) throw new Error('runtime_delta_path_escape');
  return { normalized, target, root: mapping[1], child };
}

function validateDeltaManifest(deltaManifest, packageManifest, installedVersion, arch, installedIndex) {
  if (!deltaManifest || deltaManifest.packageMode !== 'delta') throw new Error('runtime_delta_manifest_invalid');
  if (cleanVersion(deltaManifest.baseVersion) !== cleanVersion(installedVersion)
    || cleanVersion(deltaManifest.targetVersion) !== cleanVersion(packageManifest.version)
    || deltaManifest.arch !== arch) {
    throw Object.assign(new Error('runtime_delta_base_mismatch'), {
      details: { installedVersion, baseVersion: deltaManifest.baseVersion, targetVersion: deltaManifest.targetVersion, arch: deltaManifest.arch },
    });
  }
  if (!validInstalledIndex(installedIndex, installedVersion, arch)
    || deltaManifest.baseIndexSha256 !== indexDigest(installedIndex.files)) {
    throw new Error('runtime_delta_installed_index_mismatch');
  }
  if (!Array.isArray(deltaManifest.changed) || !Array.isArray(deltaManifest.deletions)) throw new Error('runtime_delta_manifest_invalid');
  for (const entry of deltaManifest.changed) safeRelative(entry.path);
  for (const relative of deltaManifest.deletions) safeRelative(relative);
  return true;
}

function createSelectiveBackup(backupRoot, deltaManifest, roots) {
  const filesRoot = path.join(backupRoot, 'files');
  const affected = [...new Set([
    ...deltaManifest.changed.map((entry) => entry.path),
    ...deltaManifest.deletions,
  ])];
  const entries = [];
  fs.mkdirSync(filesRoot, { recursive: true });
  for (const relative of affected) {
    const resolved = resolveRuntimePath(relative, roots);
    const existed = fs.existsSync(resolved.target) && fs.statSync(resolved.target).isFile();
    if (existed) {
      const backupPath = path.join(filesRoot, resolved.normalized);
      fs.mkdirSync(path.dirname(backupPath), { recursive: true });
      fs.copyFileSync(resolved.target, backupPath);
    }
    entries.push({ path: resolved.normalized, existed });
  }
  const manifest = { schemaVersion: 1, type: 'runtime-delta-selective', createdAt: new Date().toISOString(), entries };
  writeJsonAtomic(path.join(backupRoot, 'backup-manifest.json'), manifest);
  return { ok: true, path: backupRoot, filesRoot, entries };
}

function applyDelta(extractDir, deltaManifest, roots, shouldPreserve = () => false) {
  let copied = 0;
  let deleted = 0;
  let skipped = 0;
  for (const entry of deltaManifest.changed) {
    const resolved = resolveRuntimePath(entry.path, roots);
    if (shouldPreserve(resolved.child)) { skipped += 1; continue; }
    const source = path.join(extractDir, safeRelative(entry.path));
    if (!fs.existsSync(source)) throw Object.assign(new Error('runtime_delta_file_missing'), { details: { path: entry.path } });
    fs.mkdirSync(path.dirname(resolved.target), { recursive: true });
    const temporary = `${resolved.target}.${process.pid}.new`;
    fs.copyFileSync(source, temporary);
    fs.rmSync(resolved.target, { force: true });
    fs.renameSync(temporary, resolved.target);
    copied += 1;
  }
  for (const relative of deltaManifest.deletions) {
    const resolved = resolveRuntimePath(relative, roots);
    if (shouldPreserve(resolved.child)) { skipped += 1; continue; }
    if (fs.existsSync(resolved.target)) {
      fs.rmSync(resolved.target, { force: true });
      deleted += 1;
    }
  }
  return { ok: true, copied, deleted, skipped };
}

function restoreSelectiveBackup(backup, roots) {
  for (const entry of backup.entries || []) {
    const resolved = resolveRuntimePath(entry.path, roots);
    if (!entry.existed) {
      fs.rmSync(resolved.target, { force: true });
      continue;
    }
    const source = path.join(backup.filesRoot, safeRelative(entry.path));
    fs.mkdirSync(path.dirname(resolved.target), { recursive: true });
    fs.copyFileSync(source, resolved.target);
  }
  return { ok: true, restored: (backup.entries || []).length };
}

module.exports = {
  canonicalIndex,
  runtimeIndex,
  indexDigest,
  installedIndexPath,
  readInstalledIndex,
  writeInstalledIndex,
  validInstalledIndex,
  selectRuntimeArtifact,
  safeRelative,
  resolveRuntimePath,
  validateDeltaManifest,
  createSelectiveBackup,
  applyDelta,
  restoreSelectiveBackup,
};
