import { createHash } from "node:crypto";
import {
  constants as fsConstants,
  copyFile,
  lstat,
  mkdir,
  mkdtemp,
  readFile,
  readdir,
  realpath,
  rename,
  rm,
  writeFile,
} from "node:fs/promises";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { PHP, loadPHPRuntime } from "@php-wasm/universal";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");

export const PHP_ENGINE_LOCK = Object.freeze({
  bridgeAbi: 1,
  phpVersion: "8.5.8",
  pcreVersion: "10.44",
  packageVersion: "3.1.46",
  asyncMode: "asyncify",
  platform: "wasm32-emscripten",
  loaderBytes: 270_942,
  wasmBytes: 21_019_221,
  maximumPatternUtf16: 65_536,
  maximumSubjectBytes: 16_777_216,
  maximumReplacementUtf16: 65_536,
  maximumOutputBytes: 67_108_864,
  maximumMatches: 10_000,
  maximumCaptureRows: 100_000,
  maximumCaptureGroups: 1_000,
  npm: Object.freeze({
    web: Object.freeze({
      name: "@php-wasm/web-8-5",
      version: "3.1.46",
      resolved:
        "https://registry.npmjs.org/@php-wasm/web-8-5/-/web-8-5-3.1.46.tgz",
      integrity:
        "sha512-j9wwlY6aNXuKBr4JPJzqE+vkM3Onk+byJsxLqaJs4U0gYDUBLr8CuKuVr+p13apSTUosConePYTAs0214aNxYA==",
      packageJsonSha256:
        "bc69e9a9f88ed9c2e2c3ccd06ce7f842007cc0ef8edd4496a91b0131afdef1e4",
    }),
    universal: Object.freeze({
      name: "@php-wasm/universal",
      version: "3.1.46",
      resolved:
        "https://registry.npmjs.org/@php-wasm/universal/-/universal-3.1.46.tgz",
      integrity:
        "sha512-sx0KyN5kcsCMzwy+XJAyoOukSA5TEZYd0LK3l9kRP3I0CSd5h6JItkf5eZ42WltB+wD+aTRdqSwc/Mvkc7qBjQ==",
      packageJsonSha256:
        "051ffd543901513ad76b5f935882b99988622e67086df16df48fef8f7c12a116",
    }),
    util: Object.freeze({
      name: "@php-wasm/util",
      version: "3.1.46",
      resolved: "https://registry.npmjs.org/@php-wasm/util/-/util-3.1.46.tgz",
      integrity:
        "sha512-Yh9HNbx+rB2R1tScUvvAsbJVIB7msjWceWYogyR2YvAOqlDL0ymZyaQ65+K3QcHh/8HostTRwwgQh5oK4DLDrA==",
      packageJsonSha256:
        "4d2d42e0495a92e1fed76ac6273cf807d84466fe6dbd4718c5602012968d6e78",
    }),
    logger: Object.freeze({
      name: "@php-wasm/logger",
      version: "3.1.46",
      resolved:
        "https://registry.npmjs.org/@php-wasm/logger/-/logger-3.1.46.tgz",
      integrity:
        "sha512-PfnDq5ammh5FXxkGX1TGEck9gmW3i76IvBCqtX4D45xnSBIuCeu8uf2oWuty2j/c8wTEO8vyRDdvCj5oow7bXw==",
      packageJsonSha256:
        "4e04ad8620452b48b1f225431c8143f4e14888408db2e3a166e02e4ac6863fc9",
    }),
    compression: Object.freeze({
      name: "@php-wasm/stream-compression",
      version: "3.1.46",
      resolved:
        "https://registry.npmjs.org/@php-wasm/stream-compression/-/stream-compression-3.1.46.tgz",
      integrity:
        "sha512-+oUw4CPX52FgYXEh5C+MgJAg7Cy5OeaVuh7HnQR6oxb69nU63/r8tHTLV/WrWRyjBYh321uJzLV7Gg29qmiUtg==",
      packageJsonSha256:
        "5a2b0ca2dff1466bdeb38c9dca93d2fb0154cf9fef254a7c475d8cfe32344b72",
    }),
    progress: Object.freeze({
      name: "@php-wasm/progress",
      version: "3.1.46",
      resolved:
        "https://registry.npmjs.org/@php-wasm/progress/-/progress-3.1.46.tgz",
      integrity:
        "sha512-WmmgEm6yz322M3pzgAUjxckyhpVxr2WWlHyVYDyaMZWU6UIDUHfiRZRPzADohNyYvV3g0/zMNaDGmxz+TTXwQQ==",
      packageJsonSha256:
        "0408ea4cdb2d251ad357090e9532f50769e9ca7c135aa823ccad9e7b95950a36",
    }),
    ini: Object.freeze({
      name: "ini",
      version: "4.1.2",
      resolved: "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz",
      integrity:
        "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==",
      packageJsonSha256:
        "0bc65f73405054313cab76d1d5f89b888ad2c0e1261d3a97644728852a5df8dc",
    }),
    featureDetect: Object.freeze({
      name: "wasm-feature-detect",
      version: "1.8.0",
      resolved:
        "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
      integrity:
        "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
      packageJsonSha256:
        "f511a8499a742e364d5bc9cdb23fb09108b12c7683fe06fb9e2bc324832b3b5b",
    }),
  }),
  sourceAssets: Object.freeze({
    loader: "0ed44eae7cd040826775b3dcd5376bb86a14257ae9fe1ff5cda1b7daf7572088",
    wasm: "ed97ea5422dbf23e0687c98203cc8c6d7f2772a3d71b03d4991d2fdd93a048f0",
    nativeInventory:
      "c0da167d0a43b0d85f72ca17fed7d823c84b85718e4c8abf5f120f61f7c8654a",
    phpWasmLicense:
      "8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643",
    iniLicense:
      "4ec3d4c66cd87f5c8d8ad911b10f99bf27cb00cdfcff82621956e379186b016b",
    featureDetectLicense:
      "bcf29b4fd3ec2cb5f9d40a0866da446f6da62170d2ccedf4aeca9cf9406dd20c",
  }),
  legalAssets: Object.freeze({
    "LICENSE.Emscripten-4.0.19.txt":
      "620a78084fc7ca97c0b5dea9abf891f3ffcadfdbf305276f099c9c4e12fc1d86",
    "LICENSE.Oniguruma-6.9.10.txt":
      "70ba5469ea0bab6e18a32d7009068f996503168d27be57747e08da34337ff26f",
    "LICENSE.OpenSSL-1.1.1t.txt":
      "c32913b33252e71190af2066f08115c69bc9fddadf3bf29296e20c835389841c",
    "LICENSE.PCRE2-10.44.txt":
      "030087e2e8dd7c1bdd26057d25d4ded8f45bbf01ad458d68665ad04b8b0fbedf",
    "LICENSE.PHP-4.0.txt":
      "e66a38e0a83f49b6ad6ec72f6c0d7600cfa35ba95ee1a67eeeae676cd4416789",
    "LICENSE.PHP-CLI-http-parser-MIT.txt":
      "8539e2b4bd4e874b0f7d94a25b8f1b445d46ae22c8373d5a69f801a1a12f3265",
    "LICENSE.PHP-Lexbor-Apache-2.0.txt":
      "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594",
    "LICENSE.PHP-Zend-2.0.txt":
      "509aeb3be64e428d7e0004ce224620ab01bf94adaacefbb449611939df56bc5b",
    "LICENSE.PHP-bcmath-LGPL-2.1.txt":
      "7586d794d062a2a4a16513d95358aecaee4450e2062f7e64ce5ce63ac7212fd6",
    "LICENSE.PHP-libavifinfo-BSD-2-Clause.txt":
      "5d40dcb72f934c3d08e10ddb8ba4844de17bf6f9aabc1a382aacf8a8bf4567f8",
    "LICENSE.PHP-libmagic-BSD.txt":
      "0bfa856a9930bddadbef95d1be1cf4e163c0be618e76ea3275caaf255283e274",
    "LICENSE.PHP-libmbfl-LGPL-2.1.txt":
      "31d9cadcc63c15b79fc59c70f043956104a34c75f23f0ac0ada53c1edfeb88ca",
    "LICENSE.PHP-timelib-MIT.txt":
      "2120d029e7c67b9d08c1f665f1c3ed581206f00970b664cccdebe1419bf566e9",
    "LICENSE.PHP-uriparser-BSD-3-Clause.txt":
      "287f09e6546a9610f949f89e8fb937cacfeabd7bfaa8c8a0c18312193bf04ad3",
    "LICENSE.curl-7.69.1.txt":
      "db3c4a3b3695a0f317a0c5176acd2f656d18abc45b3ee78e50935a78eb1e132e",
    "LICENSE.ini-ISC.txt":
      "4ec3d4c66cd87f5c8d8ad911b10f99bf27cb00cdfcff82621956e379186b016b",
    "LICENSE.libaom-3.12.1.txt":
      "4764a286d8b2faeaf42f4418e7d7a28d58fc8fd4d00a3d0a7f44b0a4099de7f2",
    "LICENSE.libavif-1.3.0.txt":
      "165abf92cc04b39e80d29cadea7a6a7e8fddf59407d4ad2616507a7ebe8216f9",
    "LICENSE.libcxx.txt":
      "539dd7aed86e8a4f12cbdd0e6c50c189c7d74847e4fecc64ce2c6ee3a01da38b",
    "LICENSE.libcxxabi.txt":
      "e2b35be49f7284a45b7baca8fc7b3ab7440e7902392b2528a457816b5bb2a15c",
    "LICENSE.libgd-2.3.3.txt":
      "005f4b6b0141d1bd11d371bbf7d4f67947f85a4906b7f5465f942204cf918ba3",
    "LICENSE.libiconv-1.17-LGPL-2.1.txt":
      "dc626520dcd53a22f727af3ee42c770e56c97a64fe3adb063799d8ab032fe551",
    "LICENSE.libjpeg-turbo-3.0.3.txt":
      "e548c96c0c09397c94b5c5531836c02ff2c403f1f2b130cad8e3ec9439b5a7cc",
    "LICENSE.libpng-1.6.39.txt":
      "dfe5a536b0e5a531f844fb9c101a3089aca60772a503893b8e15f9457e369960",
    "LICENSE.libwebp.txt":
      "5aec868f669e384a22372a4e8a1a6cd7d44c64cd451f960ca69cc170d1e13acf",
    "LICENSE.libxml2-2.9.10.txt":
      "c5c63674f8a83c4d2e385d96d1c670a03cb871ba2927755467017317878574bd",
    "LICENSE.libyuv.txt":
      "2b2cc1180c7e6988328ad2033b04b80117419db9c4c584918bbb3cfec7e9364f",
    "LICENSE.libzip-1.9.2.txt":
      "01c022eca6d566e2e8792fd0f091a28653b2a608319922bcd4de91c49d1438e1",
    "LICENSE.llvm-compiler-rt.txt":
      "1a8f1058753f1ba890de984e48f0242a3a5c29a6a8f2ed9fd813f36985387e8d",
    "LICENSE.musl.txt":
      "f9bc4423732350eb0b3f7ed7e91d530298476f8fec0c6c427a1c04ade22655af",
    "LICENSE.php-wasm-GPL-2.0-or-later.txt":
      "8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643",
    "LICENSE.wasm-feature-detect-Apache-2.0.txt":
      "bcf29b4fd3ec2cb5f9d40a0866da446f6da62170d2ccedf4aeca9cf9406dd20c",
    "LICENSE.zlib-1.2.13.txt":
      "845efc77857d485d91fb3e0b884aaa929368c717ae8186b66fe1ed2495753243",
    "NOTICE.IANA-TZDATA-2026.1.txt":
      "8a19c6e511386301fa99ac61fec34c8be57be30255b29dad3ab294fe6b5da7c5",
    "NOTICE.PHP-Lexbor.txt":
      "b87f965fd2eba846a0a502d633dd7e7a680b93de5c514c404c948ccf1e5c9dc7",
    "NOTICE.PHP-REDIST-BINS.txt":
      "9159e8c784edf5ab9069c573bdc64a7a406ebe718768d2c25a0b243449b7665b",
    "NOTICE.PHP-public-domain-hash-code.txt":
      "1fc21940918bd2027d7b9ffccd23d45d35b33d6ca5b42a6fb481e75809763a98",
    "NOTICE.SQLite-3.51.0.txt":
      "b8262a33944a8937e6f38f573df2d945101b4bcb45de1b150c7e3a6ebd23b19b",
    "NOTICE.dlmalloc-2.8.6.txt":
      "ea9de40ffcf0d3536a71e22af9c33647c517256aa2a43167e933e74535569594",
    "PATENTS.PHP-libavifinfo.txt":
      "aef98cc6105f279463c37ec8c46dec5f2d133b6a91531ae9ec274e80fece9a88",
    "PATENTS.libaom-3.12.1.txt":
      "661fb8e504744e95587b556b94a58343448300606a41bea8c7a9b97125696e61",
    "PATENTS.libwebp.txt":
      "cc3273e0694ea5896145e0677699b53471b03ea43021ddc50e7923fbb9f5023c",
  }),
});

