import type {
  BackgroundWorkerMetadata,
  BackgroundWorkerSourceFileMetadata,
  CreateBackgroundWorkerRequestBody,
  PromptResource,
  QueueManifest,
  TaskResource,
} from "@trigger.dev/core/v3";
import { tryCatch } from "@trigger.dev/core/v3";
import { BackgroundWorkerId, stringifyDuration } from "@trigger.dev/core/v3/isomorphic";
import type { BackgroundWorker, TaskQueue, TaskQueueType } from "@trigger.dev/database";
import cronstrue from "cronstrue";
import type { PrismaClientOrTransaction } from "~/db.server";
import { $transaction, Prisma } from "~/db.server";
import { sanitizeQueueName } from "~/models/taskQueue.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { syncTaskIdentifiers } from "~/services/taskIdentifierRegistry.server";
import {
  type TaskMetadataCache,
  type TaskMetadataEntry,
} from "~/services/taskMetadataCache.server";
import { taskMetadataCacheInstance } from "~/services/taskMetadataCacheInstance.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { engine } from "../runEngine.server";
import {
  removeQueueConcurrencyLimits,
  updateEnvConcurrencyLimits,
  updateQueueConcurrencyLimits,
} from "../runQueue.server";
import { scheduleEngine } from "../scheduleEngine.server";
import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion";
import { clampMaxDuration } from "../utils/maxDuration";
import { BaseService, ServiceValidationError } from "./baseService.server";
import { CheckScheduleService } from "./checkSchedule.server";
import { projectPubSub } from "./projectPubSub.server";

import { assertNoDuplicateTaskIds } from "./duplicateTaskIds.server";
import { stripBackgroundWorkerMetadataForStorage } from "./stripBackgroundWorkerMetadataForStorage.server";
export { stripBackgroundWorkerMetadataForStorage };

export class CreateBackgroundWorkerService extends BaseService {
  private readonly _taskMetaCache: TaskMetadataCache;

  constructor(
    prisma?: PrismaClientOrTransaction,
    replica?: PrismaClientOrTransaction,
    taskMetaCache: TaskMetadataCache = taskMetadataCacheInstance
  ) {
    super(prisma, replica);
    this._taskMetaCache = taskMetaCache;
  }

