import { Context } from "../../bundler/context.js";
import { logVerbose, logWarning } from "../../bundler/log.js";
import { getTeamAndProjectFromPreviewAdminKey } from "./deployment.js";
import {
  assertLocalBackendRunning,
  localDeploymentUrl,
} from "./localDeployment/run.js";
import {
  ThrowingFetchError,
  bigBrainAPI,
  bigBrainAPIMaybeThrows,
  logAndHandleFetchError,
  typedPlatformClient,
} from "./utils/utils.js";
import { z } from "zod";
import {
  DeploymentSelection,
  ProjectSelection,
} from "./deploymentSelection.js";
import { loadLocalDeploymentCredentials } from "./localDeployment/localDeployment.js";
import { loadAnonymousDeployment } from "./localDeployment/anonymous.js";
import {
  parseDeploymentSelector,
  InProjectSelector,
} from "./deploymentSelector.js";
import { chalkStderr } from "chalk";
export type DeploymentName = string;
export type CloudDeploymentType = "prod" | "dev" | "preview" | "custom";
export type AccountRequiredDeploymentType = CloudDeploymentType | "local";
export type DeploymentType = AccountRequiredDeploymentType | "anonymous";

export type Project = {
  id: number;
  name: string;
  slug: string;
  isDemo: boolean;
};

type AdminKey = string;

/**
 * Create a new project. If `deploymentToProvision` is specified, also provision a deployment for the project.
 */
export async function createProject(
  ctx: Context,
  {
    teamSlug: selectedTeamSlug,
    projectName,
    deploymentToProvision,
  }: {
    teamSlug: string;
    projectName: string;
    deploymentToProvision: {
      deploymentType: "prod" | "dev";
      region: string | null;
    } | null;
  },
): Promise<{
  projectSlug: string;
  teamSlug: string;
  projectsRemaining: number;
}> {
  const provisioningArgs = {
    team: selectedTeamSlug,
    projectName,
    ...deploymentToProvision,
  };
  const data = await bigBrainAPI({
    ctx,
    method: "POST",
    path: "create_project",
    data: provisioningArgs,
  });
  const { projectSlug, teamSlug, projectsRemaining } = data;
  if (
    projectSlug === undefined ||
    teamSlug === undefined ||
    projectsRemaining === undefined
  ) {
    const error =
      "Unexpected response during provisioning: " + JSON.stringify(data);
    return await ctx.crash({
      exitCode: 1,
      errorType: "transient",
      errForSentry: error,
      printedMessage: error,
    });
  }
  return {
    projectSlug,
    teamSlug,
    projectsRemaining,
  };
}

// ----------------------------------------------------------------------
// Helpers for `deploymentSelectionFromOptions`
// ----------------------------------------------------------------------

export const deploymentSelectionWithinProjectSchema = z.discriminatedUnion(
  "kind",
  [
    z.object({ kind: z.literal("previewName"), previewName: z.string() }),
    z.object({ kind: z.literal("deploymentName"), deploymentName: z.string() }),
    z.object({ kind: z.literal("prod") }),
    z.object({ kind: z.literal("implicitProd") }),
    z.object({ kind: z.literal("ownDev") }),
    z.object({
      kind: z.literal("deploymentSelector"),
      selector: z.string(),
    }),
  ],
);

export type DeploymentSelectionWithinProject = z.infer<
  typeof deploymentSelectionWithinProjectSchema
>;

type DeploymentSelectionOptionsWithinProject = {
  prod?: boolean | undefined;
  // Whether this command defaults to prod when no other flags are provided. If
  // this is not set, the default will be "ownDev"
  implicitProd?: boolean;

  previewName?: string | undefined;
  deploymentName?: string | undefined;
  deployment?: string | undefined;
};

export type DeploymentSelectionOptions =
  DeploymentSelectionOptionsWithinProject & {
    url?: string | undefined;
    adminKey?: string | undefined;
    envFile?: string | undefined;
  };

