import path from "path";
import prettier from "prettier";
import { DeploymentType } from "./api.js";
import { ProjectConfig } from "./config.js";
import { functionsDir } from "./utils.js";
import { reactCodegen } from "../codegen_templates/react.js";
import {
  dataModel,
  dataModelWithoutSchema,
} from "../codegen_templates/dataModel.js";
import { server } from "../codegen_templates/server.js";
import {
  processTypeCheckResult,
  typeCheckFile,
  typeCheckFunctions,
  TypeCheckMode,
} from "./typecheck.js";
import { tsconfigCodegen } from "../codegen_templates/tsconfig.js";
import { readmeCodegen } from "../codegen_templates/readme.js";
import { Context } from "./context.js";
import {
  devDeploymentConfig,
  prodDeploymentConfig,
} from "../codegen_templates/clientConfig.js";
import { GeneratedJsWithTypes } from "../codegen_templates/common.js";
import { entryPoints } from "../../bundler/index.js";

/**
 * Run prettier so we don't have to think about formatting!
 *
 * This is a little sketchy because we are using the default prettier config
 * (not our user's one) but it's better than nothing.
 */
function format(source: string, filetype: string): string {
  return prettier.format(source, { parser: filetype });
}

function writeFile(
  ctx: Context,
  filename: string,
  source: string,
  dir: string,
  dryRun: boolean,
  debug: boolean,
  quiet: boolean,
  filetype = "typescript"
) {
  const formattedSource = format(source, filetype);
  const dest = path.join(dir, filename);
  if (debug) {
    console.log(`# ${dest}`);
    console.log(formattedSource);
    return;
  }
  if (dryRun) {
    if (ctx.fs.exists(dest)) {
      const fileText = ctx.fs.readUtf8File(dest);
      if (fileText !== formattedSource) {
        console.log(`Command would replace file: ${dest}`);
      }
    } else {
      console.log(`Command would create file: ${dest}`);
    }
    return;
  }

  if (!quiet) {
    console.log(`writing ${dest}`);
  }

  ctx.fs.writeUtf8File(dest, formattedSource);
}

function writeJsWithTypes(
  ctx: Context,
  name: string,
  content: GeneratedJsWithTypes,
  dir: string,
  dryRun: boolean,
  debug: boolean,
  quiet: boolean
) {
  writeFile(ctx, `${name}.d.ts`, content.DTS, dir, dryRun, debug, quiet);
  writeFile(ctx, `${name}.js`, content.JS, dir, dryRun, debug, quiet);
}

function doServerCodegen(
  ctx: Context,
  codegenDir: string,
  dryRun: boolean,
  hasSchemaFile: boolean,
  debug: boolean,
  quiet = false
) {
  if (hasSchemaFile) {
    writeJsWithTypes(
      ctx,
      "dataModel",
      dataModel,
      codegenDir,
      dryRun,
      debug,
      quiet
    );
  } else {
    writeJsWithTypes(
      ctx,
      "dataModel",
      dataModelWithoutSchema,
      codegenDir,
      dryRun,
      debug,
      quiet
    );
  }
  writeJsWithTypes(ctx, "server", server, codegenDir, dryRun, debug, quiet);
}

async function doReactCodegen(
  ctx: Context,
  functionsDir: string,
  codegenDir: string,
  dryRun: boolean,
  debug: boolean,
  quiet = false
) {
  const modulePaths = (await entryPoints(ctx.fs, functionsDir, false)).map(
    entryPoint => path.relative(functionsDir, entryPoint)
  );
  writeJsWithTypes(
    ctx,
    "react",
    reactCodegen(modulePaths),
    codegenDir,
    dryRun,
    debug,
    quiet
  );
}

