import {
  chmodSync,
  copyFileSync,
  existsSync,
  lstatSync,
  mkdirSync,
  readFileSync,
  readdirSync,
  rmSync,
  statSync,
} from 'node:fs';
import { createHash } from 'node:crypto';
import { computePackagePayloadHash, PACKAGE_MANIFEST_SCHEMA } from './updater-lifecycle-3.4.mjs';
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';

export const MANIFEST_SCHEMA = 'vendoo-release-manifest-v1';
export const MANIFEST_FILE = 'release-manifest.json';
export const CONTROL_RELEASE_FILES = Object.freeze([MANIFEST_FILE]);
export const REQUIRED_RELEASE_FILES = Object.freeze([
  'package.json',
  'package-lock.json',
  'bootstrap.mjs',
  'server.mjs',
  'lib/db.mjs',
  'db/schema.sql',
  'update-manifest.json',
  'UPDATE_VENDOO.bat',
  'scripts/updater/Vendoo.Updater.Preflight.ps1',
  'scripts/updater/Vendoo.Updater.UI.ps1',
  'scripts/updater/Vendoo.Updater.Host.ps1',
  'scripts/updater/Vendoo.Updater.Worker34.ps1',
  'tools/apply-release-manifest.mjs',
  'tools/updater-lifecycle-3.4.mjs',
  'tools/check-release-payload-3.4.mjs',
]);

const allowedEnvTemplates = new Set(['.env.example', '.env.docker.example']);
const skippedTopLevelDirs = new Set([
  '.git', 'node_modules', 'runtime', 'docker-data', 'uploads', 'backups', 'logs',
  'sessions', 'tmp', 'temp', '__pycache__', 'release-output', 'operations', 'updates',
  'modules', 'archiv', 'archive', 'archives', 'data',
]);
const skippedLocalAiDirs = new Set(['local-ai/comfyui', 'local-ai/downloads', 'local-ai/extract-temp']);
const skippedExactFiles = new Set([
  MANIFEST_FILE,
  'file_checksums_sha256.txt',
  '.vendoo-install-mode', '.vendoo-github-state.json',
  'pending-operation.json', 'last-operation.json', 'first-admin-code.txt',
]);
const skippedExtensions = new Set([
  '.db', '.sqlite', '.sqlite3', '.log', '.key', '.pem', '.pfx', '.p12', '.pyc',
]);