export function deploymentSelectionWithinProjectFromOptions(
  options: DeploymentSelectionOptions,
): DeploymentSelectionWithinProject {
  if (options.deployment !== undefined) {
    return { kind: "deploymentSelector", selector: options.deployment };
  }
  if (options.previewName !== undefined) {
    return { kind: "previewName", previewName: options.previewName };
  }
  if (options.deploymentName !== undefined) {
    return { kind: "deploymentName", deploymentName: options.deploymentName };
  }
  if (options.prod) {
    return { kind: "prod" };
  }
  if (options.implicitProd) {
    return { kind: "implicitProd" };
  }
  return { kind: "ownDev" };
}

export async function validateDeploymentSelectionForExistingDeployment(
  ctx: Context,
  deploymentSelection: DeploymentSelectionWithinProject,
  source: "selfHosted" | "deployKey" | "cliArgs",
) {
  if (
    deploymentSelection.kind === "ownDev" ||
    deploymentSelection.kind === "implicitProd"
  ) {
    // These are both considered the "default" selection depending on the command, so this is always fine
    return;
  }
  if (deploymentSelection.kind === "deploymentSelector") {
    switch (source) {
      case "selfHosted":
        return await ctx.crash({
          exitCode: 1,
          errorType: "fatal",
          printedMessage:
            "The `--deployment` flag cannot be used with a self-hosted deployment.",
        });
      case "deployKey":
        return await ctx.crash({
          exitCode: 1,
          errorType: "fatal",
          printedMessage:
            "The `--deployment` flag cannot be used with CONVEX_DEPLOY_KEY.",
        });
      case "cliArgs":
        return await ctx.crash({
          exitCode: 1,
          errorType: "fatal",
          printedMessage:
            "The `--deployment` flag cannot be used with --url and --admin-key.",
        });
    }
  }
  switch (source) {
    case "selfHosted":
      return await ctx.crash({
        exitCode: 1,
        errorType: "fatal",
        printedMessage:
          "The `--prod`, `--preview-name`, and `--deployment-name` flags cannot be used with a self-hosted deployment.",
      });
    case "deployKey":
      logWarning(
        "Ignoring `--prod`, `--preview-name`, or `--deployment-name` flags and using deployment from CONVEX_DEPLOY_KEY",
      );
      break;
    case "cliArgs":
      logWarning(
        "Ignoring `--prod`, `--preview-name`, or `--deployment-name` flags since this command was run with --url and --admin-key",
      );
      break;
  }
}

// ----------------------------------------------------------------------
// Helpers for `checkAccessToSelectedProject`
// ----------------------------------------------------------------------

async function hasAccessToProject(
  ctx: Context,
  selector: { projectSlug: string; teamSlug: string },
): Promise<boolean> {
  try {
    await bigBrainAPIMaybeThrows({
      ctx,
      path: `teams/${selector.teamSlug}/projects/${selector.projectSlug}/deployments`,
      method: "GET",
    });
    return true;
  } catch (err) {
    if (
      err instanceof ThrowingFetchError &&
      (err.serverErrorData?.code === "TeamNotFound" ||
        err.serverErrorData?.code === "ProjectNotFound")
    ) {
      return false;
    }
    return logAndHandleFetchError(ctx, err);
  }
}

export async function checkAccessToSelectedProject(
  ctx: Context,
  projectSelection: ProjectSelection,
): Promise<
  | { kind: "hasAccess"; teamSlug: string; projectSlug: string }
  | { kind: "noAccess" }
  | { kind: "unknown" }