  public async call(
    projectRef: string,
    environment: AuthenticatedEnvironment,
    body: CreateBackgroundWorkerRequestBody
  ): Promise<BackgroundWorker> {
    return this.traceWithEnv("call", environment, async (span) => {
      span.setAttribute("projectRef", projectRef);

      const project = await this._prisma.project.findFirstOrThrow({
        where: {
          externalRef: projectRef,
          environments: {
            some: {
              id: environment.id,
            },
          },
        },
        include: {
          backgroundWorkers: {
            where: {
              runtimeEnvironmentId: environment.id,
            },
            orderBy: {
              createdAt: "desc",
            },
            take: 1,
          },
        },
      });

      const latestBackgroundWorker = project.backgroundWorkers[0];

      if (latestBackgroundWorker?.contentHash === body.metadata.contentHash) {
        return latestBackgroundWorker;
      }

      const nextVersion = calculateNextBuildVersion(project.backgroundWorkers[0]?.version);

      logger.debug(`Creating background worker`, {
        nextVersion,
        lastVersion: project.backgroundWorkers[0]?.version,
      });

      const backgroundWorker = await this._prisma.backgroundWorker.create({
        data: {
          ...BackgroundWorkerId.generate(),
          version: nextVersion,
          runtimeEnvironmentId: environment.id,
          projectId: project.id,
          metadata: stripBackgroundWorkerMetadataForStorage(body.metadata),
          contentHash: body.metadata.contentHash,
          cliVersion: body.metadata.cliPackageVersion,
          sdkVersion: body.metadata.packageVersion,
          runtime: body.metadata.runtime,
          runtimeVersion: body.metadata.runtimeVersion,
          supportsLazyAttempts: body.supportsLazyAttempts,
          engine: body.engine,
        },
      });

      //upgrade the project to engine "V2" if it's not already
      if (project.engine === "V1" && body.engine === "V2") {
        await this._prisma.project.update({
          where: {
            id: project.id,
          },
          data: {
            engine: "V2",
          },
        });
      }

      const [filesError, tasksToBackgroundFiles] = await tryCatch(
        createBackgroundFiles(
          body.metadata.sourceFiles,
          backgroundWorker,
          environment,
          this._prisma
        )
      );

      if (filesError) {
        logger.error("Error creating background worker files", {
          error: filesError,
          backgroundWorker,
          environment,
        });

        throw new ServiceValidationError("Error creating background worker files");
      }

      const [resourcesError, workerTaskEntries] = await tryCatch(
        createWorkerResources(
          body.metadata,
          backgroundWorker,
          environment,
          this._prisma,
          tasksToBackgroundFiles
        )
      );

      if (resourcesError) {
        if (resourcesError instanceof ServiceValidationError) {
          // Customer-facing config error (e.g. duplicate task ids). Surface the
          // real message to the client via the rethrow.
          logger.warn("Error creating worker resources", {
            error: resourcesError.message,
          });
          throw resourcesError;
        }

        logger.error("Error creating worker resources", {
          error: resourcesError,
          backgroundWorker,
          environment,
        });
        throw new ServiceValidationError("Error creating worker resources");
      }

      const [schedulesError] = await tryCatch(
        syncDeclarativeSchedules(body.metadata.tasks, backgroundWorker, environment, this._prisma)
      );

      if (schedulesError) {
        if (schedulesError instanceof ServiceValidationError) {
          // Customer schedule config (typically invalid cron). Surface to
          // client via the rethrow; system returns gracefully.
          logger.warn("Error syncing declarative schedules", {
            error: schedulesError.message,
            backgroundWorker,
            environment,
          });
          throw schedulesError;
        }

        // Wrapping the underlying error into a ServiceValidationError below
        // would otherwise hide it once the SDK-level filter drops SVEs; log at
        // error so the underlying cause stays visible. Mirrors the
        // waitpointCompletionPacket.server.ts pattern from dac9c83bd.
        logger.error("Error syncing declarative schedules", {
          error: schedulesError,
          backgroundWorker,
          environment,
        });

        throw new ServiceValidationError("Error syncing declarative schedules");
      }

      const [syncIdentifiersError] = await tryCatch(
        syncTaskIdentifiers(
          environment.id,
          project.id,
          backgroundWorker.id,
          body.metadata.tasks.map((t) => ({ id: t.id, triggerSource: t.triggerSource }))
        )
      );

      if (syncIdentifiersError) {
        logger.error("Error syncing task identifiers", {
          error: syncIdentifiersError,
          backgroundWorker,
          environment,
        });
      }

      // Populate task metadata cache. DEV workers are always "current" because
      // `findCurrentWorkerFromEnvironment` resolves DEV current as the latest
      // worker by createdAt. Non-DEV (deploy-built) workers are not promoted
      // here — promotion writes the `:env:` keyspace later in
      // changeCurrentDeployment / createDeploymentBackgroundWorkerV3.
      // Cache calls log+swallow internally, so a Redis blip can't break
      // anything else here. Empty `workerTaskEntries` is intentional — the
      // populate methods clear stale hashes for zero-task deploys.
      if (workerTaskEntries) {
        if (environment.type === "DEVELOPMENT") {
          await this._taskMetaCache.populateByCurrentWorker(
            environment.id,
            backgroundWorker.id,
            workerTaskEntries
          );
        } else {
          await this._taskMetaCache.populateByWorker(backgroundWorker.id, workerTaskEntries);
        }
      }

      const [updateConcurrencyLimitsError] = await tryCatch(
        updateEnvConcurrencyLimits(environment)
      );

      if (updateConcurrencyLimitsError) {
        logger.error("Error updating environment concurrency limits", {
          error: updateConcurrencyLimitsError,
          backgroundWorker,
          environment,
        });
      }

      const [publishError] = await tryCatch(
        projectPubSub.publish(`project:${project.id}:env:${environment.id}`, "WORKER_CREATED", {
          environmentId: environment.id,
          environmentType: environment.type,
          createdAt: backgroundWorker.createdAt,
          taskCount: body.metadata.tasks.length,
          type: "local",
        })
      );

      if (publishError) {
        logger.error("Error publishing WORKER_CREATED event", {
          error: publishError,
          backgroundWorker,
          environment,
        });
      }

      if (backgroundWorker.engine === "V2") {
        const [schedulePendingVersionsError] = await tryCatch(
          engine.scheduleEnqueueRunsForBackgroundWorker(backgroundWorker.id)
        );

        if (schedulePendingVersionsError) {
          logger.error("Error scheduling pending versions", {
            error: schedulePendingVersionsError,
          });
        }
      }

      return backgroundWorker;
    });
  }
}

