import { createHash, X509Certificate } from "node:crypto";
import { spawn } from "node:child_process";
import { cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createThirdPartyNotices } from "./generate-third-party-notices.mjs";
import { runReleasePreflight } from "./release-pwa-policy.mjs";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const distDirectory = join(root, "dist");
const candidateDirectory = join(distDirectory, "release-candidate");
const pwaDirectory = join(root, "apps", "pwa", "dist");
const pwaManifestPath = join(root, "apps", "pwa", "package.json");
const pwaNoticePath = join(root, "apps", "pwa", "public", "legal", "PWA-THIRD-PARTY-NOTICES.txt");
const windowsWorkDirectory = join(distDirectory, "windows-installer");
const androidDirectory = join(root, "apps", "pwa", "android");
const androidBuildDirectory = join(androidDirectory, "app", "build");
const androidPublicDirectory = join(androidDirectory, "app", "src", "main", "assets", "public");
const androidBundle = join(androidBuildDirectory, "outputs", "bundle", "release", "app-release.aab");
const allowedAndroidExtras = ["cordova.js", "cordova_plugins.js"];
const androidPackageName = "com.makecdev.coderemotemanager";
const androidMinSdk = 26;
const androidTargetSdk = 36;
const androidAbiOrder = ["arm64-v8a", "armeabi-v7a", "x86", "x86_64"];
const androidSigningEnvironment = {
  keyStore: "CODE_REMOTE_MANAGER_ANDROID_KEYSTORE",
  keyAlias: "CODE_REMOTE_MANAGER_ANDROID_KEY_ALIAS",
  storePassword: "CODE_REMOTE_MANAGER_ANDROID_STORE_PASSWORD",
  keyPassword: "CODE_REMOTE_MANAGER_ANDROID_KEY_PASSWORD",
  certificateSha256: "CODE_REMOTE_MANAGER_ANDROID_CERT_SHA256",
};

function releaseError(code) {
  return new Error(code);
}

export async function assertPwaNoticesCurrent({
  modules = join(root, "node_modules"),
  manifestPath = pwaManifestPath,
  noticePath = pwaNoticePath,
} = {}) {
  const [generated, committed] = await Promise.all([
    createThirdPartyNotices({ modules, manifestPath }),
    readFile(noticePath, "utf8").catch(() => ""),
  ]);
  if (generated !== committed) throw releaseError("release_pwa_notices_stale");
}

function normalizedCertificateSha256(value) {
  const normalized = String(value ?? "").replaceAll(":", "").toLowerCase();
  if (!/^[a-f0-9]{64}$/.test(normalized)) throw releaseError("release_android_certificate_invalid");
  return normalized;
}

function certificateMetadataFromPem(value) {
  const certificate = String(value).match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/)?.[0];
  if (!certificate) throw releaseError("release_android_certificate_invalid");
  try {
    const parsed = new X509Certificate(certificate);
    return {
      subject: parsed.subject,
      certificateSha256: normalizedCertificateSha256(parsed.fingerprint256),
    };
  } catch {
    throw releaseError("release_android_certificate_invalid");
  }
}

export function validateAndroidCertificateMetadata(metadata, expectedCertificateSha256) {
  const subject = String(metadata?.subject ?? "");
  const keyAlias = String(metadata?.keyAlias ?? "");
  if (/debug/i.test(`${subject}\n${keyAlias}`) ||
      !/(?:^|[\n,])\s*CN\s*=\s*[^,\n]+/i.test(subject)) {
    throw releaseError("release_android_debug_certificate");
  }
  const certificateSha256 = normalizedCertificateSha256(metadata?.certificateSha256);
  if (certificateSha256 !== normalizedCertificateSha256(expectedCertificateSha256)) {
    throw releaseError("release_android_certificate_mismatch");
  }
  return certificateSha256;
}

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

async function filesBelow(directory, current = directory) {
  const files = [];
  for (const entry of await readdir(current, { withFileTypes: true })) {
    const path = join(current, entry.name);
    if (entry.isDirectory()) files.push(...await filesBelow(directory, path));
    else if (entry.isFile()) files.push(path);
    else throw releaseError("release_tree_contains_unsupported_entry");
  }
  return files;
}

export async function hashFile(path) {
  const data = await readFile(path);
  return { sha256: sha256(data), bytes: data.byteLength };
}

export async function hashTree(directory) {
  const resolved = resolve(directory);
  const files = [];
  for (const path of await filesBelow(resolved)) {
    const hashed = await hashFile(path);
    files.push({
      path: relative(resolved, path).replaceAll("\\", "/"),
      ...hashed,
    });
  }
  files.sort((left, right) => left.path.localeCompare(right.path));
  const digest = files.map((file) => `${file.path}\0${file.bytes}\0${file.sha256}\n`).join("");
  return { sha256: sha256(digest), fileCount: files.length, files };
}

export function elfLoadAlignments(value) {
  const buffer = Buffer.from(value);
  if (buffer.length < 52 || !buffer.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))) {
    throw releaseError("release_android_invalid_elf");
  }
  const elfClass = buffer[4];
  const littleEndian = buffer[5] === 1;
  if (![1, 2].includes(elfClass) || ![1, 2].includes(buffer[5])) throw releaseError("release_android_invalid_elf");
  if ((elfClass === 2 && buffer.length < 64) || (elfClass === 1 && buffer.length < 52)) {
    throw releaseError("release_android_invalid_elf");
  }
  const readUInt16 = (offset) => littleEndian ? buffer.readUInt16LE(offset) : buffer.readUInt16BE(offset);
  const readUInt32 = (offset) => littleEndian ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset);
  const readUInt64 = (offset) => Number(littleEndian ? buffer.readBigUInt64LE(offset) : buffer.readBigUInt64BE(offset));
  const programOffset = elfClass === 2 ? readUInt64(32) : readUInt32(28);
  const entrySize = readUInt16(elfClass === 2 ? 54 : 42);
  const entryCount = readUInt16(elfClass === 2 ? 56 : 44);
  const minimumEntrySize = elfClass === 2 ? 56 : 32;
  if (!Number.isSafeInteger(programOffset) || entrySize < minimumEntrySize || entryCount < 1 ||
      programOffset + (entrySize * entryCount) > buffer.length) {
    throw releaseError("release_android_invalid_elf");
  }
  const alignments = [];
  for (let index = 0; index < entryCount; index += 1) {
    const offset = programOffset + (entrySize * index);
    if (readUInt32(offset) !== 1) continue;
    const alignment = elfClass === 2 ? readUInt64(offset + 48) : readUInt32(offset + 28);
    if (!Number.isSafeInteger(alignment) || alignment < 1) throw releaseError("release_android_invalid_elf");
    alignments.push(alignment);
  }
  if (!alignments.length) throw releaseError("release_android_invalid_elf");
  return alignments;
}