> {
  switch (projectSelection.kind) {
    case "deploymentName": {
      const result = await getTeamAndProjectSlugForDeployment(ctx, {
        deploymentName: projectSelection.deploymentName,
      });
      if (result === null) {
        return { kind: "noAccess" };
      }
      return {
        kind: "hasAccess",
        teamSlug: result.teamSlug,
        projectSlug: result.projectSlug,
      };
    }
    case "teamAndProjectSlugs": {
      const hasAccess = await hasAccessToProject(ctx, {
        teamSlug: projectSelection.teamSlug,
        projectSlug: projectSelection.projectSlug,
      });
      if (!hasAccess) {
        return { kind: "noAccess" };
      }
      return {
        kind: "hasAccess",
        teamSlug: projectSelection.teamSlug,
        projectSlug: projectSelection.projectSlug,
      };
    }
    case "projectDeployKey":
      // Ideally we would be able to do an explicit check here, but if the key is invalid,
      // it will instead fail as soon as we try to use the key.
      return { kind: "unknown" };
    default: {
      projectSelection satisfies never;
      return await ctx.crash({
        exitCode: 1,
        errorType: "fatal",
        printedMessage: `Invalid project selection: ${(projectSelection as any).kind}`,
      });
    }
  }
}

export async function getTeamAndProjectSlugForDeployment(
  ctx: Context,
  selector: { deploymentName: string },
): Promise<{ teamSlug: string; projectSlug: string } | null> {
  try {
    const body = await bigBrainAPIMaybeThrows({
      ctx,
      path: `deployment/${selector.deploymentName}/team_and_project`,
      method: "GET",
    });
    return { teamSlug: body.team, projectSlug: body.project };
  } catch (err) {
    if (
      err instanceof ThrowingFetchError &&
      (err.serverErrorData?.code === "DeploymentNotFound" ||
        err.serverErrorData?.code === "ProjectNotFound")
    ) {
      return null;
    }
    return logAndHandleFetchError(ctx, err);
  }
}

// ----------------------------------------------------------------------
// Helpers for fetching deployment credentials
// ----------------------------------------------------------------------

// Used by dev for upgrade from team and project in convex.json to CONVEX_DEPLOYMENT
export async function fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows(
  ctx: Context,
  projectSelection:
    | { kind: "teamAndProjectSlugs"; teamSlug: string; projectSlug: string }
    | { kind: "projectDeployKey"; projectDeployKey: string },
  deploymentType: "prod" | "dev",
): Promise<{
  deploymentName: string;
  deploymentUrl: string;
  adminKey: AdminKey;
}> {
  if (projectSelection.kind === "projectDeployKey") {
    const auth = ctx.bigBrainAuth();
    const doesAuthMatch =
      auth !== null &&
      auth.kind === "projectKey" &&
      auth.projectKey === projectSelection.projectDeployKey;
    if (!doesAuthMatch) {
      return await ctx.crash({
        exitCode: 1,
        errorType: "fatal",
        errForSentry: new Error(
          "Expected project deploy key to match the big brain auth header",
        ),
        printedMessage: "Unexpected error when loading the Convex deployment",
      });
    }
  }
  let data;
  try {
    data = await bigBrainAPIMaybeThrows({
      ctx,
      method: "POST",
      path: "deployment/provision_and_authorize",
      data: {
        teamSlug:
          projectSelection.kind === "teamAndProjectSlugs"
            ? projectSelection.teamSlug
            : null,
        projectSlug:
          projectSelection.kind === "teamAndProjectSlugs"
            ? projectSelection.projectSlug
            : null,
        deploymentType: deploymentType === "prod" ? "prod" : "dev",
      },
    });
  } catch (error) {
    const msg = "Unknown error during authorization: " + error;
    return await ctx.crash({
      exitCode: 1,
      errorType: "transient",
      errForSentry: new Error(msg),
      printedMessage: msg,
    });
  }
  const adminKey = data.adminKey;
  const url = data.url;
  const deploymentName = data.deploymentName;
  if (adminKey === undefined || url === undefined) {
    const msg = "Unknown error during authorization: " + JSON.stringify(data);
    return await ctx.crash({
      exitCode: 1,
      errorType: "transient",
      errForSentry: new Error(msg),
      printedMessage: msg,
    });
  }
  return { adminKey, deploymentUrl: url, deploymentName };
}