const PACK_FILES = Object.freeze(
  [
    ...Object.keys(PHP_ENGINE_LOCK.legalAssets),
    "SHA256SUMS",
    "engine-metadata.json",
    "native-components.json",
    "php-loader.mjs",
    "php.wasm",
  ].sort(),
);
const CHECKSUM_FILES = Object.freeze(
  PACK_FILES.filter((name) => name !== "SHA256SUMS"),
);
const LOADER_IMPORT = "import dependencyFilename from './8_5_8/php_8_5.wasm';";
const PATCHED_LOADER =
  'const dependencyFilename = new URL("./php.wasm", import.meta.url).href;';
const PHP_NATIVE_COMPONENT_IDS = Object.freeze([
  "wordpress-playground-php-wasm",
  "php",
  "pcre2",
  "php-libbcmath",
  "timelib",
  "iana-tzdata",
  "php-libmagic",
  "lexbor",
  "libmbfl",
  "php-uriparser",
  "php-libavifinfo",
  "zlib",
  "libzip",
  "libxml2",
  "sqlite",
  "libgd",
  "libjpeg-turbo",
  "libpng",
  "libwebp-and-libsharpyuv",
  "libavif",
  "libaom",
  "libyuv",
  "openssl",
  "curl",
  "libiconv",
  "oniguruma",
  "emscripten-runtime",
  "musl-libc",
  "llvm-compiler-rt",
  "llvm-libcxx",
  "llvm-libcxxabi",
  "dlmalloc",
]);
const PHP_EXCLUDED_COMPONENT_IDS = Object.freeze([
  "gmp",
  "libsodium",
  "tidy",
  "intl-and-icu",
  "freetype",
  "imagemagick-and-imagick",
  "standalone-libaom-3.13.1-recipe",
]);
const PHP_LOADED_EXTENSIONS = Object.freeze(
  [
    "Core",
    "date",
    "lexbor",
    "openssl",
    "pcre",
    "sqlite3",
    "zlib",
    "bcmath",
    "calendar",
    "ctype",
    "curl",
    "dns_polyfill",
    "uri",
    "json",
    "mbstring",
    "SPL",
    "fileinfo",
    "filter",
    "gd",
    "hash",
    "iconv",
    "session",
    "Zend OPcache",
    "standard",
    "libxml",
    "PDO",
    "pdo_sqlite",
    "Phar",
    "post_message_to_js",
    "random",
    "Reflection",
    "exif",
    "SimpleXML",
    "soap",
    "tokenizer",
    "dom",
    "wasm_memory_storage",
    "xml",
    "xmlreader",
    "xmlwriter",
    "zip",
  ].sort(compareText),
);
const PHP_BINARY_MARKERS = Object.freeze([
  "AOMedia Project AV1 Encoder v3.12.1",
  "AOMedia Project AV1 Decoder v3.12.1",
  "libjpeg-turbo version 3.0.3 (build 20250321)",
]);
const PHP_EXCLUDED_BINARY_MARKERS = Object.freeze([
  "AOMedia Project AV1 Encoder v3.13.1",
  "AOMedia Project AV1 Decoder v3.13.1",
]);