function sameValues(left, right) {
  return left.length === right.length && left.every((value, index) => value === right[index]);
}

export function validateAndroidBundleMetadata(metadata, version) {
  const versionCode = Number(metadata?.versionCode);
  const minSdk = Number(metadata?.minSdk);
  const targetSdk = Number(metadata?.targetSdk);
  if (metadata?.packageName !== androidPackageName || metadata?.versionName !== version ||
      !Number.isInteger(versionCode) || versionCode < 1 || minSdk !== androidMinSdk || targetSdk !== androidTargetSdk) {
    throw releaseError("release_android_manifest_mismatch");
  }
  if (metadata?.debuggable !== false || metadata?.usesCleartextTraffic !== false || metadata?.allowBackup !== false) {
    throw releaseError("release_android_manifest_hardening_invalid");
  }
  if (metadata?.pageAlignment !== "PAGE_ALIGNMENT_16K") throw releaseError("release_android_page_alignment_invalid");
  if (!Array.isArray(metadata?.nativeLibraries) || !metadata.nativeLibraries.length) {
    throw releaseError("release_android_native_libraries_missing");
  }
  if (!Number.isInteger(metadata?.thirdPartyDependencyCount) || metadata.thirdPartyDependencyCount < 1 ||
      metadata?.capacitorLicenseCount !== 7) {
    throw releaseError("release_android_license_notices_missing");
  }
  const librariesByAbi = new Map(androidAbiOrder.map((abi) => [abi, []]));
  for (const library of metadata.nativeLibraries) {
    if (!librariesByAbi.has(library?.abi) || basename(String(library?.name ?? "")) !== library?.name ||
        !String(library.name).endsWith(".so") || !Array.isArray(library.loadAlignments) || !library.loadAlignments.length) {
      throw releaseError("release_android_native_library_invalid");
    }
    if (library.loadAlignments.some((alignment) => !Number.isInteger(alignment) || alignment < 1)) {
      throw releaseError("release_android_native_library_invalid");
    }
    if (["arm64-v8a", "x86_64"].includes(library.abi) &&
        library.loadAlignments.some((alignment) => alignment < 16384)) {
      throw releaseError("release_android_elf_alignment_invalid");
    }
    librariesByAbi.get(library.abi).push(library.name);
  }
  for (const libraries of librariesByAbi.values()) libraries.sort();
  for (const [thirtyTwoBit, sixtyFourBit] of [["armeabi-v7a", "arm64-v8a"], ["x86", "x86_64"]]) {
    const legacy = librariesByAbi.get(thirtyTwoBit);
    const modern = librariesByAbi.get(sixtyFourBit);
    if (legacy.length && !modern.length) throw releaseError("release_android_64_bit_required");
    if (!sameValues(legacy, modern)) throw releaseError("release_android_native_library_mismatch");
  }
  const abis = androidAbiOrder.filter((abi) => librariesByAbi.get(abi).length);
  if (!abis.includes("arm64-v8a") && !abis.includes("x86_64")) throw releaseError("release_android_64_bit_required");
  return {
    packageName: androidPackageName,
    versionCode,
    versionName: version,
    minSdk,
    targetSdk,
    debuggable: false,
    usesCleartextTraffic: false,
    allowBackup: false,
    abis,
    nativeLibraryCount: metadata.nativeLibraries.length,
    thirdPartyDependencyCount: metadata.thirdPartyDependencyCount,
    capacitorLicenseCount: metadata.capacitorLicenseCount,
    pageSize16Kb: "verified",
  };
}

export async function verifyPwaCopy(sourceDirectory, targetDirectory, { allowedExtra = [] } = {}) {
  const [source, target] = await Promise.all([hashTree(sourceDirectory), hashTree(targetDirectory)]);
  const targetFiles = new Map(target.files.map((file) => [file.path, file]));
  for (const file of source.files) {
    const copy = targetFiles.get(file.path);
    if (!copy || copy.bytes !== file.bytes || copy.sha256 !== file.sha256) throw releaseError("release_pwa_mismatch");
    targetFiles.delete(file.path);
  }
  const allowed = new Set(allowedExtra);
  if ([...targetFiles.keys()].some((path) => !allowed.has(path))) throw releaseError("release_pwa_mismatch");
  return source;
}

function safeArtifactFile(file) {
  if (typeof file !== "string" || !file || basename(file) !== file || /[\\/]/.test(file)) {
    throw releaseError("release_manifest_invalid_artifact");
  }
  return file;
}

function safeSource(source) {
  if (!/^[a-f0-9]{40}$/.test(source?.commit ?? "") || !/^[a-f0-9]{40}$/.test(source?.tree ?? "")) {
    throw releaseError("release_manifest_invalid_source");
  }
  return { commit: source.commit, tree: source.tree };
}