async function fetchExistingDevDeploymentCredentialsOrCrash(
  ctx: Context,
  deploymentName: DeploymentName,
): Promise<{
  deploymentName: string;
  adminKey: string;
  url: string;
  deploymentType: DeploymentType;
}> {
  const slugs = await fetchTeamAndProject(ctx, deploymentName);
  const credentials =
    await fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows(
      ctx,
      {
        kind: "teamAndProjectSlugs",
        teamSlug: slugs.team,
        projectSlug: slugs.project,
      },
      "dev",
    );
  return {
    deploymentName: credentials.deploymentName,
    adminKey: credentials.adminKey,
    url: credentials.deploymentUrl,
    deploymentType: "dev",
  };
}

// ----------------------------------------------------------------------
// Helpers for `loadSelectedDeploymentCredentials`
// ----------------------------------------------------------------------

async function handleOwnDev(
  ctx: Context,
  projectSelection: ProjectSelection,
): Promise<{
  deploymentName: string;
  adminKey: string;
  url: string;
  deploymentType: DeploymentType;
}> {
  switch (projectSelection.kind) {
    case "deploymentName": {
      if (projectSelection.deploymentType === "local") {
        const credentials = await loadLocalDeploymentCredentials(
          ctx,
          projectSelection.deploymentName,
        );
        return {
          deploymentName: projectSelection.deploymentName,
          adminKey: credentials.adminKey,
          url: credentials.deploymentUrl,
          deploymentType: "local",
        };
      }
      return await fetchExistingDevDeploymentCredentialsOrCrash(
        ctx,
        projectSelection.deploymentName,
      );
    }
    case "teamAndProjectSlugs":
    case "projectDeployKey": {
      // Note -- this provisions a dev deployment if one doesn't exist
      const credentials =
        await fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows(
          ctx,
          projectSelection,
          "dev",
        );
      return {
        url: credentials.deploymentUrl,
        adminKey: credentials.adminKey,
        deploymentName: credentials.deploymentName,
        deploymentType: "dev",
      };
    }
    default: {
      projectSelection satisfies never;
      return ctx.crash({
        exitCode: 1,
        errorType: "fatal",
        // This should be unreachable, so don't bother with a printed message.
        printedMessage: null,
        errForSentry: `Unexpected project selection: ${(projectSelection as any).kind}`,
      });
    }
  }
}

async function handleProd(
  ctx: Context,
  projectSelection: ProjectSelection,
): Promise<{
  deploymentName: string;
  adminKey: string;
  url: string;
  deploymentType: "prod";
}> {
  switch (projectSelection.kind) {
    case "deploymentName": {
      const credentials = await bigBrainAPI({
        ctx,
        method: "POST",
        path: "deployment/authorize_prod",
        data: {
          deploymentName: projectSelection.deploymentName,
        },
      });
      return credentials;
    }
    case "teamAndProjectSlugs":
    case "projectDeployKey": {
      const credentials =
        await fetchDeploymentCredentialsProvisioningDevOrProdMaybeThrows(
          ctx,
          projectSelection,
          "prod",
        );
      return {
        url: credentials.deploymentUrl,
        adminKey: credentials.adminKey,
        deploymentName: credentials.deploymentName,
        deploymentType: "prod",
      };
    }
  }
}

async function handlePreview(
  ctx: Context,
  previewName: string,
  projectSelection: ProjectSelection,
): Promise<{
  deploymentName: string;
  adminKey: string;
  url: string;
  deploymentType: "preview";
}> {
  switch (projectSelection.kind) {
    case "deploymentName":
    case "teamAndProjectSlugs":
      return await bigBrainAPI({
        ctx,
        method: "POST",
        path: "deployment/authorize_preview",
        data: {
          previewName: previewName,
          projectSelection: projectSelection,
        },
      });

    case "projectDeployKey":
      // TODO -- this should be supported
      return await ctx.crash({
        exitCode: 1,
        errorType: "fatal",
        printedMessage:
          "Project deploy keys are not supported for preview deployments",
      });
  }
}

