import { chalkStderr } from "chalk";
import { Command, Option } from "@commander-js/extra-typings";
import { Context, oneoffContext } from "../bundler/context.js";
import { logFinishedStep, logMessage, showSpinner } from "../bundler/log.js";
import { loadSelectedDeploymentCredentials } from "./lib/api.js";
import {
  gitBranchFromEnvironment,
  isNonProdBuildEnvironment,
  suggestedEnvVarNames,
} from "./lib/envvars.js";
import { PushOptions } from "./lib/components.js";
import {
  CONVEX_DEPLOY_KEY_ENV_VAR_NAME,
  CONVEX_SELF_HOSTED_URL_VAR_NAME,
  CONVEX_DEPLOYMENT_ENV_VAR_NAME,
  bigBrainAPI,
} from "./lib/utils/utils.js";
import { runFunctionAndLog } from "./lib/run.js";
import { usageStateWarning } from "./lib/usage.js";
import { getTeamAndProjectFromPreviewAdminKey } from "./lib/deployment.js";
import { runPush } from "./lib/components.js";
import { promptYesNo } from "./lib/utils/prompts.js";
import { deployToDeployment, runCommand } from "./lib/deploy2.js";
import { getDeploymentSelection } from "./lib/deploymentSelection.js";
import { deploymentNameAndTypeFromSelection } from "./lib/deploymentSelection.js";
import { checkVersion } from "./lib/updates.js";
import { readProjectConfig, getAuthKitConfig } from "./lib/config.js";
import { ensureAuthKitProvisionedBeforeBuild } from "./lib/workos/workos.js";
import { DASHBOARD_HOST } from "./lib/dashboard.js";
import { extractDeploymentNameForWorkOS } from "./lib/extractDeploymentNameForWorkOS.js";