export async function createWorkerResources(
  metadata: BackgroundWorkerMetadata,
  worker: BackgroundWorker,
  environment: AuthenticatedEnvironment,
  prisma: PrismaClientOrTransaction,
  tasksToBackgroundFiles?: Map<string, string>
): Promise<TaskMetadataEntry[]> {
  // Defense-in-depth against two tasks sharing an id (across all task types,
  // e.g. a schedule and a regular task). Note: the CLI's resource catalog keys
  // tasks by id and overwrites collisions, so duplicates are normally already
  // collapsed before reaching here — this guards against any client that sends
  // an un-deduplicated task list.
  assertNoDuplicateTaskIds(metadata.tasks);

  // Create the queues
  const queues = await createWorkerQueues(metadata, worker, environment, prisma);

  // Create the tasks
  const taskEntries = await createWorkerTasks(
    metadata,
    queues,
    worker,
    environment,
    prisma,
    tasksToBackgroundFiles
  );

  // Register prompts
  if (metadata.prompts && metadata.prompts.length > 0) {
    await createWorkerPrompts(metadata.prompts, worker, environment, prisma);
  }

  return taskEntries;
}

async function createWorkerTasks(
  metadata: BackgroundWorkerMetadata,
  queues: Array<TaskQueue>,
  worker: BackgroundWorker,
  environment: AuthenticatedEnvironment,
  prisma: PrismaClientOrTransaction,
  tasksToBackgroundFiles?: Map<string, string>
): Promise<TaskMetadataEntry[]> {
  // Create tasks in chunks of 20
  const CHUNK_SIZE = 20;
  const entries: TaskMetadataEntry[] = [];
  for (let i = 0; i < metadata.tasks.length; i += CHUNK_SIZE) {
    const chunk = metadata.tasks.slice(i, i + CHUNK_SIZE);
    const chunkEntries = await Promise.all(
      chunk.map((task) =>
        createWorkerTask(task, queues, worker, environment, prisma, tasksToBackgroundFiles)
      )
    );
    for (const entry of chunkEntries) {
      if (entry) entries.push(entry);
    }
  }
  return entries;
}