function compareText(left, right) {
  return left < right ? -1 : left > right ? 1 : 0;
}

function sha256(value) {
  return createHash("sha256").update(value).digest("hex");
}

async function sha256File(file) {
  return sha256(await readFile(file));
}

async function assertRealDirectory(candidate, label) {
  const details = await lstat(candidate).catch(() => null);
  if (!details?.isDirectory() || details.isSymbolicLink()) {
    throw new Error(`${label} must be a real directory: ${candidate}`);
  }
  return realpath(candidate);
}

async function assertRegularFile(candidate, label) {
  const details = await lstat(candidate).catch(() => null);
  if (!details?.isFile() || details.isSymbolicLink()) {
    throw new Error(
      `${label} must be a regular, non-symlink file: ${candidate}`,
    );
  }
  return realpath(candidate);
}

async function assertHash(file, expected, label) {
  const actual = await sha256File(file);
  if (actual !== expected) {
    throw new Error(
      `${label} SHA-256 mismatch: expected ${expected}, got ${actual}.`,
    );
  }
}

function assertExactJson(actual, expected, label) {
  if (JSON.stringify(actual) !== JSON.stringify(expected)) {
    throw new Error(`${label} drifted from the pinned PHP runtime inventory.`);
  }
}

function validateNativeInventory(inventory) {
  if (
    inventory?.schemaVersion !== 1 ||
    inventory.scope?.assetSha256 !== PHP_ENGINE_LOCK.sourceAssets.wasm ||
    inventory.scope?.assetBytes !== PHP_ENGINE_LOCK.wasmBytes ||
    inventory.scope?.phpVersion !== PHP_ENGINE_LOCK.phpVersion ||
    inventory.scope?.platform !== PHP_ENGINE_LOCK.platform ||
    inventory.buildProvenance?.wordpressPlayground?.release !==
      `v${PHP_ENGINE_LOCK.packageVersion}` ||
    inventory.buildProvenance?.wordpressPlayground?.commit !==
      "581c7c172428159eb4e6c5309054a568cd39a97a" ||
    inventory.buildProvenance?.php?.commit !==
      "26b97507444c4fbda072f57dda1820f7b7d5e467" ||
    inventory.buildProvenance?.emscripten?.version !== "4.0.19" ||
    inventory.buildProvenance?.emscripten?.commit !==
      "08e2de1031913e4ba7963b1c56f35f036a7d4d56"
  ) {
    throw new Error("PHP native-component provenance identity drifted.");
  }
  const componentIds = inventory.linkedComponents?.map(({ id }) => id);
  const excludedIds = inventory.excludedDetections?.map(({ id }) => id);
  assertExactJson(
    componentIds,
    PHP_NATIVE_COMPONENT_IDS,
    "PHP linked-component list",
  );
  assertExactJson(
    excludedIds,
    PHP_EXCLUDED_COMPONENT_IDS,
    "PHP excluded-detection list",
  );
  const referencedLegalFiles = new Set();
  for (const component of inventory.linkedComponents) {
    if (
      !Array.isArray(component.legalFiles) ||
      component.legalFiles.length === 0
    ) {
      throw new Error(
        `PHP native component ${component.id} has no legal-file route.`,
      );
    }
    for (const name of component.legalFiles) {
      if (!Object.hasOwn(PHP_ENGINE_LOCK.legalAssets, name)) {
        throw new Error(
          `PHP native component ${component.id} references an unpinned legal file: ${name}.`,
        );
      }
      referencedLegalFiles.add(name);
    }
  }
  for (const name of Object.keys(PHP_ENGINE_LOCK.legalAssets)) {
    if (
      name !== "LICENSE.ini-ISC.txt" &&
      name !== "LICENSE.wasm-feature-detect-Apache-2.0.txt" &&
      !referencedLegalFiles.has(name)
    ) {
      throw new Error(
        `Pinned PHP native legal file is not routed from the inventory: ${name}.`,
      );
    }
  }
  return inventory;
}