export const deploy = new Command("deploy")
  .summary("Deploy to your prod deployment")
  .description(
    "Deploy to your deployment. By default, this deploys to your prod deployment.\n\n" +
      `Deploys to a preview deployment if the \`${CONVEX_DEPLOY_KEY_ENV_VAR_NAME}\` environment variable is set to a Preview Deploy Key.`,
  )
  .allowExcessArguments(false)
  .addDeployOptions()
  .addOption(
    new Option(
      "--preview-run <functionName>",
      "Function to run if deploying to a preview deployment. This is ignored if deploying to a production deployment.",
    ),
  )
  .addOption(
    new Option(
      "--preview-create <name>",
      "The name to associate with this deployment if deploying to a newly created preview deployment. Defaults to the current Git branch name in Vercel, Netlify and GitHub CI. This parameter can only be used with a preview deploy key (when used with another type of key, the command will return an error).",
    ).conflicts("preview-name"),
  )
  .addOption(
    new Option(
      "--check-build-environment <mode>",
      "Whether to check for a non-production build environment before deploying to a production Convex deployment.",
    )
      .choices(["enable", "disable"] as const)
      .default("enable" as const)
      .hideHelp(),
  )
  // Hidden options to pass in admin key and url for tests and local development
  .addOption(new Option("--admin-key <adminKey>").hideHelp())
  .addOption(new Option("--url <url>").hideHelp())
  .addOption(
    new Option(
      "--preview-name <name>",
      "[deprecated] Use `--preview-create` instead. The name to associate with this deployment if deploying to a preview deployment.",
    )
      .hideHelp()
      .conflicts("preview-create"),
  )
  .addOption(
    new Option(
      "--env-file <envFile>",
      `Path to a custom file of environment variables, for choosing the \
deployment, e.g. ${CONVEX_DEPLOYMENT_ENV_VAR_NAME} or ${CONVEX_SELF_HOSTED_URL_VAR_NAME}. \
Same format as .env.local or .env files, and overrides them.`,
    ),
  )
  .addOption(
    new Option(
      "--skip-workos-check",
      "Skip WorkOS AuthKit provisioning and credential checks during deploy.",
    ).hideHelp(),
  )
  .addOption(
    new Option("--allow-deleting-large-indexes")
      .hideHelp()
      .conflicts("preview-create")
      .conflicts("preview-name"),
  )
  .showHelpAfterError()
  .action(async (cmdOptions) => {
    const ctx = await oneoffContext(cmdOptions);

    const deploymentSelection = await getDeploymentSelection(ctx, cmdOptions);
    if (
      cmdOptions.checkBuildEnvironment === "enable" &&
      isNonProdBuildEnvironment() &&
      deploymentSelection.kind === "existingDeployment" &&
      deploymentSelection.deploymentToActOn.source === "deployKey" &&
      deploymentSelection.deploymentToActOn.deploymentFields?.deploymentType ===
        "prod"
    ) {
      await ctx.crash({
        exitCode: 1,
        errorType: "invalid filesystem data",
        printedMessage: `Detected a non-production build environment and "${CONVEX_DEPLOY_KEY_ENV_VAR_NAME}" for a production Convex deployment.\n
          This is probably unintentional.
          `,
      });
    }

    if (deploymentSelection.kind === "anonymous") {
      logMessage(
        "You are currently developing anonymously with a locally running project.\n" +
          "To deploy your Convex app to the cloud, log in by running `npx convex login`.\n" +
          "See https://docs.convex.dev/production for more information on how Convex cloud works and instructions on how to set up hosting.",
      );
      return await ctx.crash({
        exitCode: 1,
        errorType: "fatal",
        printedMessage: null,
      });
    }

    if (deploymentSelection.kind === "preview") {
      // TODO -- add usage state warnings here too once we can do it without a deployment name
      // await usageStateWarning(ctx);
      if (cmdOptions.previewName !== undefined) {
        await ctx.crash({
          exitCode: 1,
          errorType: "fatal",
          printedMessage:
            "The `--preview-name` flag has been deprecated in favor of `--preview-create`. Please re-run the command using `--preview-create` instead.",
        });
      }

      const teamAndProjectSlugs = await getTeamAndProjectFromPreviewAdminKey(
        ctx,
        deploymentSelection.previewDeployKey,
      );
      await deployToNewPreviewDeployment(
        ctx,
        {
          previewDeployKey: deploymentSelection.previewDeployKey,
          projectSelection: {
            kind: "teamAndProjectSlugs",
            teamSlug: teamAndProjectSlugs.teamSlug,
            projectSlug: teamAndProjectSlugs.projectSlug,
          },
        },
        {
          ...cmdOptions,
        },
      );
    } else {
      if (cmdOptions.previewCreate !== undefined) {
        const source =
          deploymentSelection.kind === "deploymentWithinProject" &&
          deploymentSelection.targetProject.kind === "deploymentName"
            ? `at ${chalkStderr.blue.underline(`${DASHBOARD_HOST}/dp/${deploymentSelection.targetProject.deploymentName}/settings#preview-deploy-keys`)}`
            : deploymentSelection.kind === "existingDeployment" &&
                deploymentSelection.deploymentToActOn.deploymentFields !== null
              ? `at ${chalkStderr.blue.underline(`${DASHBOARD_HOST}/dp/${deploymentSelection.deploymentToActOn.deploymentFields.deploymentName}/settings#preview-deploy-keys`)}`
              : "on the dashboard";
        await ctx.crash({
          exitCode: 1,
          errorType: "fatal",
          printedMessage: `Preview deployments can only be created with preview deploy keys. Generate a preview deploy key ${source} and set the ${chalkStderr.bold(`CONVEX_DEPLOY_KEY`)} environment variable with it.`,
        });
      }

      await deployToExistingDeployment(ctx, {
        ...cmdOptions,
        skipWorkosCheck: cmdOptions.skipWorkosCheck ?? false,
        allowDeletingLargeIndexes:
          cmdOptions.allowDeletingLargeIndexes ?? false,
      });
    }
  });