export async function doCodegen({
  ctx,
  projectConfig,
  configPath,
  typeCheckMode,
  deploymentType,
  dryRun = false,
  debug = false,
  quiet = false,
}: {
  ctx: Context;
  projectConfig: ProjectConfig;
  configPath: string;
  typeCheckMode: TypeCheckMode;
  deploymentType: DeploymentType;
  dryRun?: boolean;
  debug?: boolean;
  quiet?: boolean;
}): Promise<void> {
  const funcDir = functionsDir(configPath, projectConfig);

  // Delete the old _generated.ts because v0.1.2 used to put the react generated
  // code there
  const legacyCodegenPath = path.join(funcDir, "_generated.ts");
  if (ctx.fs.exists(legacyCodegenPath)) {
    if (!dryRun) {
      console.log(`Deleting legacy codegen file: ${legacyCodegenPath}}`);
      ctx.fs.unlink(legacyCodegenPath);
    } else {
      console.log(
        `Command would delete legacy codegen file: ${legacyCodegenPath}}`
      );
    }
  }

  // Create the function dir if it doesn't already exist.
  ctx.fs.mkdir(funcDir, { allowExisting: true });

  // Recreate the codegen directory blowing out whatever was there.
  const codegenDir = path.join(funcDir, "_generated");
  if (!dryRun && !debug) {
    ctx.fs.rm(codegenDir, { force: true, recursive: true });
    ctx.fs.mkdir(codegenDir);
  }

  const schemaPath = path.join(funcDir, "schema.ts");
  const hasSchemaFile = ctx.fs.exists(schemaPath);

  writeJsWithTypes(
    ctx,
    "clientConfig",
    deploymentType === "dev"
      ? devDeploymentConfig
      : prodDeploymentConfig(projectConfig),
    codegenDir,
    dryRun,
    debug,
    quiet
  );

  // Do things in a careful order so that we always:
  // - typecheck sources before we use them.
  // - generate code in dependency order.
  //
  // The dependency chain is:
  // _generated/react.js
  // -> query and mutation functions
  // -> _generated/server.js
  // -> schema.ts
  // (where -> means "depends on")
  // 1. Typecheck the schema.ts file
  if (hasSchemaFile) {
    await processTypeCheckResult(ctx, typeCheckMode, () =>
      typeCheckFile(ctx, path.join(funcDir, "schema.ts"))
    );
  }

  // 2. Use the schema.ts file to create the server codegen
  doServerCodegen(ctx, codegenDir, dryRun, hasSchemaFile, debug, quiet);

  // 3. Typecheck the query and mutation functions
  await processTypeCheckResult(ctx, typeCheckMode, () =>
    typeCheckFunctions(ctx, funcDir)
  );

  // 4. Generate the React code
  await doReactCodegen(ctx, funcDir, codegenDir, dryRun, debug, quiet);
}
// Code generated on new project init, after which these files are not
// automatically written again in case developers have modified them.

export function doInitCodegen(
  ctx: Context,
  functionsDir: string,
  convexPackageFromFunctions: string,
  quiet = false
) {
  const dryRun = false;
  const debug = false;
  doReadmeCodegen(ctx, functionsDir, dryRun, debug, quiet);
  doTsconfigCodegen(
    ctx,
    functionsDir,
    convexPackageFromFunctions,
    dryRun,
    debug,
    quiet
  );
}
export function doReadmeCodegen(
  ctx: Context,
  functionsDir: string,
  dryRun = false,
  debug = false,
  quiet = false
) {
  writeFile(
    ctx,
    "README.md",
    readmeCodegen(),
    functionsDir,
    dryRun,
    debug,
    quiet,
    "markdown"
  );
}
export function doTsconfigCodegen(
  ctx: Context,
  functionsDir: string,
  convexPackageFromFunctions: string,
  dryRun = false,
  debug = false,
  quiet = false
) {
  writeFile(
    ctx,
    "tsconfig.json",
    tsconfigCodegen(convexPackageFromFunctions),
    functionsDir,
    dryRun,
    debug,
    quiet,
    "json"
  );
}
