#!/usr/bin/env node

import { constants } from 'node:fs';
import { access, lstat, readFile, readdir, readlink } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

export const MISSING_DEPS_EXIT = 42;
export const GENERATED_STATE_EXIT = 43;

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

async function entries(root) {
  const result = [];
  async function walk(current) {
    let children;
    try {
      children = await readdir(current, { withFileTypes: true });
    } catch (error) {
      if (error.code === 'ENOENT') return;
      throw error;
    }
    for (const child of children) {
      const target = path.join(current, child.name);
      result.push(target);
      if (child.isDirectory() && !child.isSymbolicLink()) await walk(target);
    }
  }
  await walk(root);
  return result;
}

export async function generatedSymlinkManifest(nextDir) {
  const links = [];
  for (const target of (await entries(nextDir)).sort()) {
    const stats = await lstat(target);
    if (!stats.isSymbolicLink()) continue;
    links.push({
      path: path.relative(nextDir, target).split(path.sep).join('/'),
      target: await readlink(target),
    });
  }
  return `${JSON.stringify({ version: 1, links })}\n`;
}

const webSourceRoots = (root) => [
  path.join(root, 'apps', 'web', 'src'),
  path.join(root, 'apps', 'web', 'public'),
  path.join(root, 'apps', 'web', 'next-env.d.ts'),
  path.join(root, 'apps', 'web', 'next.config.ts'),
  path.join(root, 'apps', 'web', 'postcss.config.mjs'),
  path.join(root, 'apps', 'web', 'package.json'),
  path.join(root, 'apps', 'web', 'tsconfig.json'),
  path.join(root, 'packages', 'design-tokens', 'src'),
  path.join(root, 'packages', 'design-tokens', 'package.json'),
  path.join(root, 'packages', 'design-tokens', 'tsconfig.json'),
  path.join(root, 'package.json'),
  path.join(root, 'tsconfig.base.json'),
  path.join(root, 'pnpm-lock.yaml'),
  path.join(root, 'pnpm-workspace.yaml'),
  path.join(root, 'turbo.json'),
];

// next.config.ts currently reads no server-only environment. Add any future
// server-side build inputs here; all resolved NEXT_PUBLIC_* inputs are automatic.
const serverBuildEnvironmentKeys = [];

function publicBuildEnvironment(root) {
  const webDir = path.join(root, 'apps', 'web');
  const requireFromWeb = createRequire(path.join(scriptRoot, 'apps', 'web', 'package.json'));
  const requireFromNext = createRequire(requireFromWeb.resolve('next/package.json'));
  const { loadEnvConfig, resetEnv, updateInitialEnv } = requireFromNext('@next/env');
  const originalEnvironment = { ...process.env };
  updateInitialEnv(originalEnvironment);
  try {
    const { combinedEnv } = loadEnvConfig(webDir, false, { info() {}, error() {} }, true);
    return Object.fromEntries(
      Object.entries(combinedEnv).filter(
        ([key, value]) =>
          value !== undefined &&
          (key.startsWith('NEXT_PUBLIC_') || serverBuildEnvironmentKeys.includes(key)),
      ),
    );
  } finally {
    resetEnv();
  }
}

export async function sourceFingerprint(root = process.cwd()) {
  const files = [];
  for (const sourceRoot of webSourceRoots(root)) {
    try {
      const stats = await lstat(sourceRoot);
      if (stats.isSymbolicLink()) {
        throw new Error(
          `Web build input must not be a symbolic link: ${path.relative(root, sourceRoot)}`,
        );
      }
      if (stats.isFile()) files.push(sourceRoot);
      if (stats.isDirectory()) {
        for (const target of await entries(sourceRoot)) {
          const targetStats = await lstat(target);
          if (targetStats.isSymbolicLink()) {
            throw new Error(
              `Web build input must not be a symbolic link: ${path.relative(root, target)}`,
            );
          }
          if (targetStats.isFile()) files.push(target);
        }
      }
    } catch (error) {
      if (error.code !== 'ENOENT') throw error;
    }
  }

  const digest = createHash('sha256');
  for (const [key, value] of Object.entries(publicBuildEnvironment(root)).sort()) {
    digest.update(`env:${key}\0${value.length}\0${value}\0`);
  }
  for (const target of files.sort()) {
    const contents = await readFile(target);
    digest.update(path.relative(root, target).split(path.sep).join('/'));
    digest.update('\0');
    digest.update(String(contents.length));
    digest.update('\0');
    digest.update(contents);
    digest.update('\0');
  }
  return digest.digest('hex');
}