async function deployToNewPreviewDeployment(
  ctx: Context,
  deploymentSelection: {
    previewDeployKey: string;
    projectSelection: {
      kind: "teamAndProjectSlugs";
      teamSlug: string;
      projectSlug: string;
    };
  },
  options: {
    dryRun?: boolean | undefined;
    previewCreate?: string | undefined;
    previewRun?: string | undefined;
    cmdUrlEnvVarName?: string | undefined;
    cmd?: string | undefined;
    verbose?: boolean | undefined;
    typecheck: "enable" | "try" | "disable";
    typecheckComponents: boolean;
    codegen: "enable" | "disable";
    pushAllModules?: boolean;

    debug?: boolean | undefined;
    debugBundlePath?: string | undefined;
    skipWorkosCheck?: boolean | undefined;
  },
) {
  const previewName = options.previewCreate ?? gitBranchFromEnvironment();
  if (previewName === null) {
    await ctx.crash({
      exitCode: 1,
      errorType: "fatal",
      printedMessage:
        "`npx convex deploy` to a preview deployment could not determine the preview name. Provide one using `--preview-create`",
    });
  }

  if (options.dryRun) {
    logFinishedStep(
      `Would have claimed preview deployment for "${previewName}"`,
    );
    await runCommand(ctx, {
      cmdUrlEnvVarName: options.cmdUrlEnvVarName,
      cmd: options.cmd,
      dryRun: !!options.dryRun,
      url: "https://<PREVIEW DEPLOYMENT>.convex.cloud",
      adminKey: "preview-deployment-admin-key",
    });
    logFinishedStep(
      `Would have deployed Convex functions to preview deployment for "${previewName}"`,
    );
    if (options.previewRun !== undefined) {
      logMessage(`Would have run function "${options.previewRun}"`);
    }
    return;
  }
  const data = await bigBrainAPI({
    ctx,
    method: "POST",
    path: "claim_preview_deployment",
    data: {
      projectSelection: deploymentSelection.projectSelection,
      identifier: previewName,
    },
  });

  const previewAdminKey = data.adminKey;
  const previewUrl = data.instanceUrl;

  // Extract deployment name from URL for WorkOS provisioning
  const deploymentNameForWorkOS = extractDeploymentNameForWorkOS(previewUrl);

  // Provision WorkOS before building the client bundle (if configured)
  const { projectConfig } = await readProjectConfig(ctx);
  const authKitConfig = await getAuthKitConfig(ctx, projectConfig);

  if (authKitConfig && deploymentNameForWorkOS && !options.skipWorkosCheck) {
    await ensureAuthKitProvisionedBeforeBuild(
      ctx,
      deploymentNameForWorkOS,
      { deploymentUrl: previewUrl, adminKey: previewAdminKey },
      "preview",
    );
  }

  await runCommand(ctx, {
    ...options,
    url: previewUrl,
    adminKey: previewAdminKey,
  });

  const pushOptions: PushOptions = {
    deploymentName: null,
    adminKey: previewAdminKey,
    verbose: !!options.verbose,
    dryRun: false,
    typecheck: options.typecheck,
    typecheckComponents: options.typecheckComponents,
    debug: !!options.debug,
    debugBundlePath: options.debugBundlePath,
    debugNodeApis: false,
    codegen: options.codegen === "enable",
    url: previewUrl,
    liveComponentSources: false,
    pushAllModules: !!options.pushAllModules,
    largeIndexDeletionCheck: "no verification", // fine for preview deployments
  };
  showSpinner(`Deploying to ${previewUrl}...`);
  await runPush(ctx, pushOptions);
  logFinishedStep(`Deployed Convex functions to ${previewUrl}`);

  if (options.previewRun !== undefined) {
    await runFunctionAndLog(ctx, {
      deploymentUrl: previewUrl,
      adminKey: previewAdminKey,
      functionName: options.previewRun,
      argsString: "{}",
      componentPath: undefined,
      callbacks: {
        onSuccess: () => {
          logFinishedStep(`Finished running function "${options.previewRun}"`);
        },
      },
    });
  }
}

