import child_process from "child_process";
import chalk from "chalk";
import path from "path";
import { Context } from "./context";
import * as Sentry from "@sentry/node";

export type TypecheckResult = "cantTypeCheck" | "success" | "typecheckFailed";

export type TypeCheckMode = "enable" | "try" | "disable";

/**
 * Conditionally run a typecheck function and interpret the result.
 *
 * If typeCheckMode === "disable", never run the typecheck function.
 * If typeCheckMode === "enable", run the typecheck and crash if typechecking
 * fails or we can't find tsc.
 * If typeCheckMode === "try", try and run the typecheck. crash if typechecking
 * fails but don't worry if tsc is missing and we can't run it.
 */
export async function processTypeCheckResult(
  ctx: Context,
  typeCheckMode: TypeCheckMode,
  runTypeCheck: () => Promise<TypecheckResult>
): Promise<void> {
  if (typeCheckMode === "disable") {
    return;
  }
  const result = await runTypeCheck();
  if (
    (result === "cantTypeCheck" && typeCheckMode === "enable") ||
    result === "typecheckFailed"
  ) {
    console.error(
      chalk.gray("To ignore failing typecheck, use `--typecheck=disable`.")
    );
    return await ctx.fatalError(1, "fs");
  }
}

// Runs TypeScript compiler to typecheck Convex query and mutation functions.
export async function typeCheckFunctions(
  ctx: Context,
  functionsDir: string
): Promise<TypecheckResult> {
  const tsconfig = path.join(functionsDir, "tsconfig.json");
  if (!ctx.fs.exists(tsconfig)) {
    console.error(
      "Can't find convex/tsconfig.json to use to typecheck Convex functions."
    );
    console.error("Run `npx convex codegen --tsconfig` to create one.");
    return "cantTypeCheck";
  }
  return runTsc(ctx, ["--project", functionsDir]);
}

async function runTsc(
  ctx: Context,
  tscArgs: string[]
): Promise<TypecheckResult> {
  const tscPath = path.join(
    "node_modules",
    ".bin",
    process.platform == "win32" ? "tsc.CMD" : "tsc"
  );
  if (!ctx.fs.exists(tscPath)) {
    return "cantTypeCheck";
  }
  // Run `tsc` once and have it print out the files it touched. This output won't
  // be very useful if there's an error, but we'll run it again to get a nice
  // user-facing error in this exceptional case.
  // The `--listFiles` command prints out files touched on success or error.
  const result = child_process.spawnSync(
    tscPath,
    tscArgs.concat("--listFiles")
  );
  if (result.status === null) {
    console.error(chalk.red(`TypeScript typecheck timed out.`));
    if (result.error) {
      console.error(chalk.red(`${result.error}`));
    }
    return "typecheckFailed";
  }
  // Okay, we may have failed `tsc` but at least it returned. Try to parse its
  // output to discover which files it touched.
  const filesTouched = result.stdout
    .toString("utf-8")
    .split("\n")
    .map(s => s.trim())
    .filter(s => s.length > 0);
  let anyPathsFound = false;
  for (const fileTouched of filesTouched) {
    const absPath = path.resolve(fileTouched);
    let st;
    try {
      st = ctx.fs.stat(absPath);
      anyPathsFound = true;
    } catch (err: any) {
      // Just move on if we have a bogus path from `tsc`. We'll log below if
      // we fail to stat *any* of the paths emitted by `tsc`.
      // TODO: Switch to using their JS API so we can get machine readable output.
      continue;
    }
    ctx.fs.registerPath(absPath, st);
  }
  if (filesTouched.length > 0 && !anyPathsFound) {
    const err = new Error(
      `Failed to stat any files emitted by tsc (received ${filesTouched.length})`
    );
    Sentry.captureException(err);
  }

  if (!result.error && result.status === 0) {
    return "success";
  }
  // At this point we know that `tsc` failed. Rerun it without `--listFiles`
  // and with stderr redirected to have it print out a nice error.
  try {
    // prettier-ignore
    child_process.execFileSync(
      tscPath,
      tscArgs,
      { stdio: "inherit" }
    );
    // If this passes, we had a concurrent file change that'll overlap with
    // our observations in the first run. Invalidate our context's filesystem
    // but allow the rest of the system to observe the success.
    ctx.fs.invalidate();
    return "success";
  } catch (e) {
    console.error(chalk.red("TypeScript typecheck via `tsc` failed."));
    return "typecheckFailed";
  }
}