async function handleDeploymentName(
  ctx: Context,
  deploymentName: string,
  projectSelection: ProjectSelection,
): Promise<{
  deploymentName: string;
  adminKey: string;
  url: string;
  deploymentType: DeploymentType;
}> {
  switch (projectSelection.kind) {
    case "deploymentName":
    case "teamAndProjectSlugs":
      return await bigBrainAPI({
        ctx,
        method: "POST",
        path: "deployment/authorize_within_current_project",
        data: {
          selectedDeploymentName: deploymentName,
          projectSelection: projectSelection,
        },
      });
    case "projectDeployKey":
      // TODO -- this should be supported
      return await ctx.crash({
        exitCode: 1,
        errorType: "fatal",
        printedMessage:
          "Project deploy keys are not supported with the --deployment-name flag",
      });
  }
}

async function fetchDeploymentCredentialsWithinCurrentProject(
  ctx: Context,
  projectSelection: ProjectSelection,
  deploymentSelection: DeploymentSelectionWithinProject,
): Promise<{
  deploymentName: string;
  adminKey: string;
  url: string;
  deploymentType: DeploymentType;
}> {
  switch (deploymentSelection.kind) {
    case "ownDev": {
      return await handleOwnDev(ctx, projectSelection);
    }
    case "implicitProd":
    case "prod": {
      return await handleProd(ctx, projectSelection);
    }
    case "previewName":
      return await handlePreview(
        ctx,
        deploymentSelection.previewName,
        projectSelection,
      );
    case "deploymentName":
      return await handleDeploymentName(
        ctx,
        deploymentSelection.deploymentName,
        projectSelection,
      );
    case "deploymentSelector":
      return await handleDeploymentSelector(
        ctx,
        deploymentSelection.selector,
        projectSelection,
      );
    default: {
      deploymentSelection satisfies never;
      return ctx.crash({
        exitCode: 1,
        errorType: "fatal",
        // This should be unreachable, so don't bother with a printed message.
        printedMessage: null,
        errForSentry: `Unexpected deployment selection: ${deploymentSelection as any}`,
      });
    }
  }
}

async function resolveDeploymentNameByReference(
  ctx: Context,
  teamSlug: string,
  projectSlug: string,
  reference: string,
): Promise<string> {
  try {
    const result = await typedPlatformClient(ctx, { throw: true }).GET(
      "/teams/{team_id_or_slug}/projects/{project_slug}/deployment",
      {
        params: {
          path: { team_id_or_slug: teamSlug, project_slug: projectSlug },
          query: { reference },
        },
      },
    );

    return result.data!.name;
  } catch (err) {
    if (
      err instanceof ThrowingFetchError &&
      err.serverErrorData?.code === "DeploymentNotFound"
    ) {
      return await ctx.crash({
        exitCode: 1,
        errorType: "fatal",
        printedMessage: `Deployment “${reference}” not found. To create a new deployment, use ${chalkStderr.bold(`npx convex deployment create ${reference} --team ${teamSlug} --project ${projectSlug} --select`)}`,
        errForSentry: err,
      });
    }
    return await logAndHandleFetchError(ctx, err);
  }
}

async function handleRefInProject(
  ctx: Context,
  selector: InProjectSelector,
  projectSelection: ProjectSelection,
): Promise<{
  deploymentName: string;
  adminKey: string;
  url: string;
  deploymentType: DeploymentType;
}> {
  switch (selector.kind) {
    case "dev":
      return await handleOwnDev(ctx, projectSelection);
    case "prod":
      return await handleProd(ctx, projectSelection);
    case "reference": {
      const access = await checkAccessToSelectedProject(ctx, projectSelection);
      if (access.kind !== "hasAccess") {
        return await ctx.crash({
          exitCode: 1,
          errorType: "fatal",
          printedMessage:
            "You don't have access to the selected project. Run `npx convex dev` to select a different project.",
        });
      }
      const deploymentName = await resolveDeploymentNameByReference(
        ctx,
        access.teamSlug,
        access.projectSlug,
        selector.reference,
      );
      return await handleDeploymentName(ctx, deploymentName, projectSelection);
    }
  }
}