async function verifyPackage(projectRoot, descriptor) {
  const directory = path.join(
    projectRoot,
    "node_modules",
    ...descriptor.name.split("/"),
  );
  await assertRealDirectory(directory, `${descriptor.name} npm package`);
  const packageFile = path.join(directory, "package.json");
  await assertRegularFile(packageFile, `${descriptor.name} package.json`);
  await assertHash(
    packageFile,
    descriptor.packageJsonSha256,
    `${descriptor.name} package.json`,
  );
  const packageJson = JSON.parse(await readFile(packageFile, "utf8"));
  if (
    packageJson.name !== descriptor.name ||
    packageJson.version !== descriptor.version
  ) {
    throw new Error(`${descriptor.name} npm package identity drifted.`);
  }
  return directory;
}

async function verifySources(projectRoot) {
  const lock = JSON.parse(
    await readFile(path.join(projectRoot, "package-lock.json"), "utf8"),
  );
  const rootEntry = lock.packages?.[""];
  for (const descriptor of Object.values(PHP_ENGINE_LOCK.npm)) {
    const entry = lock.packages?.[`node_modules/${descriptor.name}`];
    if (
      entry?.version !== descriptor.version ||
      entry?.resolved !== descriptor.resolved ||
      entry?.integrity !== descriptor.integrity
    ) {
      throw new Error(
        `${descriptor.name}@${descriptor.version} is not exactly pinned in package-lock.json.`,
      );
    }
    await verifyPackage(projectRoot, descriptor);
  }
  for (const direct of [
    PHP_ENGINE_LOCK.npm.web,
    PHP_ENGINE_LOCK.npm.universal,
  ]) {
    if (rootEntry?.devDependencies?.[direct.name] !== direct.version) {
      throw new Error(`${direct.name} must be an exact root devDependency.`);
    }
  }

  const web = path.join(projectRoot, "node_modules", "@php-wasm", "web-8-5");
  const loader = path.join(web, "asyncify", "php_8_5.js");
  const wasm = path.join(web, "asyncify", "8_5_8", "php_8_5.wasm");
  const phpWasmLicense = path.join(web, "LICENSE");
  const iniLicense = path.join(projectRoot, "node_modules", "ini", "LICENSE");
  const featureDetectLicense = path.join(
    projectRoot,
    "node_modules",
    "wasm-feature-detect",
    "LICENSE",
  );
  const engineSourceDirectory = path.join(projectRoot, "engines", "php");
  const nativeInventory = path.join(
    engineSourceDirectory,
    "native-components.json",
  );
  const legalFiles = Object.fromEntries(
    Object.keys(PHP_ENGINE_LOCK.legalAssets).map((name) => [
      name,
      path.join(engineSourceDirectory, name),
    ]),
  );
  Object.assign(legalFiles, {
    "LICENSE.php-wasm-GPL-2.0-or-later.txt": phpWasmLicense,
    "LICENSE.ini-ISC.txt": iniLicense,
    "LICENSE.wasm-feature-detect-Apache-2.0.txt": featureDetectLicense,
  });
  for (const [file, expected, label] of [
    [loader, PHP_ENGINE_LOCK.sourceAssets.loader, "PHP Asyncify loader"],
    [wasm, PHP_ENGINE_LOCK.sourceAssets.wasm, "PHP Asyncify WebAssembly"],
    [
      nativeInventory,
      PHP_ENGINE_LOCK.sourceAssets.nativeInventory,
      "PHP native-component inventory",
    ],
  ]) {
    await assertRegularFile(file, label);
    await assertHash(file, expected, label);
  }
  for (const [name, expected] of Object.entries(PHP_ENGINE_LOCK.legalAssets)) {
    const file = legalFiles[name];
    await assertRegularFile(file, `PHP legal asset ${name}`);
    await assertHash(file, expected, `PHP legal asset ${name}`);
  }
  validateNativeInventory(JSON.parse(await readFile(nativeInventory, "utf8")));
  return {
    loader,
    wasm,
    nativeInventory,
    legalFiles,
  };
}

