/**
 * After electron-builder: fail the pack if sharp / @img look missing or truncated
 * in release unpacked dirs. Catches AV quarantine and incomplete asarUnpack before shipping.
 */
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

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

/**
 * @param {string} startDir
 * @returns {string | null}
 */
function findAsarUnpackedNodeModules(startDir) {
  const direct = path.join(startDir, 'resources', 'app.asar.unpacked', 'node_modules');
  if (fs.existsSync(direct)) return direct;

  // macOS: *.app/Contents/Resources/app.asar.unpacked/node_modules
  if (!fs.existsSync(startDir)) return null;
  for (const name of fs.readdirSync(startDir)) {
    if (!name.endsWith('.app')) continue;
    const macNm = path.join(
      startDir,
      name,
      'Contents',
      'Resources',
      'app.asar.unpacked',
      'node_modules',
    );
    if (fs.existsSync(macNm)) return macNm;
  }
  return null;
}

/**
 * @param {string} filePath
 * @param {number} minBytes
 */
function assertJsLooksIntact(filePath, minBytes) {
  if (!fs.existsSync(filePath)) {
    throw new Error(`[verify-packaged-sharp] missing: ${filePath}`);
  }
  const st = fs.statSync(filePath);
  if (st.size < minBytes) {
    throw new Error(
      `[verify-packaged-sharp] truncated (${st.size} B < ${minBytes}): ${filePath}`,
    );
  }
  const head = fs.readFileSync(filePath, { encoding: 'utf8', flag: 'r' }).slice(0, 120);
  const trimmed = head.trimStart();
  const ok =
    trimmed.startsWith("'use strict'") ||
    trimmed.startsWith('"use strict"') ||
    head.includes('require(') ||
    head.includes('module.exports');
  if (!ok) {
    throw new Error(
      `[verify-packaged-sharp] sharp entry does not look like JS (corrupt?): ${filePath}`,
    );
  }
}

/**
 * @param {string} dir
 */
function assertHasNativeBinary(dir) {
  if (!fs.existsSync(dir)) {
    throw new Error(`[verify-packaged-sharp] missing native package dir: ${dir}`);
  }
  const stack = [dir];
  while (stack.length) {
    const cur = stack.pop();
    if (!cur) break;
    for (const name of fs.readdirSync(cur)) {
      const p = path.join(cur, name);
      const st = fs.statSync(p);
      if (st.isDirectory()) {
        stack.push(p);
        continue;
      }
      if (
        name.endsWith('.node') ||
        name.endsWith('.dll') ||
        name.endsWith('.dylib') ||
        /\.so(\.|$)/u.test(name)
      ) {
        if (st.size < 50_000) {
          throw new Error(
            `[verify-packaged-sharp] native binary too small (${st.size} B): ${p}`,
          );
        }
        return;
      }
    }
  }
  throw new Error(`[verify-packaged-sharp] no .node/.dll/.so under ${dir}`);
}

/**
 * @param {{ label: string; unpackedRoot: string; imgDirs: string[] }} probe
 */
export function verifyUnpackedSharp(probe) {
  const unpackedNm =
    findAsarUnpackedNodeModules(probe.unpackedRoot) ??
    path.join(probe.unpackedRoot, 'resources', 'app.asar.unpacked', 'node_modules');

  const sharpIndex = path.join(unpackedNm, 'sharp', 'lib', 'index.js');
  // Real sharp/lib/index.js is typically several KB; empty/AV-quarantined files fail here.
  assertJsLooksIntact(sharpIndex, 32);

  for (const img of probe.imgDirs) {
    assertHasNativeBinary(path.join(unpackedNm, '@img', img));
  }
}

/** @type {{ label: string; unpackedRoot: string; imgDirs: string[] }[]} */
const PROBES = [
  {
    label: 'win-unpacked',
    unpackedRoot: path.join(root, 'release', 'win-unpacked'),
    imgDirs: ['sharp-win32-x64'],
  },
  {
    label: 'mac-arm64',
    unpackedRoot: path.join(root, 'release', 'mac-arm64'),
    imgDirs: ['sharp-darwin-arm64'],
  },
  {
    label: 'mac',
    unpackedRoot: path.join(root, 'release', 'mac'),
    imgDirs: ['sharp-darwin-x64', 'sharp-darwin-arm64'],
  },
  {
    label: 'linux-unpacked',
    unpackedRoot: path.join(root, 'release', 'linux-unpacked'),
    imgDirs: ['sharp-linux-x64', 'sharp-linux-arm64'],
  },
];

/**
 * Verify every existing unpacked release dir. At least one must exist.
 * For multi-arch probes, require at least one listed @img package that is present.
 */
export function verifyPackagedSharpRelease(releaseRoot = root) {
  const probes = PROBES.map((p) => ({
    ...p,
    unpackedRoot: path.join(
      releaseRoot,
      path.relative(root, p.unpackedRoot),
    ),
  }));
  const existing = probes.filter((p) => fs.existsSync(p.unpackedRoot));
  if (existing.length === 0) {
    throw new Error(
      '[verify-packaged-sharp] no release unpacked dir found — run electron-builder first',
    );
  }

  for (const probe of existing) {
    const nm = findAsarUnpackedNodeModules(probe.unpackedRoot);
    if (!nm) {
      throw new Error(
        `[verify-packaged-sharp] app.asar.unpacked/node_modules missing under ${probe.unpackedRoot}`,
      );
    }
    const presentImg = probe.imgDirs.filter((d) =>
      fs.existsSync(path.join(nm, '@img', d)),
    );
    if (presentImg.length === 0) {
      throw new Error(
        `[verify-packaged-sharp] none of @img/{${probe.imgDirs.join(',')}} under ${nm}`,
      );
    }
    verifyUnpackedSharp({ ...probe, imgDirs: presentImg });
    console.log(`[verify-packaged-sharp] OK: ${probe.label}`);
  }
}

function main() {
  verifyPackagedSharpRelease();
}

const entry = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : '';
if (import.meta.url === entry) {
  try {
    main();
  } catch (err) {
    console.error(err instanceof Error ? err.message : err);
    process.exit(1);
  }
}