async function handleDeploymentSelector(
  ctx: Context,
  selector: string,
  projectSelection: ProjectSelection,
): Promise<{
  deploymentName: string;
  adminKey: string;
  url: string;
  deploymentType: DeploymentType;
}> {
  const parsed = parseDeploymentSelector(selector);
  switch (parsed.kind) {
    case "deploymentName":
      return await handleDeploymentName(
        ctx,
        parsed.deploymentName,
        projectSelection,
      );
    case "inCurrentProject":
      return await handleRefInProject(ctx, parsed.selector, projectSelection);
    case "inProject": {
      const access = await checkAccessToSelectedProject(ctx, projectSelection);
      if (access.kind !== "hasAccess") {
        return await ctx.crash({
          exitCode: 1,
          errorType: "fatal",
          printedMessage:
            "You don't have access to the selected project. Run `npx convex dev` to select a different project.",
        });
      }
      return await handleRefInProject(ctx, parsed.selector, {
        kind: "teamAndProjectSlugs",
        teamSlug: access.teamSlug,
        projectSlug: parsed.projectSlug,
      });
    }
    case "inTeamProject":
      return await handleRefInProject(ctx, parsed.selector, {
        kind: "teamAndProjectSlugs",
        teamSlug: parsed.teamSlug,
        projectSlug: parsed.projectSlug,
      });
  }
}

async function _loadExistingDeploymentCredentialsForProject(
  ctx: Context,
  targetProject: ProjectSelection,
  deploymentSelection: DeploymentSelectionWithinProject,
  { ensureLocalRunning } = { ensureLocalRunning: true },
): Promise<DetailedDeploymentCredentials> {
  const accessResult = await checkAccessToSelectedProject(ctx, targetProject);
  if (accessResult.kind === "noAccess") {
    return await ctx.crash({
      exitCode: 1,
      errorType: "fatal",
      printedMessage:
        "You don't have access to the selected project. Run `npx convex dev` to select a different project.",
    });
  }
  const result = await fetchDeploymentCredentialsWithinCurrentProject(
    ctx,
    targetProject,
    deploymentSelection,
  );
  logVerbose(
    `Deployment URL: ${result.url}, Deployment Name: ${result.deploymentName}, Deployment Type: ${result.deploymentType}`,
  );
  if (ensureLocalRunning && result.deploymentType === "local") {
    await assertLocalBackendRunning(ctx, {
      url: result.url,
      deploymentName: result.deploymentName,
    });
  }
  return {
    ...result,
    deploymentFields: {
      deploymentName: result.deploymentName,
      deploymentType: result.deploymentType,

      projectSlug:
        accessResult.kind === "hasAccess" ? accessResult.projectSlug : null,
      teamSlug:
        accessResult.kind === "hasAccess" ? accessResult.teamSlug : null,
    },
  };
}

export type DetailedDeploymentCredentials = {
  adminKey: string;
  url: string;
  deploymentFields: {
    deploymentName: string;
    deploymentType: DeploymentType;
    projectSlug: string | null;
    teamSlug: string | null;
  } | null;
};

/**
 * This is used by most commands to determine which deployment to act on, taking into account the deployment selection flags.
 */