function patchLoader(source) {
  if (!source.startsWith(`${LOADER_IMPORT}\n`)) {
    throw new Error("PHP Asyncify loader import prelude drifted.");
  }
  const patched = source.replace(LOADER_IMPORT, PATCHED_LOADER);
  if (
    patched === source ||
    patched.includes(LOADER_IMPORT) ||
    !patched.includes("export function init(RuntimeName, PHPLoader)")
  ) {
    throw new Error("PHP Asyncify loader patch was not exact.");
  }
  return patched;
}

async function fileRecord(directory, name) {
  const value = await readFile(path.join(directory, name));
  return { path: name, sha256: sha256(value), bytes: value.byteLength };
}

async function expectedMetadata(projectRoot, pack) {
  const bridgePath = path.join(projectRoot, "engines", "php", "bridge.php");
  const bridge = await readFile(bridgePath);
  const inventory = validateNativeInventory(
    JSON.parse(
      await readFile(path.join(pack, "native-components.json"), "utf8"),
    ),
  );
  const inventoryRecord = await fileRecord(pack, "native-components.json");
  const files = [];
  for (const name of CHECKSUM_FILES) {
    if (name !== "engine-metadata.json") {
      files.push(await fileRecord(pack, name));
    }
  }
  return {
    schemaVersion: 1,
    engine: "php",
    engineVersion: `PHP ${PHP_ENGINE_LOCK.phpVersion} preg / PCRE2 ${PHP_ENGINE_LOCK.pcreVersion}`,
    runtime: `@php-wasm/web-8-5 ${PHP_ENGINE_LOCK.packageVersion} Asyncify`,
    platform: PHP_ENGINE_LOCK.platform,
    status: "production-runtime",
    bridge: {
      abiVersion: PHP_ENGINE_LOCK.bridgeAbi,
      sourceFile: {
        path: "engines/php/bridge.php",
        sha256: sha256(bridge),
        bytes: bridge.byteLength,
      },
      transport: "fixed VFS script plus JSON request",
      userValuesAreJsonData: true,
      replacement:
        "bounded PHP 8.5.8 preg_get_backref-compatible expansion over native preg_match records",
      replacementParityOracle:
        "preg_replace is called only by the fixed identity self-test",
      replacementTransport:
        "bounded UTF-8 bytes encoded as base64 in the JSON envelope",
      nativeOffsetUnit: "UTF-8 bytes (PREG_OFFSET_CAPTURE)",
      maximumPatternUtf16: PHP_ENGINE_LOCK.maximumPatternUtf16,
      maximumSubjectBytes: PHP_ENGINE_LOCK.maximumSubjectBytes,
      maximumReplacementUtf16: PHP_ENGINE_LOCK.maximumReplacementUtf16,
      maximumOutputBytes: PHP_ENGINE_LOCK.maximumOutputBytes,
      maximumMatches: PHP_ENGINE_LOCK.maximumMatches,
      maximumCaptureRows: PHP_ENGINE_LOCK.maximumCaptureRows,
      maximumCaptureGroups: PHP_ENGINE_LOCK.maximumCaptureGroups,
    },
    source: {
      repository: "https://github.com/WordPress/wordpress-playground",
      packages: Object.values(PHP_ENGINE_LOCK.npm).map(
        ({ name, version, integrity }) => ({ name, version, integrity }),
      ),
      runtimeVariant: "PHP 8.5 Asyncify web build (without intl extension)",
      loaderPatch: {
        originalSha256: PHP_ENGINE_LOCK.sourceAssets.loader,
        originalBytes: PHP_ENGINE_LOCK.loaderBytes,
        change:
          "replace bundler-only wasm import with same-directory php.wasm URL",
      },
      php: {
        version: PHP_ENGINE_LOCK.phpVersion,
        pcreVersion: PHP_ENGINE_LOCK.pcreVersion,
        asyncMode: PHP_ENGINE_LOCK.asyncMode,
        platform: PHP_ENGINE_LOCK.platform,
      },
      buildProvenance: {
        wordpressPlaygroundRelease: "v3.1.46",
        wordpressPlaygroundCommit: "581c7c172428159eb4e6c5309054a568cd39a97a",
        phpTag: "php-8.5.8",
        phpTagObject: "8a3b5a5124006c11a8fbfce838ec7dd53615cc77",
        phpCommit: "26b97507444c4fbda072f57dda1820f7b7d5e467",
        emscriptenVersion: "4.0.19",
        emscriptenCommit: "08e2de1031913e4ba7963b1c56f35f036a7d4d56",
      },
      nativeComponents: {
        ...inventoryRecord,
        linkedComponentCount: inventory.linkedComponents.length,
        excludedDetectionCount: inventory.excludedDetections.length,
        scope:
          "complete native/source/legal inventory for the exact shipped php.wasm",
      },
      embeddedPathClassification:
        "Strings such as /home/web_user in generated Emscripten artifacts are virtual/build filesystem paths, not host source provenance.",
      phpWasmLicenseAppliesTo: [
        "@php-wasm/web-8-5",
        "@php-wasm/universal",
        "@php-wasm/util",
        "@php-wasm/logger",
        "@php-wasm/stream-compression",
        "@php-wasm/progress",
      ],
    },
    licensing: {
      phpCodebase: {
        selectedLicense: "PHP License version 4 (BSD-3-Clause)",
        election:
          "PHP 8.5.8 is used under PHP License version 4 via the later-version option in section 5 of PHP License 3.01.",
        provenanceUrl: "https://www.php.net/license/",
        licenseTextUrl: "https://www.php.net/license/4_0.txt",
        licenseTextSha256: PHP_ENGINE_LOCK.legalAssets["LICENSE.PHP-4.0.txt"],
      },
      pcre2: {
        version: PHP_ENGINE_LOCK.pcreVersion,
        license: "PCRE2 BSD license",
        provenanceUrl:
          "https://github.com/PCRE2Project/pcre2/blob/pcre2-10.44/LICENCE",
        licenseTextSha256:
          PHP_ENGINE_LOCK.legalAssets["LICENSE.PCRE2-10.44.txt"],
      },
      phpWasmPackages: "GPL-2.0-or-later",
      ini: "ISC",
      wasmFeatureDetect: "Apache-2.0",
      nativeInventory: {
        path: inventoryRecord.path,
        sha256: inventoryRecord.sha256,
        independentlyPinnedLegalAssets: Object.keys(PHP_ENGINE_LOCK.legalAssets)
          .length,
        statement:
          "Every linked component routes to an independently SHA-256-pinned licence, notice, or patent file; raw-string false positives and unshipped side modules are explicitly excluded.",
      },
    },
    files,
  };
}