async function createWorkerTask(
  task: TaskResource,
  queues: Array<TaskQueue>,
  worker: BackgroundWorker,
  environment: AuthenticatedEnvironment,
  prisma: PrismaClientOrTransaction,
  tasksToBackgroundFiles?: Map<string, string>
): Promise<TaskMetadataEntry | null> {
  // Hoisted so the P2002 catch branch can return the same entry shape.
  let queue: TaskQueue | undefined;
  let resolvedTriggerSource: "SCHEDULED" | "AGENT" | "STANDARD" | undefined;
  let resolvedTtl: string | null | undefined;

  try {
    queue = queues.find((queue) => queue.name === task.queue?.name);

    if (!queue) {
      // Create a TaskQueue
      queue = await createWorkerQueue(
        {
          name: task.queue?.name ?? `task/${task.id}`,
          concurrencyLimit: task.queue?.concurrencyLimit,
        },
        task.id,
        task.queue?.name ? "NAMED" : "VIRTUAL",
        worker,
        environment,
        prisma
      );
    }

    resolvedTriggerSource =
      task.triggerSource === "schedule"
        ? ("SCHEDULED" as const)
        : task.triggerSource === "agent"
          ? ("AGENT" as const)
          : ("STANDARD" as const);

    resolvedTtl =
      typeof task.ttl === "number" ? (stringifyDuration(task.ttl) ?? null) : (task.ttl ?? null);

    await prisma.backgroundWorkerTask.create({
      data: {
        friendlyId: generateFriendlyId("task"),
        projectId: worker.projectId,
        runtimeEnvironmentId: worker.runtimeEnvironmentId,
        workerId: worker.id,
        slug: task.id,
        description: task.description,
        filePath: task.filePath,
        exportName: task.exportName,
        retryConfig: task.retry,
        queueConfig: task.queue,
        machineConfig: task.machine,
        triggerSource: resolvedTriggerSource,
        config: task.agentConfig ? (task.agentConfig as any) : undefined,
        fileId: tasksToBackgroundFiles?.get(task.id) ?? null,
        maxDurationInSeconds: task.maxDuration ? clampMaxDuration(task.maxDuration) : null,
        ttl: resolvedTtl,
        queueId: queue.id,
        payloadSchema: task.payloadSchema as any,
      },
    });

    return {
      slug: task.id,
      ttl: resolvedTtl,
      triggerSource: resolvedTriggerSource,
      queueId: queue.id,
      queueName: queue.name,
    };
  } catch (error) {
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      // The error code for unique constraint violation in Prisma is P2002
      if (error.code === "P2002") {
        // Retry landing after the first attempt's row was already written.
        const existing = await prisma.backgroundWorkerTask.findFirst({
          where: { workerId: worker.id, slug: task.id },
          select: { id: true },
        });

        logger.warn("Attempted to recreate background worker task", {
          task,
          worker,
        });

        if (existing && queue && resolvedTriggerSource && resolvedTtl !== undefined) {
          return {
            slug: task.id,
            ttl: resolvedTtl,
            triggerSource: resolvedTriggerSource,
            queueId: queue.id,
            queueName: queue.name,
          };
        }
      } else {
        logger.error("Prisma Error creating background worker task", {
          error: {
            code: error.code,
            message: error.message,
          },
          task,
          worker,
        });
      }
    } else if (error instanceof Error) {
      logger.error("Error creating background worker task", {
        error: {
          name: error.name,
          message: error.message,
          stack: error.stack,
        },
        task,
        worker,
      });
    } else {
      logger.error("Unknown error creating background worker task", {
        error,
        task,
        worker,
      });
    }
    return null;
  }
}

async function createWorkerQueues(
  metadata: BackgroundWorkerMetadata,
  worker: BackgroundWorker,
  environment: AuthenticatedEnvironment,
  prisma: PrismaClientOrTransaction
) {
  if (!metadata.queues) {
    return [];
  }

  const CHUNK_SIZE = 20;
  const allQueues: Awaited<ReturnType<typeof createWorkerQueue>>[] = [];

  // Process queues in chunks
  for (let i = 0; i < metadata.queues.length; i += CHUNK_SIZE) {
    const chunk = metadata.queues.slice(i, i + CHUNK_SIZE);
    const queueChunk = await Promise.all(
      chunk.map(async (queue) => {
        return createWorkerQueue(queue, queue.name, "NAMED", worker, environment, prisma);
      })
    );
    allQueues.push(...queueChunk.filter(Boolean));
  }

  return allQueues;
}

async function createWorkerQueue(
  queue: QueueManifest,
  orderableName: string,
  queueType: TaskQueueType,
  worker: BackgroundWorker,
  environment: AuthenticatedEnvironment,
  prisma: PrismaClientOrTransaction
) {
  let queueName = sanitizeQueueName(queue.name);

  const baseConcurrencyLimit =
    typeof queue.concurrencyLimit === "number"
      ? Math.max(Math.min(queue.concurrencyLimit, environment.maximumConcurrencyLimit), 0)
      : queue.concurrencyLimit;

  const taskQueue = await upsertWorkerQueueRecord(
    queueName,
    baseConcurrencyLimit ?? null,
    orderableName,
    queueType,
    worker,
    prisma
  );

  const newConcurrencyLimit = taskQueue.concurrencyLimit;

  if (!taskQueue.paused) {
    if (typeof newConcurrencyLimit === "number") {
      logger.debug("createWorkerQueue: updating concurrency limit", {
        workerId: worker.id,
        taskQueue,
        orgId: environment.organizationId,
        projectId: environment.projectId,
        environmentId: environment.id,
        concurrencyLimit: newConcurrencyLimit,
      });
      await updateQueueConcurrencyLimits(environment, taskQueue.name, newConcurrencyLimit);
    } else {
      logger.debug("createWorkerQueue: removing concurrency limit", {
        workerId: worker.id,
        taskQueue,
        orgId: environment.organizationId,
        projectId: environment.projectId,
        environmentId: environment.id,
        concurrencyLimit: newConcurrencyLimit,
      });
      await removeQueueConcurrencyLimits(environment, taskQueue.name);
    }
  } else {
    logger.debug("createWorkerQueue: queue is paused, not updating concurrency limit", {
      workerId: worker.id,
      taskQueue,
      orgId: environment.organizationId,
      projectId: environment.projectId,
      environmentId: environment.id,
    });
  }

  return taskQueue;
}