export function normalizeReleasePath(value) {
  return String(value).split('\\').join('/').replace(/^\.\//, '');
}

export function assertSafeReleasePath(value, label = 'Release-Pfad') {
  const rel = normalizeReleasePath(value);
  if (!rel || rel === '.' || isAbsolute(rel) || rel.startsWith('/') || /^[A-Za-z]:\//.test(rel)) {
    throw new Error(`${label} ist nicht relativ: ${value}`);
  }
  const parts = rel.split('/');
  if (parts.some(part => !part || part === '.' || part === '..')) {
    throw new Error(`${label} enthält unzulässige Segmente: ${value}`);
  }
  if (parts[0].toLowerCase() === '.git') {
    throw new Error(`${label} darf .git nicht verändern: ${value}`);
  }
  return rel;
}

export function shouldIncludeReleaseEntry(relativePath, stat) {
  const rel = normalizeReleasePath(relativePath);
  if (!rel) return false;
  const parts = rel.split('/');
  const top = parts[0].toLowerCase();
  const name = basename(rel);
  const lower = rel.toLowerCase();
  if (skippedTopLevelDirs.has(top)) return false;
  if (parts.some(part => part.toLowerCase() === '__pycache__')) return false;
  if ([...skippedLocalAiDirs].some(dir => lower === dir || lower.startsWith(`${dir}/`))) return false;
  if (/^\.env(?:\..+)?$/i.test(name) && !allowedEnvTemplates.has(rel)) return false;
  if (skippedExactFiles.has(name.toLowerCase())) return false;
  if (/\.(?:db|sqlite|sqlite3)-(?:wal|shm)$/i.test(name)) return false;
  if (skippedExtensions.has(extname(name).toLowerCase())) return false;
  if (stat.isSymbolicLink()) return false;
  return stat.isDirectory() || stat.isFile();
}

export function sha256File(filePath) {
  const hash = createHash('sha256');
  hash.update(readFileSync(filePath));
  return hash.digest('hex');
}

export function listReleaseFiles(root) {
  const base = resolve(root);
  const files = [];
  function walk(current) {
    for (const name of readdirSync(current).sort((a, b) => a.localeCompare(b, 'en'))) {
      const absolute = join(current, name);
      const rel = normalizeReleasePath(relative(base, absolute));
      const stat = lstatSync(absolute);
      if (!shouldIncludeReleaseEntry(rel, stat)) continue;
      if (stat.isDirectory()) walk(absolute);
      else files.push(rel);
    }
  }
  walk(base);
  return files;
}

export function buildReleaseManifest(root, { version, updaterVersion, removedFiles = [] } = {}) {
  if (!version) throw new Error('Release-Version fehlt.');
  if (!updaterVersion) throw new Error('Updater-Version fehlt.');
  const base = resolve(root);
  const files = listReleaseFiles(base).map(path => ({
    path,
    sha256: sha256File(join(base, path)),
    size: statSync(join(base, path)).size,
  }));
  const normalizedRemoved = [...new Set(removedFiles.map(path => assertSafeReleasePath(path, 'removedFiles-Eintrag')))].sort();
  const fileSet = new Set(files.map(entry => entry.path));
  for (const required of REQUIRED_RELEASE_FILES) {
    if (!fileSet.has(required)) throw new Error(`Pflichtdatei fehlt im Release-Payload: ${required}`);
  }
  for (const removed of normalizedRemoved) {
    if (fileSet.has(removed)) throw new Error(`Datei ist gleichzeitig Payload und Löschung: ${removed}`);
    if (CONTROL_RELEASE_FILES.includes(removed)) throw new Error(`Steuerdatei darf nicht als Löschung markiert werden: ${removed}`);
  }
  return {
    schema: MANIFEST_SCHEMA,
    version,
    updaterVersion,
    mode: 'overlay-with-explicit-removals',
    requiredFiles: [...REQUIRED_RELEASE_FILES],
    removedFiles: normalizedRemoved,
    files,
  };
}

export function readAndValidateManifest(sourceRoot, { allowExtraFiles = false } = {}) {
  const base = resolve(sourceRoot);
  const manifestPath = join(base, MANIFEST_FILE);
  if (!existsSync(manifestPath)) throw new Error(`${MANIFEST_FILE} fehlt.`);
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
  if (![MANIFEST_SCHEMA, PACKAGE_MANIFEST_SCHEMA].includes(manifest.schema)) throw new Error(`Unbekanntes Manifest-Schema: ${manifest.schema}`);
  if (manifest.mode !== 'overlay-with-explicit-removals') throw new Error(`Unzulässiger Release-Modus: ${manifest.mode}`);
  if (manifest.schema === PACKAGE_MANIFEST_SCHEMA) {
    if (manifest.targetVersion !== manifest.version) throw new Error('targetVersion und version des Paketmanifests stimmen nicht überein.');
    for (const field of ['sourceCommit', 'baseCommit']) {
      if (!/^[a-f0-9]{40}$/.test(String(manifest[field] || ''))) throw new Error(`Ungültige Paket-Provenienz: ${field}`);
    }
    const createdAt = Date.parse(String(manifest.createdAt || ''));
    if (!Number.isFinite(createdAt)) throw new Error('Ungültige Paket-Provenienz: createdAt');
    if (!/^[a-f0-9]{64}$/.test(String(manifest.payloadHash || ''))) throw new Error('Ungültige Paket-Provenienz: payloadHash');
    const expectedPayloadHash = computePackagePayloadHash(manifest);
    if (manifest.payloadHash !== expectedPayloadHash) throw new Error('Payload-Hash des Paketmanifests stimmt nicht.');
  }
  if (!Array.isArray(manifest.files) || manifest.files.length === 0) throw new Error('Release-Manifest enthält keine Dateien.');
  if (!Array.isArray(manifest.removedFiles)) throw new Error('removedFiles fehlt im Release-Manifest.');
  const seen = new Set();
  for (const entry of manifest.files) {
    if (!entry || typeof entry !== 'object') throw new Error('Ungültiger Datei-Eintrag im Release-Manifest.');
    const rel = assertSafeReleasePath(entry.path, 'Manifest-Dateipfad');
    if (seen.has(rel)) throw new Error(`Doppelter Manifest-Dateipfad: ${rel}`);
    seen.add(rel);
    if (!/^[a-f0-9]{64}$/.test(String(entry.sha256 || ''))) throw new Error(`Ungültige SHA-256-Prüfsumme: ${rel}`);
    if (!Number.isSafeInteger(entry.size) || entry.size < 0) throw new Error(`Ungültige Dateigröße: ${rel}`);
  }
  const removedSeen = new Set();
  for (const value of manifest.removedFiles) {
    const rel = assertSafeReleasePath(value, 'removedFiles-Eintrag');
    if (removedSeen.has(rel)) throw new Error(`Doppelter removedFiles-Eintrag: ${rel}`);
    removedSeen.add(rel);
    if (seen.has(rel)) throw new Error(`Datei ist gleichzeitig Payload und Löschung: ${rel}`);
    if (CONTROL_RELEASE_FILES.includes(rel)) throw new Error(`Steuerdatei darf nicht als Löschung markiert werden: ${rel}`);
  }
  const required = Array.isArray(manifest.requiredFiles) ? manifest.requiredFiles : REQUIRED_RELEASE_FILES;
  for (const value of REQUIRED_RELEASE_FILES) {
    if (!required.includes(value)) throw new Error(`Manifest-Pflichtdatei fehlt in requiredFiles: ${value}`);
    if (!seen.has(value)) throw new Error(`Manifest-Payload enthält Pflichtdatei nicht: ${value}`);
  }
  const actualFiles = listReleaseFiles(base);
  const actualSet = new Set(actualFiles);
  if (!allowExtraFiles) {
    for (const rel of actualFiles) if (!seen.has(rel)) throw new Error(`Sichere Paketdatei fehlt im Manifest: ${rel}`);
  }
  for (const rel of seen) if (!actualSet.has(rel)) throw new Error(`Manifest verweist auf fehlende Paketdatei: ${rel}`);
  for (const entry of manifest.files) {
    const filePath = join(base, entry.path);
    const stat = statSync(filePath);
    if (stat.size !== entry.size) throw new Error(`Dateigröße stimmt nicht: ${entry.path}`);
    const actualHash = sha256File(filePath);
    if (actualHash !== entry.sha256) throw new Error(`SHA-256 stimmt nicht: ${entry.path}`);
  }
  return manifest;
}

export function applyReleaseManifest(sourceRoot, destinationRoot) {
  const source = resolve(sourceRoot);
  const destination = resolve(destinationRoot);
  if (source === destination) throw new Error('Quell- und Zielordner dürfen nicht identisch sein.');
  if (!existsSync(join(destination, '.git'))) throw new Error(`Ziel ist kein geklontes Git-Repository: ${destination}`);
  const manifest = readAndValidateManifest(source);
  for (const rel of manifest.removedFiles) {
    rmSync(join(destination, rel), { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
  }
  let copied = 0;
  for (const entry of manifest.files) {
    const sourceFile = join(source, entry.path);
    const targetFile = join(destination, entry.path);
    mkdirSync(dirname(targetFile), { recursive: true });
    copyFileSync(sourceFile, targetFile);
    try { chmodSync(targetFile, statSync(sourceFile).mode & 0o777); } catch {}
    const targetHash = sha256File(targetFile);
    if (targetHash !== entry.sha256) throw new Error(`Zielprüfung nach Kopieren fehlgeschlagen: ${entry.path}`);
    copied += 1;
  }
  let controlCopied = 0;
  for (const rel of CONTROL_RELEASE_FILES) {
    const sourceFile = join(source, rel);
    const targetFile = join(destination, rel);
    mkdirSync(dirname(targetFile), { recursive: true });
    copyFileSync(sourceFile, targetFile);
    if (sha256File(sourceFile) !== sha256File(targetFile)) {
      throw new Error(`Steuerdatei-Prüfung nach Kopieren fehlgeschlagen: ${rel}`);
    }
    controlCopied += 1;
  }
  for (const required of REQUIRED_RELEASE_FILES) {
    if (!existsSync(join(destination, required))) throw new Error(`Pflichtdatei fehlt nach Release-Anwendung: ${required}`);
  }
  for (const controlFile of CONTROL_RELEASE_FILES) {
    if (!existsSync(join(destination, controlFile))) throw new Error(`Steuerdatei fehlt nach Release-Anwendung: ${controlFile}`);
  }
  return {
    source,
    destination,
    version: manifest.version,
    updaterVersion: manifest.updaterVersion,
    copied,
    controlCopied,
    removed: manifest.removedFiles.length,
  };
}