async function runPhpRequest(php, request) {
  php.writeFile("/regex-tools/request.json", JSON.stringify(request));
  const response = await php.run({ scriptPath: "/regex-tools/bridge.php" });
  if (
    response.exitCode !== 0 ||
    response.errors !== "" ||
    response.text.length === 0
  ) {
    throw new Error("PHP bridge process failed its pack smoke test.");
  }
  return JSON.parse(response.text);
}

async function auditPhpRuntime(php) {
  const response = await php.run({
    code: `<?php
$extensions = get_loaded_extensions();
sort($extensions, SORT_STRING);
$gd = gd_info();
ob_start();
phpinfo(INFO_MODULES);
$moduleInfo = ob_get_clean();
$audit = [
    'extensions' => $extensions,
    'versions' => [
        'php' => PHP_VERSION,
        'pcre2' => PCRE_VERSION,
        'openssl' => OPENSSL_VERSION_TEXT,
        'curl' => curl_version()['version'],
        'zlib' => ZLIB_VERSION,
        'sqlite' => SQLite3::version()['versionString'],
        'libxml2' => LIBXML_DOTTED_VERSION,
        'libgd' => GD_VERSION,
        'libiconv' => ICONV_VERSION,
        'oniguruma' => MB_ONIGURUMA_VERSION,
        'libzip' => ZipArchive::LIBZIP_VERSION,
    ],
    'moduleMarkers' => [
        'timelib-2022.15' => str_contains($moduleInfo, '2022.15'),
        'tzdata-2026.1' => str_contains($moduleInfo, '2026.1'),
        'lexbor-2.7.0' => str_contains($moduleInfo, '2.7.0'),
        'libmbfl-1.3.2' => str_contains($moduleInfo, '1.3.2'),
        'uriparser-1.0.2' => str_contains($moduleInfo, '1.0.2'),
    ],
    'gdFeatures' => [
        'jpeg' => $gd['JPEG Support'] ?? false,
        'png' => $gd['PNG Support'] ?? false,
        'webp' => $gd['WebP Support'] ?? false,
        'avif' => $gd['AVIF Support'] ?? false,
        'syntheticFreeTypeReport' => $gd['FreeType Support'] ?? false,
    ],
    'hashAlgorithms' => array_fill_keys(
        ['fnv1a64', 'murmur3a', 'murmur3c', 'murmur3f', 'sha3-256', 'xxh32', 'xxh64', 'xxh3', 'xxh128'],
        false,
    ),
    'phpZts' => (bool) PHP_ZTS,
    'excluded' => [
        'gmp' => !extension_loaded('gmp') && !function_exists('gmp_init'),
        'libsodium' => !extension_loaded('sodium') && !function_exists('sodium_crypto_box'),
        'tidy' => !extension_loaded('tidy') && !function_exists('tidy_parse_string') && !class_exists('tidy'),
        'intl-and-icu' => !extension_loaded('intl') && !class_exists('IntlDateFormatter'),
        'imagemagick-and-imagick' => !extension_loaded('imagick') && !class_exists('Imagick'),
    ],
];
foreach (array_keys($audit['hashAlgorithms']) as $algorithm) {
    $audit['hashAlgorithms'][$algorithm] = in_array($algorithm, hash_algos(), true);
}
echo json_encode($audit, JSON_THROW_ON_ERROR);`,
  });
  if (
    response.exitCode !== 0 ||
    response.errors !== "" ||
    response.text.length === 0
  ) {
    throw new Error("PHP native-component audit process failed.");
  }
  return JSON.parse(response.text);
}

