import fs from "fs";
import path from "path";
import { execa } from "execa";
import fsExtra from "fs-extra";
import { glob } from "glob";
import { globby, isGitIgnored } from "globby";
import { z } from "zod";
import {
  appsCache,
  cachified,
  exampleAppCache,
  playgroundAppCache,
  problemAppCache,
  solutionAppCache
} from "./cache.server.js";
import { getOptionalWatcher, getWatcher } from "./change-tracker.js";
import { compileMdx } from "./compile-mdx.server.js";
import {
  closeProcess,
  isAppRunning,
  runAppDev,
  waitOnApp
} from "./process-manager.server.js";
import { singleton } from "./singleton.server.js";
import { getServerTimeHeader } from "./timing.server.js";
import { getErrorMessage, getPkgProp } from "./utils.js";
process.env.NODE_ENV = process.env.NODE_ENV ?? "development";
const workshopRoot = getWorkshopRoot();
const playgroundAppNameInfoPath = path.join(
  getWorkshopRoot(),
  "node_modules",
  ".cache",
  "kcdshop",
  "playground.json"
);
function isApp(app) {
  return app && typeof app === "object" && typeof app.name === "string" && typeof app.title === "string" && typeof app.dirName === "string" && typeof app.fullPath === "string" && typeof app.test === "object" && typeof app.dev === "object" && typeof app.type === "string";
}
function isProblemApp(app) {
  return isApp(app) && app.type === "problem";
}
function isSolutionApp(app) {
  return isApp(app) && app.type === "solution";
}
function isFirstStepProblemApp(app) {
  return isProblemApp(app) && app.stepNumber === 1;
}
function isFirstStepSolutionApp(app) {
  return isSolutionApp(app) && app.stepNumber === 1;
}
function isPlaygroundApp(app) {
  return isApp(app) && app.type === "playground";
}
function isExampleApp(app) {
  return isApp(app) && app.type === "example";
}
function isExerciseStepApp(app) {
  return isProblemApp(app) || isSolutionApp(app);
}
function exists(file) {
  return fs.promises.access(file, fs.constants.F_OK).then(
    () => true,
    () => false
  );
}
const modifiedTimes = singleton(
  "modified_times",
  () => /* @__PURE__ */ new Map()
);
function init() {
  async function handleFileChanges(event, filePath) {
    const apps = await getApps();
    for (const app of apps) {
      if (filePath.startsWith(app.fullPath)) {
        modifiedTimes.set(app.fullPath, Date.now());
        break;
      }
    }
  }
  getWatcher()?.on("all", handleFileChanges);
}
function getForceFresh(cacheEntry) {
  if (!cacheEntry)
    return true;
  const latestModifiedTime = Math.max(...Array.from(modifiedTimes.values()));
  if (!latestModifiedTime)
    return void 0;
  return latestModifiedTime > cacheEntry.metadata.createdTime ? true : void 0;
}
function setModifiedTimesForDir(dir) {
  modifiedTimes.set(dir, Date.now());
}
function getForceFreshForDir(dir, cacheEntry) {
  if (!path.isAbsolute(dir)) {
    throw new Error(`Trying to get force fresh for non-absolute path: ${dir}`);
  }
  if (!cacheEntry)
    return true;
  const modifiedTime = modifiedTimes.get(dir);
  if (!modifiedTime)
    return void 0;
  return modifiedTime > cacheEntry.metadata.createdTime ? true : void 0;
}
async function readDir(dir) {
  if (await exists(dir)) {
    return fs.promises.readdir(dir);
  }
  return [];
}
async function compileMdxIfExists(filepath, { request } = {}) {
  filepath = filepath.replace(/\\/g, "/");
  if (await exists(filepath)) {
    const compiled = await compileMdx(filepath, { request }).catch((error) => {
      console.error(`Error compiling ${filepath}:`, error);
      return null;
    });
    return compiled;
  }
  return null;
}
function getAppDirInfo(appDir) {
  const regex = /^(?<stepNumber>\d+)\.(problem|solution)(\.(?<subtitle>.*))?$/;
  const match = regex.exec(appDir);
  if (!match || !match.groups) {
    console.info(
      `Ignoring directory "${appDir}" which does not match regex "${regex}"`
    );
    return null;
  }
  const { stepNumber: stepNumberString, subtitle } = match.groups;
  const stepNumber = Number(stepNumberString);
  if (!stepNumber || !Number.isFinite(stepNumber)) {
    throw new Error(
      `Cannot identify the stepNumber for app directory "${appDir}" with regex "${regex}"`
    );
  }
  const type = match[2];
  return { stepNumber, type, subtitle };
}
function extractExerciseNumber(dir) {
  const regex = /^(?<number>\d+)\./;
  const number = regex.exec(dir)?.groups?.number;
  if (!number) {
    return null;
  }
  return Number(number);
}
async function getExercises({
  timings,
  request
} = {}) {
  const apps = await getApps({ request, timings });
  const exerciseDirs = await readDir(path.join(workshopRoot, "exercises"));
  const exercises = [];
  for (const dirName of exerciseDirs) {
    const exerciseNumber = extractExerciseNumber(dirName);
    if (!exerciseNumber)
      continue;
    const compiledReadme = await compileMdxIfExists(
      path.join(workshopRoot, "exercises", dirName, "README.mdx"),
      { request }
    );
    const compiledFinished = await compileMdxIfExists(
      path.join(workshopRoot, "exercises", dirName, "FINISHED.mdx"),
      { request }
    );
    const steps = [];
    const exerciseApps = apps.filter(isExerciseStepApp).filter((app) => app.exerciseNumber === exerciseNumber);
    for (const app of exerciseApps) {
      steps[app.stepNumber - 1] = {
        ...steps[app.stepNumber - 1],
        [app.type]: app,
        stepNumber: app.stepNumber
      };
    }
    exercises.push({
      exerciseNumber,
      dirName,
      instructionsCode: compiledReadme?.code,
      finishedCode: compiledFinished?.code,
      title: compiledReadme?.title ?? dirName,
      instructionsEpicVideoEmbeds: compiledReadme?.epicVideoEmbeds,
      finishedEpicVideoEmbeds: compiledFinished?.epicVideoEmbeds,
      steps,
      problems: apps.filter(isProblemApp).filter((app) => app.exerciseNumber === exerciseNumber),
      solutions: apps.filter(isSolutionApp).filter((app) => app.exerciseNumber === exerciseNumber)
    });
  }
  return exercises;
}
let appCallCount = 0;
async function getApps({
  timings,
  request,
  forceFresh
} = {}) {
  const key = "apps";
  const apps = await cachified({
    key,
    cache: appsCache,
    timings,
    timingKey: `apps_${appCallCount++}`,
    request,
    // This entire cache is to avoid a single request getting a fresh value
    // multiple times unnecessarily (because getApps is called many times)
    ttl: 1e3 * 60 * 60 * 24,
    forceFresh: forceFresh ?? getForceFresh(await appsCache.get(key)),
    getFreshValue: async () => {
      const playgroundApp = await getPlaygroundApp({ request, timings });
      const problemApps = await getProblemApps({ request, timings });
      const solutionApps = await getSolutionApps({ request, timings });
      const exampleApps = await getExampleApps({ request, timings });
      const sortedApps = [
        playgroundApp,
        ...problemApps,
        ...solutionApps,
        ...exampleApps
      ].filter(Boolean).sort((a, b) => {
        if (isPlaygroundApp(a)) {
          if (isPlaygroundApp(b))
            return a.name.localeCompare(b.name);
          else
            return 1;
        }
        if (isPlaygroundApp(b))
          return 1;
        if (isExampleApp(a)) {
          if (isExampleApp(b))
            return a.name.localeCompare(b.name);
          else
            return 1;
        }
        if (isExampleApp(b))
          return -1;
        if (a.type === b.type) {
          if (a.exerciseNumber === b.exerciseNumber) {
            return a.stepNumber - b.stepNumber;
          } else {
            return a.exerciseNumber - b.exerciseNumber;
          }
        }
        if (isProblemApp(a)) {
          if (a.exerciseNumber === b.exerciseNumber) {
            return a.stepNumber <= b.stepNumber ? 1 : -1;
          } else {
            return a.exerciseNumber <= b.exerciseNumber ? 1 : -1;
          }
        }
        if (isSolutionApp(a)) {
          if (a.exerciseNumber === b.exerciseNumber) {
            return a.stepNumber < b.stepNumber ? -1 : 1;
          } else {
            return a.exerciseNumber < b.exerciseNumber ? -1 : 1;
          }
        }
        console.error("unhandled sorting case", a, b);
        return 0;
      });
      return sortedApps;
    }
  });
  return apps;
}
function extractNumbersFromAppName(fullPath) {
  const regex = /(?<exerciseNumber>\d+)([^\d]*)(?<stepNumber>\d+)/g;
  const { exerciseNumber, stepNumber } = regex.exec(fullPath)?.groups ?? {};
  return { exerciseNumber, stepNumber };
}
function getAppName(fullPath) {
  const relativePath = fullPath.replace(`${workshopRoot}${path.sep}`, "");
  return relativePath.split(path.sep).join("__sep__");
}
function getFullPathFromAppName(appName) {
  const relativePath = appName.replaceAll("__sep__", path.sep);
  return path.join(workshopRoot, relativePath);
}
async function findSolutionDir({
  fullPath
}) {
  const dirName = path.basename(fullPath);
  if (dirName.includes(".problem")) {
    const info = getAppDirInfo(dirName);
    if (!info)
      return null;
    const { stepNumber } = info;
    const paddedStepNumber = stepNumber.toString().padStart(2, "0");
    const parentDir = path.dirname(fullPath);
    const siblingDirs = await fs.promises.readdir(parentDir);
    const solutionDir = siblingDirs.find(
      (dir) => dir.startsWith(`${paddedStepNumber}.solution`)
    );
    if (solutionDir) {
      return path.join(parentDir, solutionDir);
    }
  } else if (fullPath.endsWith("playground")) {
    const appName = await getPlaygroundAppName();
    if (appName) {
      const solDir = await findSolutionDir({
        fullPath: getFullPathFromAppName(appName)
      });
      return solDir;
    }
  }
  return null;
}
async function findProblemDir({
  fullPath
}) {
  const dirName = path.basename(fullPath);
  if (dirName.includes(".solution")) {
    const info = getAppDirInfo(dirName);
    if (!info)
      return null;
    const { stepNumber } = info;
    const paddedStepNumber = stepNumber.toString().padStart(2, "0");
    const parentDir = path.dirname(fullPath);
    const siblingDirs = await fs.promises.readdir(parentDir);
    const problemDir = siblingDirs.find(
      (dir) => dir.endsWith("problem") && dir.includes(paddedStepNumber)
    );
    if (problemDir) {
      return path.join(parentDir, problemDir);
    }
  } else if (fullPath.endsWith("playground")) {
    const appName = await getPlaygroundAppName();
    if (appName) {
      return findProblemDir({
        fullPath: getFullPathFromAppName(appName)
      });
    }
  }
  return null;
}
async function getTestInfo({
  fullPath
}) {
  const testScriptName = "test";
  const hasPkgJson = await exists(path.join(fullPath, "package.json"));
  const testScript = hasPkgJson ? await getPkgProp(
    fullPath,
    ["kcd-workshop.scripts", testScriptName].join("."),
    ""
  ) : null;
  if (testScript) {
    return { type: "script", script: testScript };
  }
  const testAppFullPath = await findSolutionDir({ fullPath }) ?? fullPath;
  const dirList = await fs.promises.readdir(testAppFullPath);
  const testFiles = dirList.filter((item) => item.includes(".test."));
  if (testFiles.length) {
    const name = getAppName(fullPath);
    return { type: "browser", pathname: `/app/${name}/test/`, testFiles };
  }
  return { type: "none" };
}
async function getDevInfo({
  fullPath,
  portNumber
}) {
  const hasPkgJson = await exists(path.join(fullPath, "package.json"));
  const hasDevScript = hasPkgJson ? Boolean(await getPkgProp(fullPath, ["scripts", "dev"].join("."), "")) : false;
  if (hasDevScript) {
    return {
      type: "script",
      portNumber
    };
  }
  const name = getAppName(fullPath);
  return { type: "browser", pathname: `/app/${name}/` };
}
async function getPlaygroundApp({
  timings,
  request
} = {}) {
  const playgroundDir = path.join(workshopRoot, "playground");
  const appName = await getPlaygroundAppName();
  const key = `playground-${appName}`;
  return cachified({
    key,
    cache: playgroundAppCache,
    ttl: 1e3 * 60 * 60 * 24,
    timings,
    timingKey: playgroundDir.replace(`${playgroundDir}${path.sep}`, ""),
    request,
    forceFresh: getForceFreshForDir(
      playgroundDir,
      await playgroundAppCache.get(key)
    ),
    getFreshValue: async () => {
      if (!await exists(playgroundDir))
        return null;
      if (!appName)
        return null;
      const dirName = path.basename(playgroundDir);
      const name = getAppName(playgroundDir);
      const portNumber = 4e3;
      const [compiledReadme, test, dev] = await Promise.all([
        compileMdxIfExists(path.join(playgroundDir, "README.mdx"), { request }),
        getTestInfo({ fullPath: playgroundDir }),
        getDevInfo({ fullPath: playgroundDir, portNumber })
      ]);
      return {
        name,
        appName,
        type: "playground",
        fullPath: playgroundDir,
        relativePath: playgroundDir.replace(
          `${getWorkshopRoot()}${path.sep}`,
          ""
        ),
        title: compiledReadme?.title ?? name,
        epicVideoEmbeds: compiledReadme?.epicVideoEmbeds,
        dirName,
        instructionsCode: compiledReadme?.code,
        test,
        dev
      };
    }
  }).catch((error) => {
    console.error(error);
    return null;
  });
}
async function getExampleAppFromPath(fullPath, index, request) {
  const dirName = path.basename(fullPath);
  const compiledReadme = await compileMdxIfExists(
    path.join(fullPath, "README.mdx"),
    { request }
  );
  const name = getAppName(fullPath);
  const portNumber = 8e3 + index;
  return {
    name,
    type: "example",
    fullPath,
    relativePath: fullPath.replace(`${getWorkshopRoot()}${path.sep}`, ""),
    title: compiledReadme?.title ?? name,
    epicVideoEmbeds: compiledReadme?.epicVideoEmbeds,
    dirName,
    instructionsCode: compiledReadme?.code,
    test: await getTestInfo({ fullPath }),
    dev: await getDevInfo({ fullPath, portNumber })
  };
}
async function getExampleApps({
  timings,
  request
} = {}) {
  const examplesDir = path.join(workshopRoot, "examples");
  const exampleDirs = (await glob("*", { cwd: examplesDir, ignore: "node_modules/**" })).map((p) => path.join(examplesDir, p));
  const exampleApps = [];
  for (const exampleDir of exampleDirs) {
    const index = exampleDirs.indexOf(exampleDir);
    const key = `${exampleDir}-${index}`;
    const exampleApp = await cachified({
      key,
      cache: exampleAppCache,
      ttl: 1e3 * 60 * 60 * 24,
      timings,
      timingKey: exampleDir.replace(`${examplesDir}${path.sep}`, ""),
      request,
      forceFresh: getForceFreshForDir(
        exampleDir,
        await exampleAppCache.get(key)
      ),
      getFreshValue: () => getExampleAppFromPath(exampleDir, index, request).catch((error) => {
        console.error(error);
        return null;
      })
    });
    if (exampleApp)
      exampleApps.push(exampleApp);
  }
  return exampleApps;
}
async function getSolutionAppFromPath(fullPath, request) {
  const dirName = path.basename(fullPath);
  const parentDirName = path.basename(path.dirname(fullPath));
  const exerciseNumber = extractExerciseNumber(parentDirName);
  if (!exerciseNumber)
    return null;
  const name = getAppName(fullPath);
  const info = getAppDirInfo(dirName);
  if (!info)
    return null;
  const { stepNumber } = info;
  const portNumber = 7e3 + (exerciseNumber - 1) * 10 + stepNumber;
  const compiledReadme = await compileMdxIfExists(
    path.join(fullPath, "README.mdx"),
    { request }
  );
  const problemDir = await findProblemDir({
    fullPath
  });
  const problemName = problemDir ? getAppName(problemDir) : null;
  const [test, dev] = await Promise.all([
    getTestInfo({ fullPath }),
    getDevInfo({ fullPath, portNumber })
  ]);
  return {
    name,
    title: compiledReadme?.title ?? name,
    epicVideoEmbeds: compiledReadme?.epicVideoEmbeds,
    type: "solution",
    problemName,
    exerciseNumber,
    stepNumber,
    dirName,
    fullPath,
    relativePath: fullPath.replace(`${getWorkshopRoot()}${path.sep}`, ""),
    instructionsCode: compiledReadme?.code,
    test,
    dev
  };
}
async function getSolutionApps({
  timings,
  request
} = {}) {
  const exercisesDir = path.join(workshopRoot, "exercises");
  const solutionDirs = (await glob("**/*solution*", {
    cwd: exercisesDir,
    ignore: "node_modules/**"
  })).map((p) => path.join(exercisesDir, p));
  const solutionApps = [];
  for (const solutionDir of solutionDirs) {
    const solutionApp = await cachified({
      key: solutionDir,
      cache: solutionAppCache,
      timings,
      timingKey: solutionDir.replace(`${exercisesDir}${path.sep}`, ""),
      request,
      ttl: 1e3 * 60 * 60 * 24,
      forceFresh: getForceFreshForDir(
        solutionDir,
        await solutionAppCache.get(solutionDir)
      ),
      getFreshValue: () => getSolutionAppFromPath(solutionDir, request).catch((error) => {
        console.error(error);
        return null;
      })
    });
    if (solutionApp)
      solutionApps.push(solutionApp);
  }
  return solutionApps;
}
async function getProblemAppFromPath(fullPath, request) {
  const dirName = path.basename(fullPath);
  const parentDirName = path.basename(path.dirname(fullPath));
  const exerciseNumber = extractExerciseNumber(parentDirName);
  if (!exerciseNumber)
    return null;
  const name = getAppName(fullPath);
  const info = getAppDirInfo(dirName);
  if (!info)
    return null;
  const { stepNumber } = info;
  const portNumber = 6e3 + (exerciseNumber - 1) * 10 + stepNumber;
  const compiledReadme = await compileMdxIfExists(
    path.join(fullPath, "README.mdx"),
    { request }
  );
  const solutionDir = await findSolutionDir({
    fullPath
  });
  const solutionName = solutionDir ? getAppName(solutionDir) : null;
  const [test, dev] = await Promise.all([
    getTestInfo({ fullPath }),
    getDevInfo({ fullPath, portNumber })
  ]);
  return {
    solutionName,
    name,
    title: compiledReadme?.title ?? name,
    epicVideoEmbeds: compiledReadme?.epicVideoEmbeds,
    type: "problem",
    exerciseNumber,
    stepNumber,
    dirName,
    fullPath,
    relativePath: fullPath.replace(`${getWorkshopRoot()}${path.sep}`, ""),
    instructionsCode: compiledReadme?.code,
    test,
    dev
  };
}
async function getProblemApps({
  timings,
  request
} = {}) {
  const exercisesDir = path.join(workshopRoot, "exercises");
  const problemDirs = (await glob("**/*problem*", {
    cwd: exercisesDir,
    ignore: "node_modules/**"
  })).map((p) => path.join(exercisesDir, p));
  const problemApps = [];
  for (const problemDir of problemDirs) {
    const problemApp = await cachified({
      key: problemDir,
      cache: problemAppCache,
      timings,
      timingKey: problemDir.replace(`${exercisesDir}${path.sep}`, ""),
      request,
      ttl: 1e3 * 60 * 60 * 24,
      forceFresh: getForceFreshForDir(
        problemDir,
        await problemAppCache.get(problemDir)
      ),
      getFreshValue: () => getProblemAppFromPath(problemDir).catch((error) => {
        console.error(error);
        return null;
      })
    });
    if (problemApp)
      problemApps.push(problemApp);
  }
  return problemApps;
}
async function getExercise(exerciseNumber, { request, timings } = {}) {
  const exercises = await getExercises({ request, timings });
  return exercises.find((s) => s.exerciseNumber === Number(exerciseNumber));
}
async function requireExercise(exerciseNumber, { request, timings } = {}) {
  const exercise = await getExercise(exerciseNumber, { request, timings });
  if (!exercise) {
    throw new Response("Not found", {
      status: 404,
      headers: { "Server-Timing": getServerTimeHeader(timings) }
    });
  }
  return exercise;
}
async function requireExerciseApp(params, { request, timings } = {}) {
  const app = await getExerciseApp(params, { request, timings });
  if (!app) {
    throw new Response("Not found", { status: 404 });
  }
  return app;
}
const ExerciseAppParamsSchema = z.object({
  type: z.union([z.literal("problem"), z.literal("solution")]),
  exerciseNumber: z.coerce.number().finite(),
  stepNumber: z.coerce.number().finite()
});
async function getExerciseApp(params, { request, timings } = {}) {
  const result = ExerciseAppParamsSchema.safeParse(params);
  if (!result.success) {
    return null;
  }
  const { type, exerciseNumber, stepNumber } = result.data;
  const apps = (await getApps({ request, timings })).filter(isExerciseStepApp);
  const exerciseApp = apps.find((app) => {
    if (isExampleApp(app))
      return false;
    return app.exerciseNumber === exerciseNumber && app.stepNumber === stepNumber && app.type === type;
  });
  if (!exerciseApp) {
    return null;
  }
  return exerciseApp;
}
async function getAppByName(name, { request, timings } = {}) {
  const apps = await getApps({ request, timings });
  return apps.find((a) => a.name === name);
}
async function getNextExerciseApp(app, { request, timings } = {}) {
  const apps = (await getApps({ request, timings })).filter(isExerciseStepApp);
  const index = apps.findIndex((a) => a.name === app.name);
  if (index === -1) {
    throw new Error(`Could not find app ${app.name}`);
  }
  const nextApp = apps[index + 1];
  return nextApp ? nextApp : null;
}
async function getPrevExerciseApp(app, { request, timings } = {}) {
  const apps = (await getApps({ request, timings })).filter(isExerciseStepApp);
  const index = apps.findIndex((a) => a.name === app.name);
  if (index === -1) {
    throw new Error(`Could not find app ${app.name}`);
  }
  const prevApp = apps[index - 1];
  return prevApp ? prevApp : null;
}
function getAppPageRoute(app) {
  const exerciseNumber = app.exerciseNumber.toString().padStart(2, "0");
  const stepNumber = app.stepNumber.toString().padStart(2, "0");
  return `/${exerciseNumber}/${stepNumber}/${app.type}`;
}
async function setPlayground(srcDir, { reset } = {}) {
  const isIgnored = await isGitIgnored({ cwd: srcDir });
  const destDir = path.join(getWorkshopRoot(), "playground");
  const playgroundFiles = path.join(destDir, "**");
  getOptionalWatcher()?.unwatch(playgroundFiles);
  const playgroundApp = await getAppByName("playground");
  const playgroundWasRunning = playgroundApp ? isAppRunning(playgroundApp) : false;
  if (playgroundApp && reset) {
    await closeProcess(playgroundApp.name);
    await fsExtra.remove(destDir);
  }
  const setPlaygroundTimestamp = Date.now();
  const preSetPlaygroundPath = path.join(
    srcDir,
    "kcdshop",
    "pre-set-playground.js"
  );
  if (await exists(preSetPlaygroundPath)) {
    await execa("node", [preSetPlaygroundPath], {
      cwd: workshopRoot,
      stdio: "inherit",
      env: {
        KCDSHOP_PLAYGROUND_TIMESTAMP: setPlaygroundTimestamp.toString(),
        KCDSHOP_PLAYGROUND_DEST_DIR: destDir,
        KCDSHOP_PLAYGROUND_SRC_DIR: srcDir,
        KCDSHOP_PLAYGROUND_WAS_RUNNING: playgroundWasRunning.toString()
      }
    });
  }
  const basename = path.basename(srcDir);
  await fsExtra.remove(path.join(destDir, "node_modules"));
  await fsExtra.copy(srcDir, destDir, {
    filter: async (srcFile, destFile) => {
      if (srcFile.includes(`${basename}${path.sep}build`) || srcFile.includes(`${basename}${path.sep}public${path.sep}build`)) {
        return false;
      }
      if (srcFile === srcDir)
        return true;
      if (srcFile.includes("node_modules"))
        return true;
      if (srcFile.endsWith(".env"))
        return true;
      if (isIgnored(srcFile))
        return false;
      try {
        const isDir = (await fsExtra.stat(srcFile)).isDirectory();
        if (isDir)
          return true;
        const destIsDir = (await fsExtra.stat(destFile)).isDirectory();
        if (destIsDir)
          return true;
        const currentContents = await fsExtra.readFile(destFile);
        const newContents = await fsExtra.readFile(srcFile);
        if (currentContents.equals(newContents))
          return false;
        return true;
      } catch {
        return true;
      }
    }
  });
  async function getFiles(dir) {
    const dirPath = dir.replace(/\\/g, "/");
    const files = await globby([`${dirPath}/**/*`, "!**/build/**/*"], {
      onlyFiles: false,
      dot: true
    });
    return files.map((f) => f.replace(dirPath, ""));
  }
  const srcFiles = await getFiles(srcDir);
  const destFiles = await getFiles(destDir);
  const filesToDelete = destFiles.filter(
    (fileName) => !srcFiles.includes(fileName)
  );
  for (const fileToDelete of filesToDelete) {
    await fsExtra.remove(path.join(destDir, fileToDelete));
  }
  const appName = getAppName(srcDir);
  await fsExtra.ensureDir(path.dirname(playgroundAppNameInfoPath));
  await fsExtra.writeJSON(playgroundAppNameInfoPath, { appName });
  const playgroundIsStillRunning = playgroundApp ? isAppRunning(playgroundApp) : false;
  const restartPlayground = playgroundWasRunning && !playgroundIsStillRunning;
  const postSetPlaygroundPath = path.join(
    srcDir,
    "kcdshop",
    "post-set-playground.js"
  );
  if (await exists(postSetPlaygroundPath)) {
    await execa("node", [postSetPlaygroundPath], {
      cwd: workshopRoot,
      stdio: "inherit",
      env: {
        KCDSHOP_PLAYGROUND_TIMESTAMP: setPlaygroundTimestamp.toString(),
        KCDSHOP_PLAYGROUND_SRC_DIR: srcDir,
        KCDSHOP_PLAYGROUND_DEST_DIR: destDir,
        KCDSHOP_PLAYGROUND_WAS_RUNNING: playgroundWasRunning.toString(),
        KCDSHOP_PLAYGROUND_IS_STILL_RUNNING: playgroundIsStillRunning.toString(),
        KCDSHOP_PLAYGROUND_RESTART_PLAYGROUND: restartPlayground.toString()
      }
    });
  }
  if (playgroundApp && restartPlayground) {
    await runAppDev(playgroundApp);
    await waitOnApp(playgroundApp);
  }
  getOptionalWatcher()?.add(playgroundFiles);
  modifiedTimes.set(destDir, Date.now());
}
async function getPlaygroundAppName() {
  if (!await exists(playgroundAppNameInfoPath)) {
    return null;
  }
  try {
    const jsonString = await fs.promises.readFile(
      playgroundAppNameInfoPath,
      "utf8"
    );
    const { appName } = JSON.parse(jsonString);
    if (typeof appName !== "string")
      return null;
    return appName;
  } catch {
    return null;
  }
}
async function getWorkshopTitle() {
  const title = await getPkgProp(workshopRoot, "kcd-workshop.title");
  if (!title) {
    throw new Error(
      `Workshop title not found. Make sure the root of the workshop has "kcd-workshop" with a "title" property in the package.json. ${workshopRoot}`
    );
  }
  return title;
}
async function getEpicWorkshopSlug() {
  const epicWorkshopSlug = await getPkgProp(
    workshopRoot,
    "kcd-workshop.epicWorkshopSlug"
  );
  return epicWorkshopSlug || null;
}
function getWorkshopRoot() {
  return process.env.KCDSHOP_CONTEXT_CWD ?? process.cwd();
}
async function getWorkshopInstructions({
  request
} = {}) {
  const readmeFilepath = path.join(workshopRoot, "exercises", "README.mdx");
  const compiled = await compileMdx(readmeFilepath, { request }).then(
    (r) => ({ ...r, status: "success" }),
    (e) => {
      console.error(
        `There was an error compiling the workshop readme`,
        readmeFilepath,
        e
      );
      return { status: "error", error: getErrorMessage(e) };
    }
  );
  return { compiled, file: readmeFilepath, relativePath: "exercises" };
}
async function getWorkshopFinished({
  request
} = {}) {
  const finishedFilepath = path.join(workshopRoot, "exercises", "FINISHED.mdx");
  const compiled = await compileMdx(finishedFilepath, { request }).then(
    (r) => ({ ...r, status: "success" }),
    (e) => {
      console.error(
        `There was an error compiling the workshop finished.mdx`,
        finishedFilepath,
        e
      );
      return { status: "error", error: getErrorMessage(e) };
    }
  );
  return {
    compiled,
    file: finishedFilepath,
    relativePath: "exercises/finished.mdx"
  };
}
const exercisesPath = path.join(workshopRoot, "exercises/");
const playgroundPath = path.join(workshopRoot, "playground/");
function getRelativePath(filePath) {
  return path.normalize(filePath).replace(playgroundPath, `playground${path.sep}`).replace(exercisesPath, "");
}
export {
  extractNumbersFromAppName,
  getAppByName,
  getAppPageRoute,
  getApps,
  getEpicWorkshopSlug,
  getExercise,
  getExerciseApp,
  getExercises,
  getForceFreshForDir,
  getNextExerciseApp,
  getPlaygroundAppName,
  getPrevExerciseApp,
  getRelativePath,
  getWorkshopFinished,
  getWorkshopInstructions,
  getWorkshopRoot,
  getWorkshopTitle,
  init,
  isApp,
  isExampleApp,
  isExerciseStepApp,
  isFirstStepProblemApp,
  isFirstStepSolutionApp,
  isPlaygroundApp,
  isProblemApp,
  isSolutionApp,
  modifiedTimes,
  requireExercise,
  requireExerciseApp,
  setModifiedTimesForDir,
  setPlayground
};