function safeToolchain(toolchain) {
  const safe = {};
  for (const [name, value] of Object.entries(toolchain ?? {})) {
    const text = String(value);
    if (!/^[a-zA-Z0-9 ._+()-]{1,100}$/.test(text) || /(?:token|secret|credential|auth\.json)/i.test(text)) {
      throw releaseError("release_manifest_unsafe_toolchain");
    }
    safe[name] = text;
  }
  return safe;
}

async function artifactManifest(candidateDir, input, pwaSha256) {
  const file = safeArtifactFile(input.file);
  const signing = String(input.signing);
  if (!new Set(["unsigned", "signed", "not-applicable"]).has(signing)) throw releaseError("release_manifest_invalid_signing");
  const hashed = await hashFile(join(candidateDir, file));
  return {
    file,
    ...hashed,
    signing,
    ...(signing === "not-applicable" ? {} : { embeddedPwaSha256: pwaSha256 }),
  };
}

function safeAndroidRelease(android, version) {
  const abis = Array.isArray(android?.abis) ? [...android.abis] : [];
  const expectedAbis = androidAbiOrder.filter((abi) => abis.includes(abi));
  if (!sameValues(abis, expectedAbis) || new Set(abis).size !== abis.length ||
      (abis.includes("armeabi-v7a") && !abis.includes("arm64-v8a")) ||
      (abis.includes("x86") && !abis.includes("x86_64")) ||
      (!abis.includes("arm64-v8a") && !abis.includes("x86_64")) ||
      android?.packageName !== androidPackageName || android?.versionName !== version ||
      !Number.isInteger(android?.versionCode) || android.versionCode < 1 ||
      android?.minSdk !== androidMinSdk || android?.targetSdk !== androidTargetSdk ||
      !Number.isInteger(android?.nativeLibraryCount) || android.nativeLibraryCount < 1 ||
      !Number.isInteger(android?.thirdPartyDependencyCount) || android.thirdPartyDependencyCount < 1 ||
      android?.capacitorLicenseCount !== 7 ||
      android?.debuggable !== false ||
      android?.usesCleartextTraffic !== false ||
      android?.allowBackup !== false ||
      android?.pageSize16Kb !== "verified") {
    throw releaseError("release_manifest_invalid_android");
  }
  return {
    packageName: androidPackageName,
    versionCode: android.versionCode,
    versionName: version,
    minSdk: androidMinSdk,
    targetSdk: androidTargetSdk,
    debuggable: false,
    usesCleartextTraffic: false,
    allowBackup: false,
    abis,
    nativeLibraryCount: android.nativeLibraryCount,
    thirdPartyDependencyCount: android.thirdPartyDependencyCount,
    capacitorLicenseCount: android.capacitorLicenseCount,
    pageSize16Kb: "verified",
  };
}

export function androidDistributionPolicy({ signing, certificateSha256 } = {}) {
  if (signing === "unsigned") {
    return {
      channel: "pre-signing-candidate",
      signature: "unsigned",
      verification: "bundletool-manifest-and-sha256",
    };
  }
  if (signing === "signed") {
    return {
      channel: "play-upload-candidate",
      signature: "signed",
      verification: "jarsigner-certificate-and-sha256-manifest",
      certificateSha256: normalizedCertificateSha256(certificateSha256),
    };
  }
  throw releaseError("release_manifest_invalid_android_signing");
}

export async function createReleaseManifest({ candidateDir, version, source, pwa, toolchain, android, artifacts }) {
  if (!/^\d+\.\d+\.\d+$/.test(version ?? "") || !/^[a-f0-9]{64}$/.test(pwa?.sha256 ?? "")) {
    throw releaseError("release_manifest_invalid_metadata");
  }
  const androidRelease = safeAndroidRelease(android, version);
  const artifactEntries = {};
  for (const name of ["source", "windows", "android"]) {
    artifactEntries[name] = await artifactManifest(candidateDir, artifacts?.[name] ?? {}, pwa.sha256);
  }
  const androidDistribution = androidDistributionPolicy({
    signing: artifactEntries.android.signing,
    certificateSha256: android?.certificateSha256,
  });
  return {
    schemaVersion: 2,
    product: "Code Remote Manager",
    version,
    source: safeSource(source),
    pwa: {
      sha256: pwa.sha256,
      fileCount: pwa.fileCount,
      files: pwa.files.map((file) => ({ path: file.path, bytes: file.bytes, sha256: file.sha256 })),
    },
    distribution: {
      windows: {
        channel: "public-unsigned",
        signature: "unsigned",
        verification: "sha256-manifest",
      },
      android: androidDistribution,
    },
    android: androidRelease,
    toolchain: safeToolchain(toolchain),
    artifacts: artifactEntries,
  };
}

export async function verifyReleaseManifest({ candidateDir, manifest, source }) {
  if (manifest?.schemaVersion !== 2 || manifest.product !== "Code Remote Manager" ||
      !/^\d+\.\d+\.\d+$/.test(manifest.version ?? "")) throw releaseError("release_manifest_invalid");
  const expectedSource = safeSource(source);
  if (manifest.source?.commit !== expectedSource.commit || manifest.source?.tree !== expectedSource.tree) {
    throw releaseError("release_source_mismatch");
  }
  if (!/^[a-f0-9]{64}$/.test(manifest.pwa?.sha256 ?? "") || manifest.pwa?.files?.length !== manifest.pwa?.fileCount) {
    throw releaseError("release_manifest_invalid_pwa");
  }
  const windowsDistribution = manifest.distribution?.windows;
  if (windowsDistribution?.channel !== "public-unsigned" ||
      windowsDistribution?.signature !== "unsigned" ||
      windowsDistribution?.verification !== "sha256-manifest" ||
      manifest.artifacts?.windows?.signing !== "unsigned") {
    throw releaseError("release_manifest_invalid_windows_distribution");
  }
  const androidDistribution = manifest.distribution?.android;
  safeAndroidRelease(manifest.android, manifest.version);
  const expectedAndroidDistribution = androidDistributionPolicy({
    signing: manifest.artifacts?.android?.signing,
    certificateSha256: androidDistribution?.certificateSha256,
  });
  if (!androidDistribution ||
      Object.keys(androidDistribution).length !== Object.keys(expectedAndroidDistribution).length ||
      Object.entries(expectedAndroidDistribution).some(([name, value]) => androidDistribution[name] !== value)) {
    throw releaseError("release_manifest_invalid_android_distribution");
  }
  for (const name of ["source", "windows", "android"]) {
    const artifact = manifest.artifacts?.[name];
    const file = safeArtifactFile(artifact?.file);
    const actual = await hashFile(join(candidateDir, file));
    if (actual.sha256 !== artifact.sha256 || actual.bytes !== artifact.bytes) throw releaseError("release_artifact_hash_mismatch");
    if (name !== "source" && artifact.embeddedPwaSha256 !== manifest.pwa.sha256) throw releaseError("release_pwa_mismatch");
  }
  return true;
}