async function upsertWorkerQueueRecord(
  queueName: string,
  concurrencyLimit: number | null,
  orderableName: string,
  queueType: TaskQueueType,
  worker: BackgroundWorker,
  prisma: PrismaClientOrTransaction,
  attempt: number = 0
): Promise<TaskQueue> {
  if (attempt > 3) {
    throw new Error("Failed to insert queue record");
  }

  try {
    let taskQueue = await prisma.taskQueue.findFirst({
      where: {
        runtimeEnvironmentId: worker.runtimeEnvironmentId,
        name: queueName,
      },
    });

    if (!taskQueue) {
      taskQueue = await prisma.taskQueue.create({
        data: {
          friendlyId: generateFriendlyId("queue"),
          version: "V2",
          name: queueName,
          orderableName,
          concurrencyLimit,
          runtimeEnvironmentId: worker.runtimeEnvironmentId,
          projectId: worker.projectId,
          type: queueType,
          workers: {
            connect: {
              id: worker.id,
            },
          },
        },
      });
    } else {
      const hasOverride = taskQueue.concurrencyLimitOverriddenAt !== null;

      taskQueue = await prisma.taskQueue.update({
        where: {
          id: taskQueue.id,
        },
        data: {
          workers: { connect: { id: worker.id } },
          version: "V2",
          orderableName,
          // If overridden, keep current limit and update base; otherwise update limit normally
          concurrencyLimit: hasOverride ? undefined : concurrencyLimit,
          concurrencyLimitBase: hasOverride ? concurrencyLimit : undefined,
        },
      });
    }

    return taskQueue;
  } catch (error) {
    // If the queue already exists, let's try again
    if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
      return await upsertWorkerQueueRecord(
        queueName,
        concurrencyLimit,
        orderableName,
        queueType,
        worker,
        prisma,
        attempt + 1
      );
    }
    throw error;
  }
}
//CreateDeclarativeScheduleError with a message
export class CreateDeclarativeScheduleError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "CreateDeclarativeScheduleError";
  }
}