async function smokePhp(pack, projectRoot) {
  const wasm = await readFile(path.join(pack, "php.wasm"));
  if (
    wasm.byteLength !== PHP_ENGINE_LOCK.wasmBytes ||
    !WebAssembly.validate(wasm)
  ) {
    throw new Error("PHP runtime is not the expected WebAssembly module.");
  }
  for (const marker of PHP_BINARY_MARKERS) {
    if (!wasm.includes(Buffer.from(marker, "utf8"))) {
      throw new Error(`PHP WebAssembly is missing native marker: ${marker}.`);
    }
  }
  for (const marker of PHP_EXCLUDED_BINARY_MARKERS) {
    if (wasm.includes(Buffer.from(marker, "utf8"))) {
      throw new Error(`PHP WebAssembly contains excluded marker: ${marker}.`);
    }
  }
  const loaderUrl = `${pathToFileURL(path.join(pack, "php-loader.mjs")).href}?verify=${Date.now()}-${Math.random()}`;
  const loader = await import(loaderUrl);
  if (
    loader.dependenciesTotalSize !== PHP_ENGINE_LOCK.wasmBytes ||
    typeof loader.init !== "function"
  ) {
    throw new Error("PHP Asyncify loader identity is invalid.");
  }

  const hadRequire = Object.hasOwn(globalThis, "require");
  const previousRequire = globalThis.require;
  globalThis.require = createRequire(import.meta.url);
  let php;
  try {
    const runtimeId = await loadPHPRuntime(loader, {
      wasmBinary: wasm,
      phpWasmAsyncMode: "asyncify",
      debug: false,
      print: () => undefined,
      printErr: () => undefined,
    });
    php = new PHP(runtimeId);
    php.mkdir("/regex-tools");
    php.writeFile(
      "/regex-tools/bridge.php",
      await readFile(
        path.join(projectRoot, "engines", "php", "bridge.php"),
        "utf8",
      ),
    );
    const identity = await runPhpRequest(php, {
      abi: PHP_ENGINE_LOCK.bridgeAbi,
      operation: "identity",
    });
    if (
      identity?.ok !== true ||
      identity.identity?.phpVersion !== PHP_ENGINE_LOCK.phpVersion ||
      identity.identity?.pcreVersion !== PHP_ENGINE_LOCK.pcreVersion ||
      identity.identity?.phpSapi !== "wasm" ||
      identity.identity?.integerBytes !== 8 ||
      identity.identity?.selfTest !== true
    ) {
      throw new Error("PHP runtime failed its identity smoke test.");
    }
    const common = {
      abi: PHP_ENGINE_LOCK.bridgeAbi,
      pattern: "(?<word>é+)",
      flags: "gu",
      options: {},
      subject: "😀éé x",
      scanAll: true,
      maximumMatches: 10,
      maximumCaptureRows: 10,
    };
    const execution = await runPhpRequest(php, {
      ...common,
      operation: "execute",
    });
    if (
      execution?.ok !== true ||
      execution.groupCount !== 1 ||
      JSON.stringify(execution.groupNames) !== JSON.stringify([[1, "word"]]) ||
      JSON.stringify(execution.matches?.[0]?.span) !== JSON.stringify([4, 8])
    ) {
      throw new Error("PHP runtime failed its native match smoke test.");
    }
    const replacement = await runPhpRequest(php, {
      ...common,
      operation: "replace",
      replacement: "[$1]",
      maximumOutputBytes: 1_024,
    });
    if (
      replacement?.ok !== true ||
      Object.hasOwn(replacement, "output") ||
      replacement.outputBase64 !==
        Buffer.from("😀[éé] x", "utf8").toString("base64") ||
      Buffer.from(replacement.outputBase64, "base64").toString("utf8") !==
        "😀[éé] x" ||
      replacement.outputTruncated !== false
    ) {
      throw new Error(
        "PHP runtime failed its bounded replacement transport smoke test.",
      );
    }
    const audit = await auditPhpRuntime(php);
    assertExactJson(
      audit.extensions,
      PHP_LOADED_EXTENSIONS,
      "PHP loaded-extension list",
    );
    assertExactJson(
      audit.versions,
      {
        php: "8.5.8",
        pcre2: "10.44 2024-06-07",
        openssl: "OpenSSL 1.1.1t  7 Feb 2023",
        curl: "7.69.1",
        zlib: "1.2.13",
        sqlite: "3.51.0",
        libxml2: "2.9.10",
        libgd: "2.3.3",
        libiconv: "1.17",
        oniguruma: "6.9.10",
        libzip: "1.9.2",
      },
      "PHP linked-library versions",
    );
    if (
      Object.values(audit.moduleMarkers).some((present) => present !== true) ||
      Object.values(audit.gdFeatures).some((present) => present !== true) ||
      Object.values(audit.hashAlgorithms).some((present) => present !== true) ||
      audit.phpZts !== false ||
      Object.values(audit.excluded).some((excluded) => excluded !== true)
    ) {
      throw new Error(
        "PHP native-component positive/negative runtime audit failed.",
      );
    }
  } finally {
    try {
      php?.exit(0);
    } catch {
      // The Emscripten runtime may signal its normal exit as an exception.
    }
    if (hadRequire) globalThis.require = previousRequire;
    else delete globalThis.require;
  }
}

export async function verifyPhpPack(directory, projectRoot = root) {
  const pack = await assertRealDirectory(directory, "PHP engine pack");
  const names = (await readdir(pack, { withFileTypes: true }))
    .map((entry) => {
      if (!entry.isFile() || entry.isSymbolicLink()) {
        throw new Error(`PHP pack contains a non-regular entry: ${entry.name}`);
      }
      return entry.name;
    })
    .sort(compareText);
  if (JSON.stringify(names) !== JSON.stringify(PACK_FILES)) {
    throw new Error(`PHP pack must contain only: ${PACK_FILES.join(", ")}.`);
  }
  const checksumLines = (await readFile(path.join(pack, "SHA256SUMS"), "utf8"))
    .trimEnd()
    .split("\n");
  const expectedLines = [];
  for (const name of CHECKSUM_FILES) {
    expectedLines.push(`${await sha256File(path.join(pack, name))}  ${name}`);
  }
  if (JSON.stringify(checksumLines) !== JSON.stringify(expectedLines)) {
    throw new Error("PHP pack checksum manifest is stale or malformed.");
  }
  await assertHash(
    path.join(pack, "php.wasm"),
    PHP_ENGINE_LOCK.sourceAssets.wasm,
    "Packed PHP WebAssembly",
  );
  await assertHash(
    path.join(pack, "native-components.json"),
    PHP_ENGINE_LOCK.sourceAssets.nativeInventory,
    "Packed PHP native-component inventory",
  );
  validateNativeInventory(
    JSON.parse(
      await readFile(path.join(pack, "native-components.json"), "utf8"),
    ),
  );
  for (const [name, expected] of Object.entries(PHP_ENGINE_LOCK.legalAssets)) {
    await assertHash(
      path.join(pack, name),
      expected,
      `Packed PHP legal asset ${name}`,
    );
  }
  const loader = await readFile(path.join(pack, "php-loader.mjs"), "utf8");
  if (
    !loader.startsWith(`${PATCHED_LOADER}\n`) ||
    loader.includes(LOADER_IMPORT)
  ) {
    throw new Error("Packed PHP loader patch is invalid.");
  }
  const metadata = JSON.parse(
    await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
  );
  const expected = await expectedMetadata(projectRoot, pack);
  if (JSON.stringify(metadata) !== JSON.stringify(expected)) {
    throw new Error("PHP engine metadata does not match the pinned pack.");
  }
  await smokePhp(pack, projectRoot);
  return metadata;
}