async function deployToExistingDeployment(
  ctx: Context,
  options: {
    verbose?: boolean | undefined;
    dryRun?: boolean | undefined;
    yes?: boolean | undefined;
    typecheck: "enable" | "try" | "disable";
    typecheckComponents: boolean;
    codegen: "enable" | "disable";
    cmd?: string | undefined;
    cmdUrlEnvVarName?: string | undefined;
    pushAllModules?: boolean;

    debugBundlePath?: string | undefined;
    debug?: boolean | undefined;
    adminKey?: string | undefined;
    url?: string | undefined;
    writePushRequest?: string | undefined;
    liveComponentSources?: boolean | undefined;
    envFile?: string | undefined;
    skipWorkosCheck?: boolean | undefined;
    allowDeletingLargeIndexes: boolean;
  },
) {
  const deploymentSelection = await getDeploymentSelection(ctx, {
    ...options,
    implicitProd: true,
  });
  const deploymentToActOn = await loadSelectedDeploymentCredentials(
    ctx,
    deploymentSelection,
  );
  const { deploymentFields } = deploymentToActOn;

  const configuredDeployment =
    deploymentNameAndTypeFromSelection(deploymentSelection);
  if (configuredDeployment !== null && configuredDeployment.name !== null) {
    const shouldPushToProd =
      configuredDeployment.name === deploymentFields?.deploymentName ||
      (options.yes ??
        (await askToConfirmPush(
          ctx,
          {
            configuredName: configuredDeployment.name,
            configuredType: configuredDeployment.type,
            requestedName: deploymentFields?.deploymentName!,
            requestedType: deploymentFields?.deploymentType!,
          },
          deploymentToActOn.url,
        )));
    if (!shouldPushToProd) {
      await ctx.crash({
        exitCode: 1,
        printedMessage: null,
        errorType: "fatal",
      });
    }
  }

  const isCloudDeployment = deploymentFields !== null;
  await Promise.all([
    deployToDeployment(
      ctx,
      {
        url: deploymentToActOn.url,
        adminKey: deploymentToActOn.adminKey,
        deploymentName: deploymentFields?.deploymentName ?? null,
        ...(deploymentFields?.deploymentType !== undefined
          ? { deploymentType: deploymentFields.deploymentType }
          : {}),
      },
      { ...options, skipWorkosCheck: options.skipWorkosCheck },
    ),
    ...(isCloudDeployment
      ? [
          usageStateWarning(ctx, deploymentFields.deploymentName),
          checkVersion(ctx),
        ]
      : []),
  ]);
}

async function askToConfirmPush(
  ctx: Context,
  deployment: {
    configuredName: string;
    configuredType: string | null;
    requestedName: string;
    requestedType: string;
  },
  prodUrl: string,
) {
  logMessage(
    `\
You're currently developing against your ${chalkStderr.bold(
      deployment.configuredType ?? "dev",
    )} deployment

  ${deployment.configuredName} (set in CONVEX_DEPLOYMENT)

Your ${chalkStderr.bold(deployment.requestedType)} deployment ${chalkStderr.bold(
      deployment.requestedName,
    )} serves traffic at:

  ${(await suggestedEnvVarNames(ctx)).convexUrlEnvVar}=${chalkStderr.bold(prodUrl)}

Make sure that your published client is configured with this URL (for instructions see https://docs.convex.dev/hosting)\n`,
  );
  return promptYesNo(ctx, {
    message: `Do you want to push your code to your ${deployment.requestedType} deployment ${deployment.requestedName} now?`,
    default: true,
  });
}