export async function syncDeclarativeSchedules(
  tasks: TaskResource[],
  worker: BackgroundWorker,
  environment: AuthenticatedEnvironment,
  prisma: PrismaClientOrTransaction
) {
  const tasksWithDeclarativeSchedules = tasks.filter((task) => task.schedule);
  logger.info("Syncing declarative schedules", {
    tasksWithDeclarativeSchedules,
    environment,
  });

  const existingDeclarativeSchedules = await prisma.taskSchedule.findMany({
    where: {
      type: "DECLARATIVE",
      projectId: environment.projectId,
    },
    include: {
      instances: true,
    },
  });

  const checkSchedule = new CheckScheduleService(prisma);

  //start out by assuming they're all missing
  const missingSchedules = new Set<string>(
    existingDeclarativeSchedules.map((schedule) => schedule.id)
  );

  //create/update schedules (+ instances)
  for (const task of tasksWithDeclarativeSchedules) {
    if (task.schedule === undefined) continue;

    // Check if this schedule should be created in the current environment
    if (task.schedule.environments && task.schedule.environments.length > 0) {
      if (!task.schedule.environments.includes(environment.type)) {
        logger.debug("Skipping schedule creation due to environment filter", {
          taskId: task.id,
          environmentType: environment.type,
          allowedEnvironments: task.schedule.environments,
        });
        continue;
      }
    }

    const existingSchedule = existingDeclarativeSchedules.find(
      (schedule) =>
        schedule.taskIdentifier === task.id &&
        schedule.instances.some((instance) => instance.environmentId === environment.id)
    );

    //this throws errors if the schedule is invalid
    await checkSchedule.call(
      environment.projectId,
      {
        cron: task.schedule.cron,
        timezone: task.schedule.timezone,
        taskIdentifier: task.id,
        friendlyId: existingSchedule?.friendlyId,
      },
      [environment.id]
    );

    if (existingSchedule) {
      const schedule = await prisma.taskSchedule.update({
        where: {
          id: existingSchedule.id,
        },
        data: {
          generatorExpression: task.schedule.cron,
          generatorDescription: cronstrue.toString(task.schedule.cron),
          timezone: task.schedule.timezone,
        },
        include: {
          instances: true,
        },
      });

      missingSchedules.delete(existingSchedule.id);
      const instance = schedule.instances.at(0);
      if (instance) {
        await scheduleEngine.registerNextTaskScheduleInstance({ instanceId: instance.id });
      } else {
        throw new CreateDeclarativeScheduleError(
          `Missing instance for declarative schedule ${schedule.id}`
        );
      }
    } else {
      const newSchedule = await prisma.taskSchedule.create({
        data: {
          friendlyId: generateFriendlyId("sched"),
          projectId: environment.projectId,
          taskIdentifier: task.id,
          generatorExpression: task.schedule.cron,
          generatorDescription: cronstrue.toString(task.schedule.cron),
          timezone: task.schedule.timezone,
          type: "DECLARATIVE",
          instances: {
            create: [
              {
                environmentId: environment.id,
                projectId: environment.projectId,
              },
            ],
          },
        },
        include: {
          instances: true,
        },
      });

      const instance = newSchedule.instances.at(0);

      if (instance) {
        await scheduleEngine.registerNextTaskScheduleInstance({ instanceId: instance.id });
      } else {
        throw new CreateDeclarativeScheduleError(
          `Missing instance for declarative schedule ${newSchedule.id}`
        );
      }
    }
  }

  //Delete instances for this environment
  //Delete schedules that have no instances left
  const potentiallyDeletableSchedules = await prisma.taskSchedule.findMany({
    where: {
      id: {
        in: Array.from(missingSchedules),
      },
    },
    include: {
      instances: true,
    },
  });

  for (const schedule of potentiallyDeletableSchedules) {
    const canDeleteSchedule =
      schedule.instances.length === 0 ||
      schedule.instances.every((instance) => instance.environmentId === environment.id);

    if (canDeleteSchedule) {
      //we can delete schedules with no instances other than ones for the current environment
      await prisma.taskSchedule.delete({
        where: {
          id: schedule.id,
        },
      });
    } else {
      //otherwise we delete the instance (other environments remain untouched)
      await prisma.taskScheduleInstance.deleteMany({
        where: {
          taskScheduleId: schedule.id,
          environmentId: environment.id,
        },
      });
    }
  }
}

export async function createBackgroundFiles(
  files: Array<BackgroundWorkerSourceFileMetadata> | undefined,
  worker: BackgroundWorker,
  environment: AuthenticatedEnvironment,
  prisma: PrismaClientOrTransaction
) {
  // Maps from each taskId to the backgroundWorkerFileId
  const results = new Map<string, string>();

  if (!files) {
    return results;
  }

  for (const file of files) {
    const backgroundWorkerFile = await prisma.backgroundWorkerFile.upsert({
      where: {
        projectId_contentHash: {
          projectId: environment.projectId,
          contentHash: file.contentHash,
        },
      },
      create: {
        friendlyId: generateFriendlyId("file"),
        projectId: environment.projectId,
        contentHash: file.contentHash,
        filePath: file.filePath,
        contents: Buffer.from(file.contents),
        backgroundWorkers: {
          connect: {
            id: worker.id,
          },
        },
      },
      update: {
        backgroundWorkers: {
          connect: {
            id: worker.id,
          },
        },
      },
    });

    for (const taskId of file.taskIds) {
      results.set(taskId, backgroundWorkerFile.id);
    }
  }

  return results;
}

import { createHash } from "crypto";

function hashContent(content: string): string {
  return createHash("sha256").update(content).digest("hex").slice(0, 16);
}