export async function buildPhpPack({
  projectRoot = root,
  output = path.join(root, ".engine-build", "php"),
  force = false,
} = {}) {
  const sources = await verifySources(projectRoot);
  const target = path.resolve(output);
  const existing = await lstat(target).catch(() => null);
  if (existing && !force) {
    throw new Error(`${target} exists; pass --force to replace it.`);
  }
  if (existing && (existing.isSymbolicLink() || !existing.isDirectory())) {
    throw new Error("PHP pack output must be a real directory when replaced.");
  }

  await mkdir(path.dirname(target), { recursive: true });
  const stage = await mkdtemp(path.join(path.dirname(target), ".php-pack-"));
  try {
    await copyFile(
      sources.wasm,
      path.join(stage, "php.wasm"),
      fsConstants.COPYFILE_EXCL,
    );
    await copyFile(
      sources.nativeInventory,
      path.join(stage, "native-components.json"),
      fsConstants.COPYFILE_EXCL,
    );
    for (const name of Object.keys(PHP_ENGINE_LOCK.legalAssets)) {
      await copyFile(
        sources.legalFiles[name],
        path.join(stage, name),
        fsConstants.COPYFILE_EXCL,
      );
    }
    const loaderSource = await readFile(sources.loader, "utf8");
    await writeFile(
      path.join(stage, "php-loader.mjs"),
      patchLoader(loaderSource),
      { flag: "wx", mode: 0o644 },
    );
    const metadata = await expectedMetadata(projectRoot, stage);
    await writeFile(
      path.join(stage, "engine-metadata.json"),
      `${JSON.stringify(metadata, null, 2)}\n`,
      { flag: "wx", mode: 0o644 },
    );
    const checksumLines = [];
    for (const name of CHECKSUM_FILES) {
      checksumLines.push(
        `${await sha256File(path.join(stage, name))}  ${name}`,
      );
    }
    await writeFile(
      path.join(stage, "SHA256SUMS"),
      `${checksumLines.join("\n")}\n`,
      { flag: "wx", mode: 0o644 },
    );
    await verifyPhpPack(stage, projectRoot);
    if (existing) await rm(target, { recursive: true });
    await rename(stage, target);
  } catch (error) {
    await rm(stage, { recursive: true, force: true });
    throw error;
  }
  return {
    output: target,
    metadata: await verifyPhpPack(target, projectRoot),
  };
}

export async function installPhpPack(source, projectRoot = root) {
  const sourcePath = await assertRealDirectory(source, "PHP staged pack");
  const target = path.join(projectRoot, "public", "engines", "php");
  if (sourcePath === (await realpath(target).catch(() => ""))) {
    throw new Error("PHP staged pack and installation target must differ.");
  }
  const metadata = await verifyPhpPack(sourcePath, projectRoot);
  await mkdir(path.dirname(target), { recursive: true });
  const stage = await mkdtemp(path.join(path.dirname(target), ".php-install-"));
  let backup;
  try {
    for (const name of PACK_FILES) {
      await copyFile(
        path.join(sourcePath, name),
        path.join(stage, name),
        fsConstants.COPYFILE_EXCL,
      );
    }
    await verifyPhpPack(stage, projectRoot);
    const existing = await lstat(target).catch(() => null);
    if (existing) {
      if (existing.isSymbolicLink() || !existing.isDirectory()) {
        throw new Error("Installed PHP pack target must be a real directory.");
      }
      backup = `${target}.backup-${process.pid}-${Date.now()}`;
      await rename(target, backup);
    }
    await rename(stage, target);
    if (backup) await rm(backup, { recursive: true });
  } catch (error) {
    await rm(stage, { recursive: true, force: true });
    if (backup && !(await lstat(target).catch(() => null))) {
      await rename(backup, target);
    }
    throw error;
  }
  return { output: target, metadata };
}

function parseArguments(arguments_) {
  let output = path.join(root, ".engine-build", "php");
  let force = false;
  for (let index = 0; index < arguments_.length; index += 1) {
    const argument = arguments_[index];
    if (argument === "--force") {
      if (force) throw new Error("--force was provided more than once.");
      force = true;
    } else if (argument === "--output") {
      const value = arguments_[index + 1];
      if (!value || value.startsWith("--")) {
        throw new Error("--output requires a directory.");
      }
      output = path.resolve(root, value);
      index += 1;
    } else {
      throw new Error(`Unknown PHP engine-pack argument: ${argument}`);
    }
  }
  return { output, force };
}

async function main() {
  const result = await buildPhpPack(parseArguments(process.argv.slice(2)));
  console.log(
    `Built verified ${result.metadata.engineVersion} pack at ${result.output}.`,
  );
}

if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
  await main();
}