async function verifyTreeAgainstPwa(directory, pwa, { allowedExtra = [] } = {}) {
  const target = await hashTree(directory);
  const targetFiles = new Map(target.files.map((file) => [file.path, file]));
  for (const file of pwa.files) {
    const copy = targetFiles.get(file.path);
    if (!copy || copy.bytes !== file.bytes || copy.sha256 !== file.sha256) throw releaseError("release_pwa_mismatch");
    targetFiles.delete(file.path);
  }
  const allowed = new Set(allowedExtra);
  if ([...targetFiles.keys()].some((path) => !allowed.has(path))) throw releaseError("release_pwa_mismatch");
}

function commandForPlatform(command, args) {
  if (process.platform === "win32" && /\.(?:cmd|bat)$/i.test(command)) {
    return { command: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", command, ...args] };
  }
  return { command, args };
}

async function run(command, args, { cwd = root, capture = false, allowFailure = false, env = {} } = {}) {
  const executable = commandForPlatform(command, args);
  const commandError = releaseError(`release_command_failed_${basename(command).replace(/\W/g, "_")}`);
  const child = spawn(executable.command, executable.args, {
    cwd,
    env: { ...process.env, ...env },
    windowsHide: true,
    stdio: capture || allowFailure ? ["ignore", "pipe", "pipe"] : "inherit",
  });
  let stdout = "";
  let stderr = "";
  if (child.stdout) child.stdout.on("data", (chunk) => { stdout += String(chunk); });
  if (child.stderr) child.stderr.on("data", (chunk) => { stderr += String(chunk); });
  const code = await new Promise((resolveExit, reject) => {
    child.once("error", () => reject(commandError));
    child.once("exit", resolveExit);
  });
  if (code !== 0 && !allowFailure) throw commandError;
  return { code, stdout: stdout.trim(), stderr: stderr.trim() };
}

async function gitValue(args) {
  return (await run("git.exe", args, { capture: true })).stdout;
}

async function cleanSource() {
  const status = await gitValue(["status", "--porcelain=v1", "--untracked-files=all"]);
  if (status) throw releaseError("release_source_not_clean");
  return {
    commit: await gitValue(["rev-parse", "HEAD"]),
    tree: await gitValue(["rev-parse", "HEAD^{tree}"]),
  };
}

function firstVersionLine(value) {
  return value.split(/\r?\n/, 1)[0].replace(/^v/, "").trim();
}

async function toolchainMetadata() {
  const [npm, dotnet, java, git, wrapper, androidBuild, variables] = await Promise.all([
    run("npm.cmd", ["--version"], { capture: true }),
    run("dotnet.exe", ["--version"], { capture: true }),
    run("java.exe", ["--version"], { capture: true }),
    run("git.exe", ["--version"], { capture: true }),
    readFile(join(androidDirectory, "gradle", "wrapper", "gradle-wrapper.properties"), "utf8"),
    readFile(join(androidDirectory, "build.gradle"), "utf8"),
    readFile(join(androidDirectory, "variables.gradle"), "utf8"),
  ]);
  return {
    node: process.versions.node,
    npm: firstVersionLine(npm.stdout),
    dotnet: firstVersionLine(dotnet.stdout),
    java: firstVersionLine(java.stdout),
    git: firstVersionLine(git.stdout).replace(/^git version /, ""),
    gradle: wrapper.match(/gradle-([0-9.]+)-/)?.[1] ?? "Unavailable",
    androidGradlePlugin: androidBuild.match(/com\.android\.tools\.build:gradle:([0-9.]+)/)?.[1] ?? "Unavailable",
    compileSdk: variables.match(/compileSdkVersion = (\d+)/)?.[1] ?? "Unavailable",
    targetSdk: variables.match(/targetSdkVersion = (\d+)/)?.[1] ?? "Unavailable",
    innoSetup: "6",
  };
}

function encodedPowerShell(script) {
  return Buffer.from(script, "utf16le").toString("base64");
}

async function assertWindowsUnsigned(path) {
  const encodedPath = Buffer.from(path, "utf8").toString("base64");
  const script = `$p=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String("${encodedPath}"));if((Get-AuthenticodeSignature -LiteralPath $p).Status -ne "NotSigned"){exit 7}`;
  await run("powershell.exe", ["-NoProfile", "-EncodedCommand", encodedPowerShell(script)], { capture: true });
}

async function androidSigningInputs() {
  const keyStore = process.env[androidSigningEnvironment.keyStore];
  const keyAlias = process.env[androidSigningEnvironment.keyAlias];
  const expectedCertificateSha256 = normalizedCertificateSha256(
    process.env[androidSigningEnvironment.certificateSha256],
  );
  const storePassword = process.env[androidSigningEnvironment.storePassword];
  const keyPassword = process.env[androidSigningEnvironment.keyPassword];
  if (!keyStore || !isAbsolute(keyStore) || !/^[a-zA-Z0-9._-]{1,100}$/.test(keyAlias ?? "") ||
      !storePassword || !keyPassword || /[\r\n]/.test(storePassword) || /[\r\n]/.test(keyPassword)) {
    throw releaseError("release_android_signing_input_missing");
  }
  const keyStoreState = await stat(keyStore).catch(() => null);
  if (!keyStoreState?.isFile()) throw releaseError("release_android_signing_input_missing");
  const certificate = await run("keytool.exe", [
    "-J-Duser.language=en", "-J-Duser.country=US", "-exportcert", "-rfc",
    "-keystore", keyStore, "-alias", keyAlias,
    "-storepass:env", androidSigningEnvironment.storePassword,
  ], { capture: true });
  validateAndroidCertificateMetadata({
    ...certificateMetadataFromPem(certificate.stdout),
    keyAlias,
  }, expectedCertificateSha256);
  return { keyStore, keyAlias, certificateSha256: expectedCertificateSha256 };
}

async function assertAndroidUnsigned(path) {
  const verification = await run("jarsigner.exe", [
    "-J-Duser.language=en", "-J-Duser.country=US", "-verify", "-verbose", path,
  ], { capture: true, allowFailure: true });
  const output = `${verification.stdout}\n${verification.stderr}`;
  if (!/jar is unsigned/i.test(output) || /jar verified\./i.test(output)) {
    throw releaseError("release_android_signature_invalid");
  }
}

async function assertAndroidSigned(path, expectedCertificateSha256) {
  const verification = await run("jarsigner.exe", [
    "-J-Duser.language=en", "-J-Duser.country=US", "-verify", "-verbose", "-certs", path,
  ], { capture: true });
  const output = `${verification.stdout}\n${verification.stderr}`;
  if (!/jar verified\./i.test(output) || /(?:jar is unsigned|unsigned entries)/i.test(output)) {
    throw releaseError("release_android_signature_invalid");
  }
  const certificate = await run("keytool.exe", [
    "-J-Duser.language=en", "-J-Duser.country=US", "-printcert", "-jarfile", path, "-rfc",
  ], { capture: true });
  return validateAndroidCertificateMetadata(
    certificateMetadataFromPem(certificate.stdout),
    expectedCertificateSha256,
  );
}

async function signAndroid(path, signing) {
  await run("jarsigner.exe", [
    "-J-Duser.language=en", "-J-Duser.country=US",
    "-keystore", signing.keyStore,
    "-storepass:env", androidSigningEnvironment.storePassword,
    "-keypass:env", androidSigningEnvironment.keyPassword,
    path,
    signing.keyAlias,
  ], { capture: true });
  return assertAndroidSigned(path, signing.certificateSha256);
}

async function runBundletool(action, bundlePath, extraEnvironment = {}) {
  const result = await run("gradlew.bat", ["-q", "releaseBundletool", "--no-daemon"], {
    cwd: androidDirectory,
    capture: true,
    env: {
      CODE_REMOTE_MANAGER_BUNDLETOOL_ACTION: action,
      CODE_REMOTE_MANAGER_ANDROID_BUNDLE_PATH: resolve(bundlePath),
      ...extraEnvironment,
    },
  });
  return result.stdout;
}

function manifestAttribute(manifest, name) {
  return manifest.match(new RegExp(`(?:android:)?${name}="([^"]+)"`))?.[1] ?? "Unavailable";
}

function applicationAttribute(manifest, name) {
  const application = manifest.match(/<application\b([^>]*)>/)?.[1] ?? "";
  return application.match(new RegExp(`android:${name}="([^"]+)"`))?.[1];
}

export function validateAndroidManifest(manifest) {
  const packageName = manifestAttribute(manifest, "package");
  const debuggable = applicationAttribute(manifest, "debuggable");
  const usesCleartextTraffic = applicationAttribute(manifest, "usesCleartextTraffic");
  const allowBackup = applicationAttribute(manifest, "allowBackup");
  if (packageName !== androidPackageName) throw releaseError("release_android_manifest_mismatch");
  if (![undefined, "false"].includes(debuggable) ||
      usesCleartextTraffic !== "false" ||
      allowBackup !== "false") {
    throw releaseError("release_android_manifest_hardening_invalid");
  }
  return {
    packageName,
    debuggable: false,
    usesCleartextTraffic: false,
    allowBackup: false,
  };
}

export async function readAndroidBundleMetadata(bundlePath, pwa) {
  const resolvedBundlePath = resolve(bundlePath);
  await runBundletool("validate", resolvedBundlePath);
  const [manifest, config] = await Promise.all([
    runBundletool("manifest", resolvedBundlePath),
    runBundletool("config", resolvedBundlePath),
  ]);
  const temporary = await mkdtemp(join(tmpdir(), "crm-aab-"));
  try {
    await run("jar.exe", ["xf", resolvedBundlePath], { cwd: temporary, capture: true });
    await verifyTreeAgainstPwa(join(temporary, "base", "assets", "public"), pwa, { allowedExtra: allowedAndroidExtras });
    const report = JSON.parse(await readFile(
      join(temporary, "base", "assets", "android-third-party-licenses.json"),
      "utf8",
    ).catch(() => "{}"));
    const dependencies = Array.isArray(report.dependencies) ? report.dependencies : [];
    const validDependencies = dependencies.filter((dependency) =>
      typeof dependency?.moduleName === "string" && dependency.moduleName &&
      typeof dependency?.moduleVersion === "string" && dependency.moduleVersion &&
      typeof dependency?.moduleLicense === "string" && dependency.moduleLicense &&
      typeof dependency?.moduleLicenseUrl === "string" && /^https?:\/\//.test(dependency.moduleLicenseUrl));
    const capacitorLicenseCount = (await readdir(
      join(temporary, "base", "assets", "capacitor"),
      { withFileTypes: true },
    ).catch(() => [])).filter((entry) => entry.isFile() && /-LICENSE\.txt$/.test(entry.name)).length;
    const nativeLibraries = [];
    for (const path of await filesBelow(temporary)) {
      const bundleRelativePath = relative(temporary, path).replaceAll("\\", "/");
      const match = bundleRelativePath.match(/(?:^|\/)lib\/([^/]+)\/([^/]+\.so)$/);
      if (!match) continue;
      nativeLibraries.push({
        abi: match[1],
        name: match[2],
        loadAlignments: elfLoadAlignments(await readFile(path)),
      });
    }
    const manifestPolicy = validateAndroidManifest(manifest);
    return {
      ...manifestPolicy,
      versionCode: manifestAttribute(manifest, "versionCode"),
      versionName: manifestAttribute(manifest, "versionName"),
      minSdk: manifestAttribute(manifest, "minSdkVersion"),
      targetSdk: manifestAttribute(manifest, "targetSdkVersion"),
      pageAlignment: config.match(/PAGE_ALIGNMENT_[A-Z0-9_]+/)?.[0] ?? "Unavailable",
      thirdPartyDependencyCount: validDependencies.length === dependencies.length ? dependencies.length : 0,
      capacitorLicenseCount,
      nativeLibraries,
    };
  } finally {
    await rm(temporary, { recursive: true, force: true });
  }
}

export async function inspectAndroidBundle(bundlePath, pwa, version) {
  return validateAndroidBundleMetadata(await readAndroidBundleMetadata(bundlePath, pwa), version);
}

async function writeReleaseMetadata(version, source) {
  const metadata = { schemaVersion: 1, product: "Code Remote Manager", version, source };
  await writeFile(join(pwaDirectory, "release-metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`);
}

function artifactNames(version) {
  return {
    source: `CodeRemoteManager-${version}-source.zip`,
    windows: "CodeRemoteManagerSetup.exe",
    android: `CodeRemoteManager-${version}-android.aab`,
  };
}

async function prepareRelease() {
  await runReleasePreflight();
  await assertPwaNoticesCurrent();
  const source = await cleanSource();
  const version = JSON.parse(await readFile(join(root, "package.json"), "utf8")).version;
  const names = artifactNames(version);
  await rm(distDirectory, { recursive: true, force: true });
  await mkdir(candidateDirectory, { recursive: true });
  await rm(androidBuildDirectory, { recursive: true, force: true });
  await rm(androidPublicDirectory, { recursive: true, force: true });
  try {
    await run("npm.cmd", ["--workspace", "apps/pwa", "run", "build"]);
    await writeReleaseMetadata(version, source);
    const pwa = await hashTree(pwaDirectory);
    await run("powershell.exe", [
      "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", join(root, "windows-installer", "build-installer.ps1"),
      "-UseExistingPwa", "-ReleaseCandidate",
    ]);
    await assertWindowsUnsigned(join(candidateDirectory, names.windows));

    await run("npm.cmd", ["--workspace", "apps/pwa", "exec", "--", "cap", "sync", "android"]);
    await verifyPwaCopy(pwaDirectory, androidPublicDirectory, { allowedExtra: allowedAndroidExtras });
    await run("gradlew.bat", ["bundleRelease", "--no-daemon"], { cwd: androidDirectory });
    const android = await inspectAndroidBundle(androidBundle, pwa, version);
    await cp(androidBundle, join(candidateDirectory, names.android));

    await run("git.exe", ["archive", "--format=zip", `--output=${join(candidateDirectory, names.source)}`, source.commit]);
    const manifest = await createReleaseManifest({
      candidateDir: candidateDirectory,
      version,
      source,
      pwa,
      toolchain: await toolchainMetadata(),
      android,
      artifacts: {
        source: { file: names.source, signing: "not-applicable" },
        windows: { file: names.windows, signing: "unsigned" },
        android: { file: names.android, signing: "unsigned" },
      },
    });
    await writeFile(join(candidateDirectory, "release-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
    await verifyReleaseManifest({ candidateDir: candidateDirectory, manifest, source });
    await assertAndroidUnsigned(join(candidateDirectory, names.android));
    const copiedAndroid = await inspectAndroidBundle(join(candidateDirectory, names.android), manifest.pwa, manifest.version);
    if (JSON.stringify(copiedAndroid) !== JSON.stringify(manifest.android)) {
      throw releaseError("release_android_manifest_mismatch");
    }
    console.log(`Verified release candidate ${version} from ${source.commit}.`);
  } catch (error) {
    await rm(candidateDirectory, { recursive: true, force: true });
    throw error;
  } finally {
    await rm(windowsWorkDirectory, { recursive: true, force: true });
    await rm(androidBuildDirectory, { recursive: true, force: true });
    await rm(androidPublicDirectory, { recursive: true, force: true });
  }
}

async function verifyPreparedRelease() {
  await assertPwaNoticesCurrent();
  const source = await cleanSource();
  const manifest = JSON.parse(await readFile(join(candidateDirectory, "release-manifest.json"), "utf8"));
  await verifyReleaseManifest({ candidateDir: candidateDirectory, manifest, source });
  await assertWindowsUnsigned(join(candidateDirectory, manifest.artifacts.windows.file));
  const androidPath = join(candidateDirectory, manifest.artifacts.android.file);
  if (manifest.distribution.android.signature === "unsigned") await assertAndroidUnsigned(androidPath);
  else await assertAndroidSigned(androidPath, manifest.distribution.android.certificateSha256);
  const android = await inspectAndroidBundle(androidPath, manifest.pwa, manifest.version);
  if (JSON.stringify(android) !== JSON.stringify(manifest.android)) throw releaseError("release_android_manifest_mismatch");
  console.log(`Verified release candidate ${manifest.version} from ${source.commit}.`);
}

async function signPreparedAndroidRelease() {
  await assertPwaNoticesCurrent();
  const source = await cleanSource();
  const manifestPath = join(candidateDirectory, "release-manifest.json");
  const originalManifestText = await readFile(manifestPath, "utf8");
  const manifest = JSON.parse(originalManifestText);
  await verifyReleaseManifest({ candidateDir: candidateDirectory, manifest, source });
  if (manifest.distribution.android.signature !== "unsigned" ||
      manifest.artifacts.android.signing !== "unsigned") {
    throw releaseError("release_android_signing_state_invalid");
  }
  const androidPath = join(candidateDirectory, manifest.artifacts.android.file);
  await assertAndroidUnsigned(androidPath);
  const signing = await androidSigningInputs();
  const temporary = await mkdtemp(join(tmpdir(), "crm-android-signing-"));
  const stagedCandidate = join(temporary, "candidate");
  const originalAndroid = join(temporary, "original.aab");
  try {
    await mkdir(stagedCandidate, { recursive: true });
    await cp(androidPath, originalAndroid);
    for (const artifact of Object.values(manifest.artifacts)) {
      await cp(join(candidateDirectory, artifact.file), join(stagedCandidate, artifact.file));
    }
    const stagedAndroid = join(stagedCandidate, manifest.artifacts.android.file);
    await signAndroid(stagedAndroid, signing);
    const android = await inspectAndroidBundle(stagedAndroid, manifest.pwa, manifest.version);
    if (JSON.stringify(android) !== JSON.stringify(manifest.android)) {
      throw releaseError("release_android_manifest_mismatch");
    }
    const signedManifest = await createReleaseManifest({
      candidateDir: stagedCandidate,
      version: manifest.version,
      source,
      pwa: manifest.pwa,
      toolchain: manifest.toolchain,
      android: { ...android, certificateSha256: signing.certificateSha256 },
      artifacts: {
        source: { file: manifest.artifacts.source.file, signing: "not-applicable" },
        windows: { file: manifest.artifacts.windows.file, signing: "unsigned" },
        android: { file: manifest.artifacts.android.file, signing: "signed" },
      },
    });
    await verifyReleaseManifest({ candidateDir: stagedCandidate, manifest: signedManifest, source });
    await assertAndroidSigned(stagedAndroid, signing.certificateSha256);
    try {
      await cp(stagedAndroid, androidPath);
      await writeFile(manifestPath, `${JSON.stringify(signedManifest, null, 2)}\n`);
      await verifyReleaseManifest({ candidateDir: candidateDirectory, manifest: signedManifest, source });
      await assertAndroidSigned(androidPath, signing.certificateSha256);
    } catch (error) {
      await cp(originalAndroid, androidPath);
      await writeFile(manifestPath, originalManifestText);
      throw error;
    }
    console.log(`Signed and verified Android candidate ${manifest.version} from ${source.commit}.`);
  } finally {
    await rm(temporary, { recursive: true, force: true });
  }
}

async function androidSdkDirectory() {
  const properties = await readFile(join(androidDirectory, "local.properties"), "utf8").catch(() => "");
  const encoded = properties.match(/^sdk\.dir=(.+)$/m)?.[1]?.trim();
  if (!encoded) throw releaseError("release_android_sdk_missing");
  const directory = encoded.replace(/\\([\\:= ])/g, "$1");
  const state = await stat(directory).catch(() => null);
  if (!state?.isDirectory()) throw releaseError("release_android_sdk_missing");
  return directory;
}

async function androidBuildTools(sdkDirectory) {
  const directory = join(sdkDirectory, "build-tools");
  const versions = (await readdir(directory, { withFileTypes: true }).catch(() => []))
    .filter((entry) => entry.isDirectory())
    .map((entry) => entry.name)
    .sort((left, right) => right.localeCompare(left, undefined, { numeric: true }));
  for (const version of versions) {
    const aapt2 = join(directory, version, "aapt2.exe");
    const zipalign = join(directory, version, "zipalign.exe");
    const apksigner = join(directory, version, "apksigner.bat");
    const [aapt2State, zipalignState, apksignerState] = await Promise.all([
      stat(aapt2).catch(() => null),
      stat(zipalign).catch(() => null),
      stat(apksigner).catch(() => null),
    ]);
    if (aapt2State?.isFile() && zipalignState?.isFile() && apksignerState?.isFile()) {
      return { aapt2, zipalign, apksigner };
    }
  }
  throw releaseError("release_android_build_tools_missing");
}

async function connectedAndroidDevice(adb) {
  const result = await run(adb, ["devices", "-l"], { capture: true });
  const devices = result.stdout.split(/\r?\n/).slice(1).map((line) => line.trim()).filter(Boolean);
  const ready = devices.filter((line) => /\sdevice(?:\s|$)/.test(line));
  if (devices.length !== 1 || ready.length !== 1) throw releaseError("release_android_device_unavailable");
  const rawModel = ready[0].match(/\bmodel:([^\s]+)/)?.[1] ?? "Unavailable";
  return {
    serial: ready[0].split(/\s+/, 1)[0],
    model: /^[a-zA-Z0-9._+-]{1,80}$/.test(rawModel) ? rawModel : "Unavailable",
  };
}

async function verifyAndroidDevice() {
  const source = await cleanSource();
  const manifest = JSON.parse(await readFile(join(candidateDirectory, "release-manifest.json"), "utf8"));
  await verifyReleaseManifest({ candidateDir: candidateDirectory, manifest, source });
  if (manifest.distribution.android.signature !== "signed") {
    throw releaseError("release_android_signature_required");
  }
  const signing = await androidSigningInputs();
  if (signing.certificateSha256 !== manifest.distribution.android.certificateSha256) {
    throw releaseError("release_android_certificate_mismatch");
  }
  const androidPath = join(candidateDirectory, manifest.artifacts.android.file);
  await assertAndroidSigned(androidPath, signing.certificateSha256);
  const sdkDirectory = await androidSdkDirectory();
  const adb = join(sdkDirectory, "platform-tools", "adb.exe");
  const adbState = await stat(adb).catch(() => null);
  if (!adbState?.isFile()) throw releaseError("release_android_sdk_missing");
  const device = await connectedAndroidDevice(adb);
  const buildTools = await androidBuildTools(sdkDirectory);
  const temporary = await mkdtemp(join(tmpdir(), "crm-android-device-"));
  try {
    const apksPath = join(temporary, "candidate.apks");
    const storePasswordFile = join(temporary, "store-password.txt");
    const keyPasswordFile = join(temporary, "key-password.txt");
    await Promise.all([
      writeFile(storePasswordFile, process.env[androidSigningEnvironment.storePassword], { mode: 0o600, flag: "wx" }),
      writeFile(keyPasswordFile, process.env[androidSigningEnvironment.keyPassword], { mode: 0o600, flag: "wx" }),
    ]);
    try {
      await runBundletool("device-apks", androidPath, {
        CODE_REMOTE_MANAGER_ANDROID_APKS_PATH: apksPath,
        CODE_REMOTE_MANAGER_ANDROID_AAPT2: buildTools.aapt2,
        CODE_REMOTE_MANAGER_ANDROID_KEYSTORE: signing.keyStore,
        CODE_REMOTE_MANAGER_ANDROID_KEY_ALIAS: signing.keyAlias,
        CODE_REMOTE_MANAGER_ANDROID_STORE_PASSWORD_FILE: storePasswordFile,
        CODE_REMOTE_MANAGER_ANDROID_KEY_PASSWORD_FILE: keyPasswordFile,
      });
    } finally {
      await Promise.all([
        rm(storePasswordFile, { force: true }),
        rm(keyPasswordFile, { force: true }),
      ]);
    }
    await run("jar.exe", ["xf", apksPath], { cwd: temporary, capture: true });
    const apkPath = join(temporary, "universal.apk");
    const apkState = await stat(apkPath).catch(() => null);
    if (!apkState?.isFile()) throw releaseError("release_android_device_apk_missing");
    await run(buildTools.zipalign, ["-c", "-P", "16", "-v", "4", apkPath], { capture: true });
    const apkSignature = await run(buildTools.apksigner, ["verify", "--print-certs", apkPath], { capture: true });
    const apkCertificateSha256 = normalizedCertificateSha256(
      apkSignature.stdout.match(/certificate SHA-256 digest:\s*([a-f0-9:]+)/i)?.[1],
    );
    if (apkCertificateSha256 !== signing.certificateSha256) throw releaseError("release_android_certificate_mismatch");
    await run(adb, ["-s", device.serial, "install", "-r", apkPath], { capture: true });
    await run(adb, ["-s", device.serial, "shell", "am", "start", "-W", "-n", `${androidPackageName}/.MainActivity`], {
      capture: true,
    });
    const installed = await run(adb, ["-s", device.serial, "shell", "dumpsys", "package", androidPackageName], {
      capture: true,
    });
    if (!new RegExp(`versionCode=${manifest.android.versionCode}\\b`).test(installed.stdout) ||
        !new RegExp(`versionName=${manifest.version.replaceAll(".", "\\.")}\\b`).test(installed.stdout)) {
      throw releaseError("release_android_device_version_mismatch");
    }
    const rawPageSize = (await run(adb, ["-s", device.serial, "shell", "getconf", "PAGE_SIZE"], { capture: true })).stdout;
    const pageSize = /^\d{3,6}$/.test(rawPageSize) ? rawPageSize : "Unavailable";
    console.log(`Installed and launched ${manifest.version} on ${device.model}; device page size ${pageSize}.`);
    console.log("Connection and pairing still require operator confirmation in the running app.");
  } finally {
    await rm(temporary, { recursive: true, force: true });
  }
}

async function main() {
  const action = process.argv[2];
  if (action === "prepare") await prepareRelease();
  else if (action === "verify") await verifyPreparedRelease();
  else if (action === "sign-android-candidate") await signPreparedAndroidRelease();
  else if (action === "verify-android-device") await verifyAndroidDevice();
  else if (action === "inspect-android") {
    const manifest = JSON.parse(await readFile(join(candidateDirectory, "release-manifest.json"), "utf8"));
    const android = await inspectAndroidBundle(
      join(candidateDirectory, manifest.artifacts.android.file), manifest.pwa, manifest.version,
    );
    console.log(JSON.stringify(android));
  }
  else if (action === "inspect-android-raw") {
    const manifest = JSON.parse(await readFile(join(candidateDirectory, "release-manifest.json"), "utf8"));
    const android = await readAndroidBundleMetadata(
      join(candidateDirectory, manifest.artifacts.android.file), manifest.pwa,
    );
    console.log(JSON.stringify(android));
  }
  else if (action === "verify-android-bundle") {
    if (!process.argv[3] || !process.argv[4] || !process.argv[5]) throw releaseError("release_android_arguments_required");
    const pwa = await hashTree(process.argv[4]);
    console.log(JSON.stringify(await inspectAndroidBundle(process.argv[3], pwa, process.argv[5])));
  }
  else if (action === "verify-pwa-copy") {
    if (!process.argv[3] || !process.argv[4]) throw releaseError("release_pwa_arguments_required");
    await verifyPwaCopy(process.argv[3], process.argv[4]);
  }
  else if (action === "verify-pwa-notices") {
    await assertPwaNoticesCurrent();
  } else throw releaseError(
    "Use prepare, verify, sign-android-candidate, verify-android-device, inspect-android, inspect-android-raw, verify-android-bundle, verify-pwa-copy, or verify-pwa-notices",
  );
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  main().catch((error) => {
    console.error(error.message);
    process.exitCode = 1;
  });
}