async function createWorkerPrompts(
  prompts: PromptResource[],
  worker: BackgroundWorker,
  environment: AuthenticatedEnvironment,
  prisma: PrismaClientOrTransaction
) {
  for (const promptResource of prompts) {
    try {
      // Upsert the Prompt record (identity + schema)
      const prompt = await prisma.prompt.upsert({
        where: {
          projectId_runtimeEnvironmentId_slug: {
            projectId: worker.projectId,
            runtimeEnvironmentId: environment.id,
            slug: promptResource.id,
          },
        },
        create: {
          friendlyId: generateFriendlyId("prompt"),
          organizationId: environment.organizationId,
          projectId: worker.projectId,
          runtimeEnvironmentId: environment.id,
          slug: promptResource.id,
          description: promptResource.description,
          filePath: promptResource.filePath,
          exportName: promptResource.exportName,
          variableSchema: promptResource.variableSchema as any,
          defaultModel: promptResource.model,
          defaultConfig: promptResource.config as any,
        },
        update: {
          description: promptResource.description,
          filePath: promptResource.filePath,
          exportName: promptResource.exportName,
          variableSchema: promptResource.variableSchema as any,
          defaultModel: promptResource.model,
          defaultConfig: promptResource.config as any,
        },
      });

      // Compute the version-definition hash for dedup. Includes the model and
      // config, not just the prompt text, so changing a code prompt's model or
      // config creates a new version — otherwise a model-only change is silently
      // skipped and the old model keeps serving.
      const contentString = promptResource.content ?? "";
      const contentHash = hashContent(
        JSON.stringify({
          content: contentString,
          model: promptResource.model ?? null,
          config: promptResource.config ?? null,
        })
      );

      // Find the latest version overall (for version numbering) and the latest
      // code-sourced version (for content dedup). We compare against the latest
      // code version specifically so that dashboard edits don't interfere with
      // dedup — if the code hasn't changed since the last deploy, we skip even
      // if a dashboard edit happened in between.
      const latestVersion = await prisma.promptVersion.findFirst({
        where: { promptId: prompt.id },
        orderBy: { version: "desc" },
      });

      const latestCodeVersion = await prisma.promptVersion.findFirst({
        where: { promptId: prompt.id, source: "code" },
        orderBy: { version: "desc" },
      });

      if (latestCodeVersion?.contentHash === contentHash) {
        // Code definition (text + model + config) unchanged since last deploy —
        // skip creating a new version.
        continue;
      }

      const nextVersion = (latestVersion?.version ?? 0) + 1;

      // Determine labels for the new version.
      // Deploys always move "current" to the new code version. If a dashboard
      // override exists, it sits on top via the "override" label and the API
      // serves that instead — so "current" movement is safe.
      const labels = ["latest", "current"];

      // Wrap label removal + version creation in a transaction so labels
      // aren't stripped if the create fails (e.g. concurrent deploy race).
      await $transaction(prisma, async (tx) => {
        // Remove "latest" label from all existing versions
        if (latestVersion) {
          await tx.$executeRaw`
            UPDATE "prompt_versions"
            SET "labels" = array_remove("labels", 'latest')
            WHERE "promptId" = ${prompt.id} AND 'latest' = ANY("labels")
          `;
        }

        // Remove "current" from any existing version
        await tx.$executeRaw`
          UPDATE "prompt_versions"
          SET "labels" = array_remove("labels", 'current')
          WHERE "promptId" = ${prompt.id} AND 'current' = ANY("labels")
        `;

        await tx.promptVersion.create({
          data: {
            promptId: prompt.id,
            version: nextVersion,
            textContent: contentString,
            model: promptResource.model,
            config: promptResource.config as any,
            source: "code",
            contentHash,
            labels,
            workerId: worker.id,
          },
        });
      });

      logger.debug("Registered prompt version", {
        promptSlug: promptResource.id,
        version: nextVersion,
        labels,
      });
    } catch (error) {
      if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
        logger.warn("Prompt version already exists", { prompt: promptResource.id });
      } else {
        logger.error("Error creating prompt version", {
          error: error instanceof Error ? error.message : String(error),
          prompt: promptResource.id,
        });
      }
    }
  }
}