export async function runPreflight({ root = process.cwd(), uid = process.getuid?.() } = {}) {
  const binDir = path.join(root, 'node_modules', '.bin');
  const requiredBinaries = ['eslint', 'husky', 'prettier', 'tsc', 'turbo', 'vitest'];
  const missingBinaries = [];
  for (const binary of requiredBinaries) {
    try {
      await access(path.join(binDir, binary), constants.X_OK);
    } catch {
      missingBinaries.push(binary);
    }
  }
  if (missingBinaries.length > 0) {
    return {
      code: MISSING_DEPS_EXIT,
      message: `MOSAIC_PREFLIGHT_MISSING_DEPS: dependency installation is missing ${missingBinaries.join(', ')}; run pnpm install --frozen-lockfile`,
    };
  }

  const buildLock = path.join(root, '.mosaic-test-work', 'web-build.lock');
  try {
    await lstat(buildLock);
    return {
      code: GENERATED_STATE_EXIT,
      message: `MOSAIC_PREFLIGHT_GENERATED_STATE: web build is in progress or interrupted at ${buildLock}; wait for it to finish or rerun pnpm build to recover the stale lock`,
    };
  } catch (error) {
    if (error.code !== 'ENOENT') throw error;
  }

  const nextDir = path.join(root, 'apps', 'web', '.next');
  let generated = [];
  try {
    const nextStats = await lstat(nextDir);
    if (!nextStats.isDirectory() || nextStats.isSymbolicLink()) {
      return {
        code: GENERATED_STATE_EXIT,
        message:
          'MOSAIC_PREFLIGHT_GENERATED_STATE: apps/web/.next must be a real directory, not a symbolic link, and is not trustworthy; run pnpm clean:generated, then rerun the gate',
      };
    }
    generated = [nextDir, ...(await entries(nextDir))];
  } catch (error) {
    if (error.code !== 'ENOENT') throw error;
  }

  if (generated.length > 0) {
    const foreign = [];
    for (const target of generated) {
      const stats = await lstat(target);
      if (uid !== undefined && stats.uid !== uid) foreign.push(path.relative(root, target));
    }

    // Detects accidental, independent, stale, and foreign-residue mutation of
    // generated state: the class this check was born from was a five-month-stale
    // .next whose validator referenced deleted pages and produced 19 phantom TS2307
    // errors indistinguishable from real type errors.
    //
    // Does NOT defend against an actor with same-UID write access to the generated
    // tree, which can regenerate both the manifest and marker consistently
    // (CWE-345). No local construction can, absent a trust anchor outside that
    // actor's authority. RM-59 tracks executor/spine-side attestation.
    let certification = null;
    let certifiedManifest = null;
    try {
      const [certificationContents, manifestContents] = await Promise.all([
        readFile(path.join(nextDir, '.mosaic-source-hash'), 'utf8'),
        readFile(path.join(nextDir, '.mosaic-symlink-manifest'), 'utf8'),
      ]);
      try {
        const parsed = JSON.parse(certificationContents);
        if (
          parsed.version === 1 &&
          typeof parsed.sourceFingerprint === 'string' &&
          typeof parsed.symlinkManifestHash === 'string'
        ) {
          certification = parsed;
          certifiedManifest = manifestContents;
        }
      } catch {
        // Invalid certification is handled as untrusted generated state below.
      }
    } catch (error) {
      if (error.code !== 'ENOENT') throw error;
    }
    const stale = certification?.sourceFingerprint !== (await sourceFingerprint(root));
    const actualManifest = await generatedSymlinkManifest(nextDir);
    const certifiedManifestHash =
      certifiedManifest === null
        ? null
        : createHash('sha256').update(certifiedManifest).digest('hex');
    const changedSymlinks =
      certification?.symlinkManifestHash !== certifiedManifestHash ||
      certifiedManifest !== actualManifest;
    if (foreign.length > 0 || stale || changedSymlinks) {
      const reasons = [
        foreign.length > 0 ? `foreign-owned paths: ${foreign.slice(0, 3).join(', ')}` : '',
        stale ? 'generated source fingerprint does not match web source/configuration' : '',
        changedSymlinks
          ? 'generated symbolic-link manifest does not match the certified build'
          : '',
      ].filter(Boolean);
      return {
        code: GENERATED_STATE_EXIT,
        message: `MOSAIC_PREFLIGHT_GENERATED_STATE: apps/web/.next is not trustworthy (${reasons.join('; ')}); run pnpm clean:generated, then rerun the gate`,
      };
    }
  }

  return { code: 0, message: 'checkout preflight passed' };
}

async function main() {
  const result = await runPreflight();
  const stream = result.code === 0 ? process.stdout : process.stderr;
  stream.write(`${result.message}\n`);
  process.exitCode = result.code;
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  await main();
}
