import { createHash } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { lstat, mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';

import {
  botNavigationSourceProfileForMap,
} from '../server/bot-navigation.js';

const MAX_SOURCE_BYTES = 64 * 1024 * 1024;

function sha256(bytes) {
  return createHash('sha256').update(bytes).digest('hex');
}

function verifyBytes(bytes, identity, label) {
  if (bytes.length !== identity.size || sha256(bytes) !== identity.sha256) {
    throw new Error(`${label} differs from its pinned identity`);
  }
}

async function readPinnedFile(file, identity, label) {
  const before = await lstat(file);
  if (!before.isFile() || before.isSymbolicLink() || before.size !== identity.size ||
      before.size <= 0 || before.size > MAX_SOURCE_BYTES) {
    throw new Error(`${label} is not the pinned regular file`);
  }
  const bytes = await readFile(file);
  const after = await lstat(file);
  if (!after.isFile() || after.isSymbolicLink() || after.dev !== before.dev ||
      after.ino !== before.ino || after.size !== before.size ||
      after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) {
    throw new Error(`${label} changed while it was being read`);
  }
  verifyBytes(bytes, identity, label);
  return bytes;
}

function extractZipEntry(archive, entry, identity, label) {
  const result = spawnSync('unzip', ['-p', archive, entry], {
    encoding: null,
    maxBuffer: MAX_SOURCE_BYTES,
  });
  if (result.error || result.status !== 0 || result.signal) {
    throw new Error(`${label} cannot be extracted: ` +
      `${result.error?.message || result.stderr?.toString('utf8').trim() || `exit ${result.status}`}`);
  }
  verifyBytes(result.stdout, identity, label);
  return result.stdout;
}

async function stagePinnedBytes(directory, identity, extension, bytes, label) {
  await mkdir(directory, { recursive: true });
  const file = path.join(directory, `${identity.sha256}.${extension}`);
  try {
    await writeFile(file, bytes, { flag: 'wx', mode: 0o600 });
  } catch (error) {
    if (error?.code !== 'EEXIST') throw error;
    await readPinnedFile(file, identity, label);
  }
  return file;
}

export async function loadBotNavigationSource(profile, {
  sourceDirectory,
  temporaryDirectory,
}) {
  const approved = botNavigationSourceProfileForMap(profile?.map);
  if (!approved || approved.id !== profile?.id || approved.sha256 !== profile?.sha256 ||
      approved.kind !== 'verified-aas-source') {
    throw new Error('unknown bot navigation source profile');
  }

  const archive = path.resolve(sourceDirectory, approved.archive.path);
  const archiveBytes = await readPinnedFile(archive, approved.archive, `${approved.id} archive`);
  const pinnedArchive = await stagePinnedBytes(
    path.join(temporaryDirectory, 'source-archives'),
    approved.archive,
    'zip',
    archiveBytes,
    `${approved.id} staged archive`,
  );

  const containerBytes = extractZipEntry(
    pinnedArchive,
    approved.container.path,
    approved.container,
    `${approved.id} container`,
  );
  const container = await stagePinnedBytes(
    path.join(temporaryDirectory, 'source-containers'),
    approved.container,
    'pk4',
    containerBytes,
    `${approved.id} staged container`,
  );

  const source = extractZipEntry(
    container,
    approved.entry.path,
    approved.entry,
    `${approved.id} entry`,
  );
  return { profile: approved, source };
}