export async function loadSelectedDeploymentCredentials(
  ctx: Context,
  deploymentSelection: DeploymentSelection,
  { ensureLocalRunning } = { ensureLocalRunning: true },
): Promise<DetailedDeploymentCredentials> {
  switch (deploymentSelection.kind) {
    case "existingDeployment":
      // We're already set up.
      logVerbose(
        `Deployment URL: ${deploymentSelection.deploymentToActOn.url}, Deployment Name: ${deploymentSelection.deploymentToActOn.deploymentFields?.deploymentName ?? "unknown"}, Deployment Type: ${deploymentSelection.deploymentToActOn.deploymentFields?.deploymentType ?? "unknown"}`,
      );
      return {
        adminKey: deploymentSelection.deploymentToActOn.adminKey,
        url: deploymentSelection.deploymentToActOn.url,
        deploymentFields:
          deploymentSelection.deploymentToActOn.deploymentFields,
      };
    case "chooseProject":
      return await ctx.crash({
        exitCode: 1,
        errorType: "fatal",
        printedMessage:
          "No CONVEX_DEPLOYMENT set, run `npx convex dev` to configure a Convex project",
      });
    case "preview": {
      const slugs = await getTeamAndProjectFromPreviewAdminKey(
        ctx,
        deploymentSelection.previewDeployKey,
      );
      return await _loadExistingDeploymentCredentialsForProject(
        ctx,
        {
          kind: "teamAndProjectSlugs",
          teamSlug: slugs.teamSlug,
          projectSlug: slugs.projectSlug,
        },
        // Note that the user could select a non-preview deployment here, and it would succeed if the user is logged in locally because getBigBrainAuth prefers the user's access token over the preview deploy key.
        deploymentSelection.selectionWithinProject,
        { ensureLocalRunning },
      );
    }
    case "deploymentWithinProject": {
      return await _loadExistingDeploymentCredentialsForProject(
        ctx,
        deploymentSelection.targetProject,
        deploymentSelection.selectionWithinProject,
        { ensureLocalRunning },
      );
    }
    case "anonymous": {
      if (deploymentSelection.deploymentName === null) {
        return await ctx.crash({
          exitCode: 1,
          errorType: "fatal",
          printedMessage:
            "No CONVEX_DEPLOYMENT set, run `npx convex dev` to configure a Convex project",
        });
      }
      const config = await loadAnonymousDeployment(
        ctx,
        deploymentSelection.deploymentName,
      );
      const url = localDeploymentUrl(config.ports.cloud);
      if (ensureLocalRunning) {
        await assertLocalBackendRunning(ctx, {
          url,
          deploymentName: deploymentSelection.deploymentName,
        });
      }
      return {
        adminKey: config.adminKey,
        url,
        deploymentFields: {
          deploymentName: deploymentSelection.deploymentName,
          deploymentType: "anonymous",
          projectSlug: null,
          teamSlug: null,
        },
      };
    }
    default: {
      deploymentSelection satisfies never;
      return await ctx.crash({
        exitCode: 1,
        errorType: "fatal",
        printedMessage: "Unknown deployment type",
      });
    }
  }
}

export async function fetchTeamAndProject(
  ctx: Context,
  deploymentName: string,
) {
  const data = (await bigBrainAPI({
    ctx,
    method: "GET",
    path: `deployment/${deploymentName}/team_and_project`,
  })) as {
    team: string; // slug
    project: string; // slug
    teamId: number;
    projectId: number;
  };

  const { team, project } = data;
  if (team === undefined || project === undefined) {
    const msg =
      "Unknown error when fetching team and project: " + JSON.stringify(data);
    return await ctx.crash({
      exitCode: 1,
      errorType: "transient",
      errForSentry: new Error(msg),
      printedMessage: msg,
    });
  }

  return data;
}

export async function fetchTeamAndProjectForKey(
  ctx: Context,
  // Deployment deploy key, like `prod:happy-animal-123|<stuff>`
  deployKey: string,
) {
  const data = (await bigBrainAPI({
    ctx,
    method: "POST",
    path: `deployment/team_and_project_for_key`,
    data: {
      deployKey: deployKey,
    },
  })) as {
    team: string; // slug
    project: string; // slug
    teamId: number;
    projectId: number;
  };

  const { team, project } = data;
  if (team === undefined || project === undefined) {
    const msg =
      "Unknown error when fetching team and project: " + JSON.stringify(data);
    return await ctx.crash({
      exitCode: 1,
      errorType: "transient",
      errForSentry: new Error(msg),
      printedMessage: msg,
    });
  }

  return data;
}

export async function getTeamsForUser(ctx: Context) {
  const teams = await bigBrainAPI<{ id: number; name: string; slug: string }[]>(
    {
      ctx,
      method: "GET",
      path: "teams",
    },
  );
  return teams;
}
