import { z } from "zod";
import { DeserializedJsonSchema } from "../../schemas/json.js";
import {
  FlushedRunMetadata,
  GitMeta,
  MachinePresetName,
  SerializedError,
  TaskRunError,
} from "./common.js";
import { BackgroundWorkerMetadata } from "./resources.js";
import { DequeuedMessage, MachineResources } from "./runEngine.js";

export const RunEngineVersion = z.union([z.literal("V1"), z.literal("V2")]);

export const WhoAmIResponseSchema = z.object({
  userId: z.string(),
  email: z.string().email(),
  dashboardUrl: z.string(),
  project: z
    .object({
      name: z.string(),
      url: z.string(),
      orgTitle: z.string(),
    })
    .optional(),
});

export type WhoAmIResponse = z.infer<typeof WhoAmIResponseSchema>;

export const GetProjectResponseBody = z.object({
  id: z.string(),
  externalRef: z
    .string()
    .describe(
      "The external reference for the project, also known as the project ref, a unique identifier starting with proj_"
    ),
  name: z.string(),
  slug: z.string(),
  createdAt: z.coerce.date(),
  organization: z.object({
    id: z.string(),
    title: z.string(),
    slug: z.string(),
    createdAt: z.coerce.date(),
  }),
});

export type GetProjectResponseBody = z.infer<typeof GetProjectResponseBody>;

export const GetProjectsResponseBody = z.array(GetProjectResponseBody);

export type GetProjectsResponseBody = z.infer<typeof GetProjectsResponseBody>;

export const GetOrgsResponseBody = z.array(
  z.object({
    id: z.string(),
    title: z.string(),
    slug: z.string(),
    createdAt: z.coerce.date(),
  })
);

export type GetOrgsResponseBody = z.infer<typeof GetOrgsResponseBody>;

export const CreateProjectRequestBody = z.object({
  name: z
    .string()
    .trim()
    .min(1, "Name is required")
    .max(255, "Name must be less than 255 characters"),
});

export type CreateProjectRequestBody = z.infer<typeof CreateProjectRequestBody>;

export const GetProjectEnvResponse = z.object({
  apiKey: z.string(),
  name: z.string(),
  apiUrl: z.string(),
  projectId: z.string(),
});

export type GetProjectEnvResponse = z.infer<typeof GetProjectEnvResponse>;

export const ProjectEnvironment = z.object({
  id: z.string(),
  /// The slug used as the environment identifier in env var endpoints (e.g. "dev", "stg", "prod", "preview")
  slug: z.string(),
  type: z.enum(["DEVELOPMENT", "STAGING", "PREVIEW", "PRODUCTION"]),
  /// Whether this is the branchable parent (preview); individual branches are not returned
  isBranchableEnvironment: z.boolean(),
  branchName: z.string().nullable(),
  paused: z.boolean(),
});

export type ProjectEnvironment = z.infer<typeof ProjectEnvironment>;

export const GetProjectEnvironmentsResponseBody = z.array(ProjectEnvironment);

export type GetProjectEnvironmentsResponseBody = z.infer<typeof GetProjectEnvironmentsResponseBody>;

// Zod schema for the response body type
export const GetWorkerTaskResponse = z.object({
  id: z.string(),
  slug: z.string(),
  filePath: z.string(),
  triggerSource: z.string(),
  createdAt: z.coerce.date(),
  payloadSchema: z.any().nullish(),
});

export const GetWorkerByTagResponse = z.object({
  worker: z.object({
    id: z.string(),
    version: z.string(),
    engine: z.string().nullish(),
    sdkVersion: z.string().nullish(),
    cliVersion: z.string().nullish(),
    tasks: z.array(GetWorkerTaskResponse),
  }),
  urls: z.object({
    runs: z.string(),
  }),
});

export type GetWorkerByTagResponse = z.infer<typeof GetWorkerByTagResponse>;

export const GetJWTRequestBody = z.object({
  claims: z
    .object({
      scopes: z.array(z.string()).default([]),
    })
    .optional(),
  expirationTime: z.union([z.number(), z.string()]).optional(),
});

export type GetJWTRequestBody = z.infer<typeof GetJWTRequestBody>;

export const GetJWTResponse = z.object({
  token: z.string(),
});

export type GetJWTResponse = z.infer<typeof GetJWTResponse>;

export const CreateBackgroundWorkerRequestBody = z.object({
  localOnly: z.boolean(),
  metadata: BackgroundWorkerMetadata,
  engine: RunEngineVersion.optional(),
  supportsLazyAttempts: z.boolean().optional(),
  buildPlatform: z.string().optional(),
  targetPlatform: z.string().optional(),
});

export type CreateBackgroundWorkerRequestBody = z.infer<typeof CreateBackgroundWorkerRequestBody>;

export const CreateBackgroundWorkerResponse = z.object({
  id: z.string(),
  version: z.string(),
  contentHash: z.string(),
});

export type CreateBackgroundWorkerResponse = z.infer<typeof CreateBackgroundWorkerResponse>;

//an array of 1, 2, or 3 strings
const RunTag = z.string().max(128, "Tags must be less than 128 characters");
export const RunTags = z.union([RunTag, RunTag.array()]);

export type RunTags = z.infer<typeof RunTags>;

/** Stores the original user-provided idempotency key and scope */
export const IdempotencyKeyOptionsSchema = z.object({
  key: z.string(),
  scope: z.enum(["run", "attempt", "global"]),
});

export type IdempotencyKeyOptionsSchema = z.infer<typeof IdempotencyKeyOptionsSchema>;

// Coerces user-supplied concurrencyKey values to string. The downstream Prisma
// column is String?, so passing a number (a common foot-gun when callers do
// `concurrencyKey: payload.userId`) used to fail at `prisma.taskRun.create`
// with PrismaClientValidationError. Accept the intent and stringify here.
const ConcurrencyKeySchema = z.union([z.string(), z.number()]).transform((value) => String(value));

export const TriggerTaskRequestBody = z
  .object({
    payload: z.any(),
    context: z.any(),
    options: z
      .object({
        /** @deprecated engine v1 only */
        dependentAttempt: z.string().optional(),
        /** @deprecated engine v1 only */
        parentAttempt: z.string().optional(),
        /** @deprecated engine v1 only */
        dependentBatch: z.string().optional(),
        /**
         * If triggered in a batch, this is the BatchTaskRun id
         */
        parentBatch: z.string().optional(),
        /**
         * RunEngine v2
         * If triggered inside another run, the parentRunId is the friendly ID of the parent run.
         */
        parentRunId: z.string().optional(),
        /**
         * RunEngine v2
         * Should be `true` if `triggerAndWait` or `batchTriggerAndWait`
         */
        resumeParentOnCompletion: z.boolean().optional(),
        /**
         * Locks the version to the passed value.
         * Automatically set when using `triggerAndWait` or `batchTriggerAndWait`
         */
        lockToVersion: z.string().optional(),

        queue: z
          .object({
            name: z.string(),
            // @deprecated, this is now specified on the queue
            concurrencyLimit: z.number().int().optional(),
          })
          .optional(),
        concurrencyKey: ConcurrencyKeySchema.optional(),
        delay: z.string().or(z.coerce.date()).optional(),
        idempotencyKey: z
          .string()
          // Caps user-supplied keys before they reach the unique idempotency index
          // on the underlying table — values past this fail at the database layer
          // rather than returning a clean 400.
          .max(2048, "idempotencyKey must be 2048 characters or less")
          .optional(),
        idempotencyKeyTTL: z.string().optional(),
        /** The original user-provided idempotency key and scope */
        idempotencyKeyOptions: IdempotencyKeyOptionsSchema.optional(),
        machine: MachinePresetName.optional(),
        maxAttempts: z.number().int().optional(),
        maxDuration: z.number().optional(),
        metadata: z.any(),
        metadataType: z.string().optional(),
        payloadType: z.string().optional(),
        tags: RunTags.optional(),
        test: z.boolean().optional(),
        ttl: z.string().or(z.number().nonnegative().int()).optional(),
        priority: z.number().optional(),
        bulkActionId: z.string().optional(),
        region: z.string().optional(),
        debounce: z
          .object({
            key: z.string().max(512),
            delay: z.string(),
            mode: z.enum(["leading", "trailing"]).optional(),
            maxDelay: z.string().optional(),
          })
          .optional(),
      })
      .optional(),
  })
  .superRefine((value, ctx) => {
    if (value.options?.payloadType !== "application/store") {
      return;
    }

    if (typeof value.payload !== "string" || value.payload.length === 0) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "payload must be a non-empty string when options.payloadType is application/store",
        path: ["payload"],
      });
    }
  });

export type TriggerTaskRequestBody = z.infer<typeof TriggerTaskRequestBody>;

export const TriggerTaskResponse = z.object({
  id: z.string(),
  isCached: z.boolean().optional(),
});

export type TriggerTaskResponse = z.infer<typeof TriggerTaskResponse>;

export const BatchTriggerTaskRequestBody = z.object({
  items: TriggerTaskRequestBody.array(),
  dependentAttempt: z.string().optional(),
});

export type BatchTriggerTaskRequestBody = z.infer<typeof BatchTriggerTaskRequestBody>;

export const BatchTriggerTaskItem = z.object({
  task: z.string(),
  payload: z.any(),
  context: z.any(),
  options: z
    .object({
      concurrencyKey: ConcurrencyKeySchema.optional(),
      delay: z.string().or(z.coerce.date()).optional(),
      idempotencyKey: z
        .string()
        // Caps user-supplied keys before they reach the unique idempotency index
        // on the underlying table — values past this fail at the database layer
        // rather than returning a clean 400.
        .max(2048, "idempotencyKey must be 2048 characters or less")
        .optional(),
      idempotencyKeyTTL: z.string().optional(),
      /** The original user-provided idempotency key and scope */
      idempotencyKeyOptions: IdempotencyKeyOptionsSchema.optional(),
      lockToVersion: z.string().optional(),
      machine: MachinePresetName.optional(),
      maxAttempts: z.number().int().optional(),
      maxDuration: z.number().optional(),
      metadata: z.any(),
      metadataType: z.string().optional(),
      parentAttempt: z.string().optional(),
      payloadType: z.string().optional(),
      queue: z
        .object({
          name: z.string(),
        })
        .optional(),
      tags: RunTags.optional(),
      test: z.boolean().optional(),
      ttl: z.string().or(z.number().nonnegative().int()).optional(),
      priority: z.number().optional(),
      region: z.string().optional(),
      debounce: z
        .object({
          key: z.string().max(512),
          delay: z.string(),
          mode: z.enum(["leading", "trailing"]).optional(),
          maxDelay: z.string().optional(),
        })
        .optional(),
    })
    .optional(),
});

export type BatchTriggerTaskItem = z.infer<typeof BatchTriggerTaskItem>;

export const BatchTriggerTaskV2RequestBody = z.object({
  items: BatchTriggerTaskItem.array(),
  /** @deprecated engine v1 only */
  dependentAttempt: z.string().optional(),
  /**
   * RunEngine v2
   * If triggered inside another run, the parentRunId is the friendly ID of the parent run.
   */
  parentRunId: z.string().optional(),
  /**
   * RunEngine v2
   * Should be `true` if `triggerAndWait` or `batchTriggerAndWait`
   */
  resumeParentOnCompletion: z.boolean().optional(),
});

export type BatchTriggerTaskV2RequestBody = z.infer<typeof BatchTriggerTaskV2RequestBody>;

export const BatchTriggerTaskV2Response = z.object({
  id: z.string(),
  isCached: z.boolean(),
  idempotencyKey: z.string().optional(),
  runs: z.array(
    z.object({
      id: z.string(),
      taskIdentifier: z.string(),
      isCached: z.boolean(),
      idempotencyKey: z.string().optional(),
    })
  ),
});

export type BatchTriggerTaskV2Response = z.infer<typeof BatchTriggerTaskV2Response>;

export const BatchTriggerTaskV3RequestBody = z.object({
  items: BatchTriggerTaskItem.array(),
  /**
   * RunEngine v2
   * If triggered inside another run, the parentRunId is the friendly ID of the parent run.
   */
  parentRunId: z.string().optional(),
  /**
   * RunEngine v2
   * Should be `true` if `triggerAndWait` or `batchTriggerAndWait`
   */
  resumeParentOnCompletion: z.boolean().optional(),
});

export type BatchTriggerTaskV3RequestBody = z.infer<typeof BatchTriggerTaskV3RequestBody>;

export const BatchTriggerTaskV3Response = z.object({
  id: z.string(),
  runCount: z.number(),
});

export type BatchTriggerTaskV3Response = z.infer<typeof BatchTriggerTaskV3Response>;

// ============================================================================
// 2-Phase Batch API (v3) - Streaming NDJSON Support
// ============================================================================

/**
 * Phase 1: Create batch request body
 * Creates the batch record and optionally blocks parent run for batchTriggerAndWait
 */
export const CreateBatchRequestBody = z.object({
  /** Expected number of items in the batch */
  runCount: z.number().int().positive(),
  /** Parent run ID for batchTriggerAndWait (friendly ID) */
  parentRunId: z.string().optional(),
  /** Whether to resume parent on completion (true for batchTriggerAndWait) */
  resumeParentOnCompletion: z.boolean().optional(),
  /** Idempotency key for the batch */
  idempotencyKey: z
    .string()
    // Caps user-supplied keys before they reach the unique idempotency index
    // on the underlying table — values past this fail at the database layer
    // rather than returning a clean 400.
    .max(2048, "idempotencyKey must be 2048 characters or less")
    .optional(),
  /** The original user-provided idempotency key and scope */
  idempotencyKeyOptions: IdempotencyKeyOptionsSchema.optional(),
});

export type CreateBatchRequestBody = z.infer<typeof CreateBatchRequestBody>;

/**
 * Phase 1: Create batch response
 */
export const CreateBatchResponse = z.object({
  /** The batch ID (friendly ID) */
  id: z.string(),
  /** The expected run count */
  runCount: z.number(),
  /** Whether this response came from a cached/idempotent batch */
  isCached: z.boolean(),
  /** The idempotency key if provided */
  idempotencyKey: z.string().optional(),
});

export type CreateBatchResponse = z.infer<typeof CreateBatchResponse>;

/**
 * Phase 2: Individual item in the NDJSON stream
 * Each line in the NDJSON body should match this schema.
 *
 * `options` reuses the strict shape from BatchTriggerTaskItem so that the
 * Phase-2 streaming path validates option fields identically to the V2/V3
 * batch trigger endpoints — historically this used z.record(z.unknown()) and
 * let invalid values (e.g. numeric concurrencyKey) reach Prisma.
 */
export const BatchItemNDJSON = z.object({
  /** Zero-based index of this item (used for idempotency and ordering) */
  index: z.number().int().nonnegative(),
  /** The task identifier to trigger */
  task: z.string(),
  /** The payload for this task run */
  payload: z.unknown().optional(),
  /** Options for this specific item */
  options: BatchTriggerTaskItem.shape.options,
});

export type BatchItemNDJSON = z.infer<typeof BatchItemNDJSON>;

/**
 * Phase 2: Stream items response
 * Returned after the NDJSON stream completes
 */
export const StreamBatchItemsResponse = z.object({
  /** The batch ID */
  id: z.string(),
  /** Number of items successfully accepted */
  itemsAccepted: z.number(),
  /** Number of items that were deduplicated (already enqueued) */
  itemsDeduplicated: z.number(),
  /** Whether the batch was sealed and is ready for processing.
   * If false, the batch needs more items before processing can start.
   * Clients should check this field and retry with missing items if needed. */
  sealed: z.boolean(),
  /** Total items currently enqueued (only present when sealed=false to help with retries) */
  enqueuedCount: z.number().optional(),
  /** Expected total item count (only present when sealed=false to help with retries) */
  expectedCount: z.number().optional(),
});

export type StreamBatchItemsResponse = z.infer<typeof StreamBatchItemsResponse>;

export const BatchTriggerTaskResponse = z.object({
  batchId: z.string(),
  runs: z.string().array(),
});

export type BatchTriggerTaskResponse = z.infer<typeof BatchTriggerTaskResponse>;

export const GetBatchResponseBody = z.object({
  id: z.string(),
  items: z.array(
    z.object({
      id: z.string(),
      taskRunId: z.string(),
      status: z.enum(["PENDING", "CANCELED", "COMPLETED", "FAILED"]),
    })
  ),
});

export type GetBatchResponseBody = z.infer<typeof GetBatchResponseBody>;

export const AddTagsRequestBody = z.object({
  tags: RunTags,
});

export type AddTagsRequestBody = z.infer<typeof AddTagsRequestBody>;

export const RescheduleRunRequestBody = z.object({
  delay: z.string().or(z.coerce.date()),
});

export type RescheduleRunRequestBody = z.infer<typeof RescheduleRunRequestBody>;

export const GetEnvironmentVariablesResponseBody = z.object({
  variables: z.record(z.string()),
});

export type GetEnvironmentVariablesResponseBody = z.infer<
  typeof GetEnvironmentVariablesResponseBody
>;

export const StartDeploymentIndexingRequestBody = z.object({
  imageReference: z.string(),
  selfHosted: z.boolean().optional(),
});

export type StartDeploymentIndexingRequestBody = z.infer<typeof StartDeploymentIndexingRequestBody>;

export const StartDeploymentIndexingResponseBody = z.object({
  id: z.string(),
  contentHash: z.string(),
});

export type StartDeploymentIndexingResponseBody = z.infer<
  typeof StartDeploymentIndexingResponseBody
>;

export const FinalizeDeploymentRequestBody = z.object({
  skipPromotion: z.boolean().optional(),
  imageDigest: z.string().optional(),
  skipPushToRegistry: z.boolean().optional(),
});

export type FinalizeDeploymentRequestBody = z.infer<typeof FinalizeDeploymentRequestBody>;

export const BuildServerMetadata = z.object({
  buildId: z.string().optional(),
  isNativeBuild: z.boolean().optional(),
  artifactKey: z.string().optional(),
  skipPromotion: z.boolean().optional(),
  configFilePath: z.string().optional(),
  skipEnqueue: z.boolean().optional(),
});

export type BuildServerMetadata = z.infer<typeof BuildServerMetadata>;

export const ProgressDeploymentRequestBody = z.object({
  contentHash: z.string().optional(),
  gitMeta: GitMeta.optional(),
  runtime: z.string().optional(),
  buildServerMetadata: BuildServerMetadata.optional(),
});

export type ProgressDeploymentRequestBody = z.infer<typeof ProgressDeploymentRequestBody>;

export const CancelDeploymentRequestBody = z.object({
  reason: z.string().max(200, "Reason must be less than 200 characters").optional(),
});

export type CancelDeploymentRequestBody = z.infer<typeof CancelDeploymentRequestBody>;

export const ExternalBuildData = z.object({
  buildId: z.string(),
  buildToken: z.string(),
  projectId: z.string(),
});

export type ExternalBuildData = z.infer<typeof ExternalBuildData>;

const anyString = z.custom<string & {}>((v) => typeof v === "string");

export const DeploymentTriggeredVia = z
  .enum([
    "cli:manual",
    "cli:ci_other",
    "cli:github_actions",
    "cli:gitlab_ci",
    "cli:circleci",
    "cli:jenkins",
    "cli:azure_pipelines",
    "cli:bitbucket_pipelines",
    "cli:travis_ci",
    "cli:buildkite",
    "git_integration:github",
    "dashboard",
  ])
  .or(anyString);

export type DeploymentTriggeredVia = z.infer<typeof DeploymentTriggeredVia>;

// TriggerSource, TriggerAction, and RunAnnotations are defined in runEngine.ts
// They are re-exported through the schemas barrel (index.ts)

export const UpsertBranchRequestBody = z.object({
  git: GitMeta.optional(),
  env: z.enum(["preview", "development"]),
  branch: z.string(),
});

export type UpsertBranchRequestBody = z.infer<typeof UpsertBranchRequestBody>;

export const UpsertBranchResponseBody = z.object({
  id: z.string(),
});

export type UpsertBranchResponseBody = z.infer<typeof UpsertBranchResponseBody>;

export const CreateArtifactRequestBody = z.object({
  type: z.enum(["deployment_context"]).default("deployment_context"),
  contentType: z.string().default("application/gzip"),
  contentLength: z.number().optional(),
});

export type CreateArtifactRequestBody = z.infer<typeof CreateArtifactRequestBody>;

export const CreateArtifactResponseBody = z.object({
  artifactKey: z.string(),
  uploadUrl: z.string(),
  uploadFields: z.record(z.string()),
  expiresAt: z.string().datetime(),
});

export type CreateArtifactResponseBody = z.infer<typeof CreateArtifactResponseBody>;

export const InitializeDeploymentResponseBody = z.object({
  id: z.string(),
  contentHash: z.string(),
  shortCode: z.string(),
  version: z.string(),
  imageTag: z.string(),
  imagePlatform: z.string(),
  externalBuildData: ExternalBuildData.optional().nullable(),
  eventStream: z
    .object({
      s2: z.object({
        basin: z.string(),
        stream: z.string(),
        accessToken: z.string(),
      }),
    })
    .optional(),
});

export type InitializeDeploymentResponseBody = z.infer<typeof InitializeDeploymentResponseBody>;

const InitializeDeploymentRequestBodyBase = z.object({
  contentHash: z.string(),
  userId: z.string().optional(),
  /** @deprecated This is now determined by the webapp. This is only used to warn users with old CLI versions. */
  selfHosted: z.boolean().optional(),
  gitMeta: GitMeta.optional(),
  type: z.enum(["MANAGED", "UNMANAGED", "V1"]).optional(),
  runtime: z.string().optional(),
  initialStatus: z.enum(["PENDING", "BUILDING"]).optional(),
  isLocalBuild: z.boolean().optional(),
  triggeredVia: DeploymentTriggeredVia.optional(),
  buildId: z.string().optional(),
});
type BaseOutput = z.output<typeof InitializeDeploymentRequestBodyBase>;

type NativeBuildOutput = BaseOutput & {
  isNativeBuild: true;
  skipPromotion?: boolean;
  artifactKey?: string;
  configFilePath?: string;
  skipEnqueue?: boolean;
};

type NonNativeBuildOutput = BaseOutput & {
  isNativeBuild: false;
  skipPromotion?: never;
  artifactKey?: never;
  configFilePath?: never;
  skipEnqueue?: never;
};

const InitializeDeploymentRequestBodyFull = InitializeDeploymentRequestBodyBase.extend({
  isNativeBuild: z.boolean().default(false),
  skipPromotion: z.boolean().optional(),
  artifactKey: z.string().optional(),
  configFilePath: z.string().optional(),
  skipEnqueue: z.boolean().optional().default(false),
});

export const InitializeDeploymentRequestBody = InitializeDeploymentRequestBodyFull.transform(
  (data): NativeBuildOutput | NonNativeBuildOutput => {
    if (data.isNativeBuild) {
      return { ...data, isNativeBuild: true as const };
    }
    const { skipPromotion, artifactKey, configFilePath, skipEnqueue, ...rest } = data;
    return { ...rest, isNativeBuild: false as const };
  }
);

export type InitializeDeploymentRequestBody = z.infer<typeof InitializeDeploymentRequestBody>;

export const RemoteBuildProviderStatusResponseBody = z.object({
  status: z.enum(["operational", "degraded", "unknown"]),
  message: z.string(),
});

export type RemoteBuildProviderStatusResponseBody = z.infer<
  typeof RemoteBuildProviderStatusResponseBody
>;

export const GenerateRegistryCredentialsResponseBody = z.object({
  username: z.string(),
  password: z.string(),
  expiresAt: z.string(),
  repositoryUri: z.string(),
});

export type GenerateRegistryCredentialsResponseBody = z.infer<
  typeof GenerateRegistryCredentialsResponseBody
>;

export const DeploymentErrorData = z.object({
  name: z.string(),
  message: z.string(),
  stack: z.string().optional(),
  stderr: z.string().optional(),
});

export type DeploymentErrorData = z.infer<typeof DeploymentErrorData>;

export const FailDeploymentRequestBody = z.object({
  error: DeploymentErrorData,
});

export type FailDeploymentRequestBody = z.infer<typeof FailDeploymentRequestBody>;

export const FailDeploymentResponseBody = z.object({
  id: z.string(),
});

export type FailDeploymentResponseBody = z.infer<typeof FailDeploymentResponseBody>;

export const PromoteDeploymentResponseBody = z.object({
  id: z.string(),
  version: z.string(),
  shortCode: z.string(),
});

export type PromoteDeploymentResponseBody = z.infer<typeof PromoteDeploymentResponseBody>;

export const GetDeploymentResponseBody = z.object({
  id: z.string(),
  status: z.enum([
    "PENDING",
    "INSTALLING",
    "BUILDING",
    "DEPLOYING",
    "DEPLOYED",
    "FAILED",
    "CANCELED",
    "TIMED_OUT",
  ]),
  contentHash: z.string(),
  shortCode: z.string(),
  version: z.string(),
  imageReference: z.string().nullish(),
  imagePlatform: z.string(),
  commitSHA: z.string().nullish(),
  externalBuildData: ExternalBuildData.optional().nullable(),
  errorData: DeploymentErrorData.nullish(),
  worker: z
    .object({
      id: z.string(),
      version: z.string(),
      tasks: z.array(
        z.object({
          id: z.string(),
          slug: z.string(),
          filePath: z.string(),
          exportName: z.string().optional(),
        })
      ),
    })
    .optional(),
  integrationDeployments: z
    .array(
      z.object({
        id: z.string(),
        integrationName: z.string(),
        integrationDeploymentId: z.string(),
        commitSHA: z.string(),
        createdAt: z.coerce.date(),
      })
    )
    .nullish(),
});

export type GetDeploymentResponseBody = z.infer<typeof GetDeploymentResponseBody>;

export const GetLatestDeploymentResponseBody = GetDeploymentResponseBody.omit({
  worker: true,
});
export type GetLatestDeploymentResponseBody = z.infer<typeof GetLatestDeploymentResponseBody>;

export const DeploymentLogEvent = z.object({
  type: z.literal("log"),
  data: z.object({
    level: z.enum(["debug", "info", "warn", "error"]).optional().default("info"),
    message: z.string(),
  }),
});

export const DeploymentFinalizedEvent = z.object({
  type: z.literal("finalized"),
  data: z.object({
    result: z.enum(["succeeded", "failed", "timed_out", "canceled"]).or(anyString),
    message: z.string().optional(),
  }),
});

export const DeploymentEvent = z.discriminatedUnion("type", [
  DeploymentLogEvent,
  DeploymentFinalizedEvent,
]);

export type DeploymentEvent = z.infer<typeof DeploymentEvent>;
export type DeploymentLogEvent = z.infer<typeof DeploymentLogEvent>;
export type DeploymentFinalizedEvent = z.infer<typeof DeploymentFinalizedEvent>;

export const DeploymentEventFromString = z
  .string()
  .transform((s, ctx) => {
    try {
      return JSON.parse(s);
    } catch {
      ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Invalid JSON" });
      return z.NEVER;
    }
  })
  .pipe(DeploymentEvent);

export const CreateUploadPayloadUrlResponseBody = z.object({
  presignedUrl: z.string(),
  /** Present on `/api/v2/packets` PUT (upload handshake); omitted on v1 GET download presign. */
  storagePath: z.string().optional(),
});

export const WorkersListResponseBody = z
  .object({
    type: z.string(),
    name: z.string(),
    description: z.string().nullish(),
    latestVersion: z.string().nullish(),
    lastHeartbeatAt: z.string().nullish(),
    isDefault: z.boolean(),
    updatedAt: z.coerce.date(),
  })
  .array();
export type WorkersListResponseBody = z.infer<typeof WorkersListResponseBody>;

export const WorkersCreateRequestBody = z.object({
  name: z.string().optional(),
  description: z.string().optional(),
});
export type WorkersCreateRequestBody = z.infer<typeof WorkersCreateRequestBody>;

export const WorkersCreateResponseBody = z.object({
  workerGroup: z.object({
    name: z.string(),
    description: z.string().nullish(),
  }),
  token: z.object({
    plaintext: z.string(),
  }),
});
export type WorkersCreateResponseBody = z.infer<typeof WorkersCreateResponseBody>;

export const DevConfigResponseBody = z.object({
  environmentId: z.string(),
  dequeueIntervalWithRun: z.number(),
  dequeueIntervalWithoutRun: z.number(),
  maxConcurrentRuns: z.number(),
  engineUrl: z.string(),
});
export type DevConfigResponseBody = z.infer<typeof DevConfigResponseBody>;

export const DevDequeueRequestBody = z.object({
  currentWorker: z.string(),
  oldWorkers: z.string().array(),
  maxResources: MachineResources.optional(),
});
export type DevDequeueRequestBody = z.infer<typeof DevDequeueRequestBody>;

export const DevDequeueResponseBody = z.object({
  dequeuedMessages: DequeuedMessage.array(),
});
export type DevDequeueResponseBody = z.infer<typeof DevDequeueResponseBody>;

export const DevDisconnectRequestBody = z.object({
  runFriendlyIds: z.string().array(),
});
export type DevDisconnectRequestBody = z.infer<typeof DevDisconnectRequestBody>;

export const DevDisconnectResponseBody = z.object({
  cancelled: z.number(),
  bulkActionId: z.string().optional(),
});
export type DevDisconnectResponseBody = z.infer<typeof DevDisconnectResponseBody>;

export type CreateUploadPayloadUrlResponseBody = z.infer<typeof CreateUploadPayloadUrlResponseBody>;

export const ReplayRunResponse = z.object({
  id: z.string(),
});

export type ReplayRunResponse = z.infer<typeof ReplayRunResponse>;

export const CanceledRunResponse = z.object({
  id: z.string(),
});

export type CanceledRunResponse = z.infer<typeof CanceledRunResponse>;

export const ResetIdempotencyKeyResponse = z.object({
  id: z.string(),
});

export type ResetIdempotencyKeyResponse = z.infer<typeof ResetIdempotencyKeyResponse>;

export const ScheduleType = z.union([z.literal("DECLARATIVE"), z.literal("IMPERATIVE")]);

export const ScheduledTaskPayload = z.object({
  /** The schedule id associated with this run (you can have many schedules for the same task).
    You can use this to remove the schedule, update it, etc */
  scheduleId: z.string(),
  /** The type of schedule – `"DECLARATIVE"` or `"IMPERATIVE"`.
   *
   * **DECLARATIVE** – defined inline on your `schedules.task` using the `cron` property. They can only be created, updated or deleted by modifying the `cron` property on your task.
   *
   * **IMPERATIVE** – created using the `schedules.create` functions or in the dashboard.
   */
  type: ScheduleType,
  /** When the task was scheduled to run.
   * Note this will be slightly different from `new Date()` because it takes a few ms to run the task.
   *
   * This date is UTC. To output it as a string with a timezone you would do this:
   * ```ts
   * const formatted = payload.timestamp.toLocaleString("en-US", {
        timeZone: payload.timezone,
    });
    ```  */
  timestamp: z.date(),
  /** When the task was last run (it has been).
    This can be undefined if it's never been run. This date is UTC. */
  lastTimestamp: z.date().optional(),
  /** You can optionally provide an external id when creating the schedule.
    Usually you would use a userId or some other unique identifier.
    This defaults to undefined if you didn't provide one. */
  externalId: z.string().optional(),
  /** The IANA timezone the schedule is set to. The default is UTC.
   * You can see the full list of supported timezones here: https://cloud.trigger.dev/timezones
   */
  timezone: z.string(),
  /** The next 5 dates this task is scheduled to run */
  upcoming: z.array(z.date()),
});

export type ScheduledTaskPayload = z.infer<typeof ScheduledTaskPayload>;

export const CreateScheduleOptions = z.object({
  /** The id of the task you want to attach to. */
  task: z.string(),
  /**  The schedule in CRON format.
   *
   * ```txt
*    *    *    *    *    *
┬    ┬    ┬    ┬    ┬
│    │    │    │    |
│    │    │    │    └ day of week (0 - 7, 1L - 7L) (0 or 7 is Sun)
│    │    │    └───── month (1 - 12)
│    │    └────────── day of month (1 - 31, L)
│    └─────────────── hour (0 - 23)
└──────────────────── minute (0 - 59)
   * ```

"L" means the last. In the "day of week" field, 1L means the last Monday of the month. In the day of month field, L means the last day of the month.

   */
  cron: z.string(),
  /** You can only create one schedule with this key. If you use it twice, the second call will update the schedule.
   *
   * This is required to prevent you from creating duplicate schedules. */
  deduplicationKey: z.string(),
  /** Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task.
   *
   * This allows you to have per-user CRON tasks.
   */
  externalId: z.string().optional(),
  /** Optionally, you can specify a timezone in the IANA format. If unset it will use UTC.
   * If specified then the CRON will be evaluated in that timezone and will respect daylight savings.
   *
   * If you set the CRON to `0 0 * * *` and the timezone to `America/New_York` then the task will run at midnight in New York time, no matter whether it's daylight savings or not.
   *
   * You can see the full list of supported timezones here: https://cloud.trigger.dev/timezones
   *
   * @example "America/New_York", "Europe/London", "Asia/Tokyo", "Africa/Cairo"
   *
   */
  timezone: z.string().optional(),
});

export type CreateScheduleOptions = z.infer<typeof CreateScheduleOptions>;

export const UpdateScheduleOptions = CreateScheduleOptions.omit({ deduplicationKey: true });

export type UpdateScheduleOptions = z.infer<typeof UpdateScheduleOptions>;

export const ScheduleGenerator = z.object({
  type: z.literal("CRON"),
  expression: z.string(),
  description: z.string(),
});

export type ScheduleGenerator = z.infer<typeof ScheduleGenerator>;

export const ScheduleObject = z.object({
  id: z.string(),
  type: ScheduleType,
  task: z.string(),
  active: z.boolean(),
  deduplicationKey: z.string().nullish(),
  externalId: z.string().nullish(),
  generator: ScheduleGenerator,
  timezone: z.string(),
  nextRun: z.coerce.date().nullish(),
  environments: z.array(
    z.object({
      id: z.string(),
      type: z.string(),
      userName: z.string().nullish(),
    })
  ),
});

export type ScheduleObject = z.infer<typeof ScheduleObject>;

export const DeletedScheduleObject = z.object({
  id: z.string(),
});

export type DeletedScheduleObject = z.infer<typeof DeletedScheduleObject>;

export const ListSchedulesResult = z.object({
  data: z.array(ScheduleObject),
  pagination: z.object({
    currentPage: z.number(),
    totalPages: z.number(),
    count: z.number(),
  }),
});

export type ListSchedulesResult = z.infer<typeof ListSchedulesResult>;

export const ListScheduleOptions = z.object({
  page: z.number().optional(),
  perPage: z.number().optional(),
});

export type ListScheduleOptions = z.infer<typeof ListScheduleOptions>;

export const TimezonesResult = z.object({
  timezones: z.array(z.string()),
});

export type TimezonesResult = z.infer<typeof TimezonesResult>;

export const RunStatus = z.enum([
  /// Task is waiting for a version update because it cannot execute without additional information (task, queue, etc.)
  "PENDING_VERSION",
  /// Task is waiting to be executed by a worker
  "QUEUED",
  /// Task is waiting to be executed by a worker
  "DEQUEUED",
  /// Task is currently being executed by a worker
  "EXECUTING",
  /// Task has been paused by the system, and will be resumed by the system
  "WAITING",
  /// Task has been completed successfully
  "COMPLETED",
  /// Task has been canceled by the user
  "CANCELED",
  /// Task has been completed with errors
  "FAILED",
  /// Task has crashed and won't be retried, most likely the worker ran out of resources, e.g. memory or storage
  "CRASHED",
  /// Task has failed to complete, due to an error in the system
  "SYSTEM_FAILURE",
  /// Task has been scheduled to run at a specific time
  "DELAYED",
  /// Task has expired and won't be executed
  "EXPIRED",
  /// Task has reached it's maxDuration and has been stopped
  "TIMED_OUT",
]);

export type RunStatus = z.infer<typeof RunStatus>;

export const AttemptStatus = z.enum([
  "PENDING",
  "EXECUTING",
  "PAUSED",
  "COMPLETED",
  "FAILED",
  "CANCELED",
]);

export type AttemptStatus = z.infer<typeof AttemptStatus>;

export const RunEnvironmentDetails = z.object({
  id: z.string(),
  name: z.string(),
  user: z.string().optional(),
});

export type RunEnvironmentDetails = z.infer<typeof RunEnvironmentDetails>;

export const RunScheduleDetails = z.object({
  id: z.string(),
  externalId: z.string().optional(),
  deduplicationKey: z.string().optional(),
  generator: ScheduleGenerator,
});

export type RunScheduleDetails = z.infer<typeof RunScheduleDetails>;

export const TriggerFunction = z.enum([
  "triggerAndWait",
  "trigger",
  "batchTriggerAndWait",
  "batchTrigger",
]);

export type TriggerFunction = z.infer<typeof TriggerFunction>;

const CommonRunFields = {
  id: z.string(),
  status: RunStatus,
  taskIdentifier: z.string(),
  idempotencyKey: z.string().optional(),
  version: z.string().optional(),
  isQueued: z.boolean(),
  isExecuting: z.boolean(),
  isWaiting: z.boolean(),
  isCompleted: z.boolean(),
  isSuccess: z.boolean(),
  isFailed: z.boolean(),
  isCancelled: z.boolean(),
  isTest: z.boolean(),
  createdAt: z.coerce.date(),
  updatedAt: z.coerce.date(),
  startedAt: z.coerce.date().optional(),
  finishedAt: z.coerce.date().optional(),
  delayedUntil: z.coerce.date().optional(),
  ttl: z.string().optional(),
  expiredAt: z.coerce.date().optional(),
  tags: z.string().array(),
  costInCents: z.number(),
  baseCostInCents: z.number(),
  durationMs: z.number(),
  metadata: z.record(z.any()).optional(),
  taskKind: z.string().optional(),
  region: z.string().optional(),
};

const RetrieveRunCommandFields = {
  ...CommonRunFields,
  depth: z.number(),
  triggerFunction: z.enum(["triggerAndWait", "trigger", "batchTriggerAndWait", "batchTrigger"]),
  batchId: z.string().optional(),
};

export const RelatedRunDetails = z.object(RetrieveRunCommandFields);

export const RetrieveRunResponse = z.object({
  ...RetrieveRunCommandFields,
  payload: z.any().optional(),
  payloadPresignedUrl: z.string().optional(),
  output: z.any().optional(),
  outputPresignedUrl: z.string().optional(),
  error: SerializedError.optional(),
  schedule: RunScheduleDetails.optional(),
  relatedRuns: z.object({
    root: RelatedRunDetails.optional(),
    parent: RelatedRunDetails.optional(),
    children: z.array(RelatedRunDetails).optional(),
  }),
  attemptCount: z.number().default(0),
});

export type RetrieveRunResponse = z.infer<typeof RetrieveRunResponse>;

export const ListRunResponseItem = z.object({
  ...CommonRunFields,
  env: RunEnvironmentDetails,
});

export type ListRunResponseItem = z.infer<typeof ListRunResponseItem>;

export const ListRunResponse = z.object({
  data: z.array(ListRunResponseItem),
  pagination: z.object({
    next: z.string().optional(),
    previous: z.string().optional(),
  }),
});

export type ListRunResponse = z.infer<typeof ListRunResponse>;

export const CreateEnvironmentVariableRequestBody = z.object({
  name: z.string(),
  value: z.string(),
});

export type CreateEnvironmentVariableRequestBody = z.infer<
  typeof CreateEnvironmentVariableRequestBody
>;

export const UpdateEnvironmentVariableRequestBody = z.object({
  value: z.string(),
});

export type UpdateEnvironmentVariableRequestBody = z.infer<
  typeof UpdateEnvironmentVariableRequestBody
>;

export const ImportEnvironmentVariablesRequestBody = z.object({
  variables: z.record(z.string()),
  parentVariables: z.record(z.string()).optional(),
  override: z.boolean().optional(),
  // When omitted, variables default to non-secret (the DB default is false).
  isSecret: z.boolean().optional(),
  source: z
    .discriminatedUnion("type", [
      z.object({ type: z.literal("user"), userId: z.string() }),
      z.object({ type: z.literal("integration"), integration: z.string() }),
    ])
    .optional(),
});

export type ImportEnvironmentVariablesRequestBody = z.infer<
  typeof ImportEnvironmentVariablesRequestBody
>;

export const EnvironmentVariableResponseBody = z.object({
  success: z.boolean(),
});

export type EnvironmentVariableResponseBody = z.infer<typeof EnvironmentVariableResponseBody>;

export const EnvironmentVariableValue = z.object({
  value: z.string(),
});

export type EnvironmentVariableValue = z.infer<typeof EnvironmentVariableValue>;

export const EnvironmentVariable = z.object({
  name: z.string(),
  value: z.string(),
});
export const EnvironmentVariables = z.array(EnvironmentVariable);

export type EnvironmentVariables = z.infer<typeof EnvironmentVariables>;

export const EnvironmentVariableWithSecret = z.object({
  /** The name of the env var, e.g. `DATABASE_URL` */
  name: z.string(),
  /** The value of the env var. If it's a secret, this will be a redacted value, not the real value.  */
  value: z.string(),
  /**
   * Whether the env var is a secret or not.
   * When you create env vars you can mark them as secrets.
   *
   * You can't view the value of a secret env var after setting it initially.
   * For a secret env var, the value will be redacted.
   */
  isSecret: z.boolean(),
});
export type EnvironmentVariableWithSecret = z.infer<typeof EnvironmentVariableWithSecret>;

export const UpdateMetadataRequestBody = FlushedRunMetadata;

export type UpdateMetadataRequestBody = z.infer<typeof UpdateMetadataRequestBody>;

export const UpdateMetadataResponseBody = z.object({
  metadata: z.record(DeserializedJsonSchema),
});

export type UpdateMetadataResponseBody = z.infer<typeof UpdateMetadataResponseBody>;

const RawShapeDate = z
  .string()
  .transform((val) => `${val}Z`)
  .pipe(z.coerce.date());

const RawOptionalShapeDate = z
  .string()
  .nullish()
  .transform((val) => (val ? new Date(`${val}Z`) : val));

export const SubscribeRunRawShape = z.object({
  id: z.string(),
  taskIdentifier: z.string(),
  friendlyId: z.string(),
  status: z.string(),
  createdAt: RawShapeDate,
  updatedAt: RawShapeDate,
  startedAt: RawOptionalShapeDate,
  delayUntil: RawOptionalShapeDate,
  queuedAt: RawOptionalShapeDate,
  expiredAt: RawOptionalShapeDate,
  completedAt: RawOptionalShapeDate,
  idempotencyKey: z.string().nullish(),
  number: z.number().default(0),
  isTest: z.boolean().default(false),
  usageDurationMs: z.number().default(0),
  costInCents: z.number().default(0),
  baseCostInCents: z.number().default(0),
  ttl: z.string().nullish(),
  payload: z.string().nullish(),
  payloadType: z.string().nullish(),
  metadata: z.string().nullish(),
  metadataType: z.string().nullish(),
  output: z.string().nullish(),
  outputType: z.string().nullish(),
  runTags: z.array(z.string()).nullish().default([]),
  error: TaskRunError.nullish(),
  realtimeStreams: z.array(z.string()).nullish().default([]),
});

export type SubscribeRunRawShape = z.infer<typeof SubscribeRunRawShape>;

export const BatchStatus = z.enum([
  "PENDING",
  "PROCESSING",
  "COMPLETED",
  "PARTIAL_FAILED",
  "ABORTED",
]);

export type BatchStatus = z.infer<typeof BatchStatus>;

export const RetrieveBatchResponse = z.object({
  id: z.string(),
  status: BatchStatus,
  idempotencyKey: z.string().optional(),
  createdAt: z.coerce.date(),
  updatedAt: z.coerce.date(),
  runCount: z.number(),
  runs: z.array(z.string()),
});

export type RetrieveBatchResponse = z.infer<typeof RetrieveBatchResponse>;

export const RetrieveBatchV2Response = z.object({
  id: z.string(),
  status: BatchStatus,
  idempotencyKey: z.string().optional(),
  createdAt: z.coerce.date(),
  updatedAt: z.coerce.date(),
  runCount: z.number(),
  runs: z.array(z.string()),
  processing: z.object({
    completedAt: z.coerce.date().optional(),
    errors: z.array(
      z.object({
        index: z.number(),
        taskIdentifier: z.string(),
        error: z.string(),
        errorCode: z.string().optional(),
      })
    ),
  }),
});

export type RetrieveBatchV2Response = z.infer<typeof RetrieveBatchV2Response>;

export const SubscribeRealtimeStreamChunkRawShape = z.object({
  id: z.string(),
  runId: z.string(),
  sequence: z.number(),
  key: z.string(),
  value: z.string(),
  createdAt: z.coerce.date(),
});

export type SubscribeRealtimeStreamChunkRawShape = z.infer<
  typeof SubscribeRealtimeStreamChunkRawShape
>;

export const TimePeriod = z.string().or(z.coerce.date());
export type TimePeriod = z.infer<typeof TimePeriod>;

export const CreateWaitpointTokenRequestBody = z.object({
  /**
   * An optional idempotency key for the waitpoint.
   * If you use the same key twice (and the key hasn't expired), you will get the original waitpoint back.
   *
   * Note: This waitpoint may already be complete, in which case when you wait for it, it will immediately continue.
   */
  idempotencyKey: z
    .string()
    // Caps user-supplied keys before they reach the unique idempotency index
    // on the underlying table — values past this fail at the database layer
    // rather than returning a clean 400.
    .max(2048, "idempotencyKey must be 2048 characters or less")
    .optional(),
  /**
   * When set, this means the passed in idempotency key will expire after this time.
   * This means after that time if you pass the same idempotency key again, you will get a new waitpoint.
   */
  idempotencyKeyTTL: z.string().optional(),
  /** The resume token will timeout after this time.
   * If you are waiting for the token in a run, the token will return a result where `ok` is false.
   *
   * You can pass a `Date` object, or a string in this format: "30s", "1m", "2h", "3d", "4w".
   */
  timeout: TimePeriod.optional(),
  /**
   * Tags to attach to the waitpoint. Tags can be used to filter waitpoints in the dashboard.
   *
   * You can set up to 10 tags per waitpoint, they must be less than 128 characters each.
   *
   * We recommend prefixing tags with a namespace using an underscore or colon, like `user_1234567` or `org:9876543`.
   *
   * @example
   *
   * ```ts
   * await wait.createToken({ tags: ["user:1234567", "org:9876543"] });
   * ```
   */
  tags: RunTags.optional(),
});
export type CreateWaitpointTokenRequestBody = z.infer<typeof CreateWaitpointTokenRequestBody>;

export const CreateWaitpointTokenResponseBody = z.object({
  id: z.string(),
  isCached: z.boolean(),
  url: z.string(),
});
export type CreateWaitpointTokenResponseBody = z.infer<typeof CreateWaitpointTokenResponseBody>;

export const CreateInputStreamWaitpointRequestBody = z.object({
  streamId: z.string(),
  timeout: z.string().optional(),
  idempotencyKey: z
    .string()
    // Caps user-supplied keys before they reach the unique idempotency index
    // on the underlying table — values past this fail at the database layer
    // rather than returning a clean 400.
    .max(2048, "idempotencyKey must be 2048 characters or less")
    .optional(),
  idempotencyKeyTTL: z.string().optional(),
  tags: z.union([z.string(), z.array(z.string())]).optional(),
  /**
   * The last S2 sequence number the client has seen on this input stream.
   * Used to check for data that arrived before .wait() was called.
   * If undefined, the server checks from the beginning of the stream.
   */
  lastSeqNum: z.number().optional(),
});
export type CreateInputStreamWaitpointRequestBody = z.infer<
  typeof CreateInputStreamWaitpointRequestBody
>;

export const CreateInputStreamWaitpointResponseBody = z.object({
  waitpointId: z.string(),
  isCached: z.boolean(),
});
export type CreateInputStreamWaitpointResponseBody = z.infer<
  typeof CreateInputStreamWaitpointResponseBody
>;

/**
 * Create a run-scoped waitpoint that completes when the next record lands on
 * a Session channel (`.in` or `.out`). Mirrors `CreateInputStreamWaitpointRequestBody`
 * but keyed by `{sessionId, io}` instead of `{runId, streamId}`. The run is
 * still the thing being suspended — Session only supplies the trigger source.
 */
export const CreateSessionStreamWaitpointRequestBody = z.object({
  /** Session friendlyId (`session_*`) or user-supplied externalId. */
  session: z.string(),
  io: z.enum(["out", "in"]),
  timeout: z.string().optional(),
  idempotencyKey: z
    .string()
    // Caps user-supplied keys before they reach the unique idempotency index
    // on the underlying table — values past this fail at the database layer
    // rather than returning a clean 400.
    .max(2048, "idempotencyKey must be 2048 characters or less")
    .optional(),
  idempotencyKeyTTL: z.string().optional(),
  tags: z.union([z.string(), z.array(z.string())]).optional(),
  /**
   * Last S2 sequence number the client has seen on this session channel.
   * Used to catch data that arrived before `.wait()` was called.
   */
  lastSeqNum: z.number().optional(),
});
export type CreateSessionStreamWaitpointRequestBody = z.infer<
  typeof CreateSessionStreamWaitpointRequestBody
>;

export const CreateSessionStreamWaitpointResponseBody = z.object({
  waitpointId: z.string(),
  isCached: z.boolean(),
});
export type CreateSessionStreamWaitpointResponseBody = z.infer<
  typeof CreateSessionStreamWaitpointResponseBody
>;

export const waitpointTokenStatuses = ["WAITING", "COMPLETED", "TIMED_OUT"] as const;
export const WaitpointTokenStatus = z.enum(waitpointTokenStatuses);
export type WaitpointTokenStatus = z.infer<typeof WaitpointTokenStatus>;

export const WaitpointTokenItem = z.object({
  id: z.string(),
  /** If you make a POST request to this URL, it will complete the waitpoint. */
  url: z.string(),
  status: WaitpointTokenStatus,
  completedAt: z.coerce.date().optional(),
  completedAfter: z.coerce.date().optional(),
  timeoutAt: z.coerce.date().optional(),
  idempotencyKey: z.string().optional(),
  idempotencyKeyExpiresAt: z.coerce.date().optional(),
  tags: z.array(z.string()),
  createdAt: z.coerce.date(),
});
export type WaitpointTokenItem = z.infer<typeof WaitpointTokenItem>;

export const WaitpointListTokenItem = WaitpointTokenItem.omit({
  completedAfter: true,
});
export type WaitpointListTokenItem = z.infer<typeof WaitpointListTokenItem>;

export const WaitpointRetrieveTokenResponse = WaitpointListTokenItem.and(
  z.object({
    output: z.string().optional(),
    outputType: z.string().optional(),
    outputIsError: z.boolean().optional(),
  })
);
export type WaitpointRetrieveTokenResponse = z.infer<typeof WaitpointRetrieveTokenResponse>;

export const CompleteWaitpointTokenRequestBody = z.object({
  data: z.any().nullish(),
});
export type CompleteWaitpointTokenRequestBody = z.infer<typeof CompleteWaitpointTokenRequestBody>;

/**
 * Trigger config persisted on a Session. Drives every run the session
 * schedules — `basePayload` is the customer's wire payload (for
 * chat.agent: `{ chatId, ...clientData }`), runtime fields like
 * `trigger: "preload" | "trigger"` are merged on top per-call by the
 * server's trigger machinery.
 */
export const SessionTriggerConfig = z.object({
  basePayload: z.record(z.unknown()),
  machine: MachinePresetName.optional(),
  queue: z.string().max(128).optional(),
  tags: z.array(z.string().max(128)).max(5).optional(),
  maxAttempts: z.number().int().positive().max(10).optional(),
  /** Per-run wall-clock cap (seconds). Forwarded to `TaskRunOptions.maxDuration`. */
  maxDuration: z.number().int().positive().optional(),
  /** Pin every run to a specific worker version. Forwarded to `TaskRunOptions.lockToVersion`. */
  lockToVersion: z.string().optional(),
  /** Region to schedule runs in. Forwarded to `TaskRunOptions.region`. */
  region: z.string().optional(),
  /** Convenience field surfaced to chat.agent via the wire payload. */
  idleTimeoutInSeconds: z.number().int().positive().max(3600).optional(),
});
export type SessionTriggerConfig = z.infer<typeof SessionTriggerConfig>;

/**
 * Request body for `POST /api/v1/sessions`. Creates a Session and
 * triggers its first run. Sessions are task-bound: `taskIdentifier` and
 * `triggerConfig` are required, and re-runs scheduled by the server
 * (after run termination, after `end-and-continue`) reuse the same
 * config.
 */
export const CreateSessionRequestBody = z.object({
  /** Plain string discriminator — e.g. `"chat.agent"`. Not validated against an enum on the server. */
  type: z.string().min(1).max(64),
  /** User-supplied idempotency key. Unique per environment. Empty strings are rejected. */
  externalId: z
    .string()
    .trim()
    .min(1)
    .max(256)
    .refine((v) => !v.startsWith("session_"), {
      message: "externalId cannot start with 'session_' (reserved prefix for internal friendlyIds)",
    })
    .optional(),
  /** Task this session triggers runs against. Required. */
  taskIdentifier: z.string().min(1).max(128),
  /** Trigger config used for every run scheduled by this session. */
  triggerConfig: SessionTriggerConfig,
  /** Up to 10 tags for dashboard filtering. */
  tags: z.array(z.string().max(128)).max(10).optional(),
  /** Arbitrary JSON metadata. */
  metadata: z.record(z.unknown()).optional(),
  /** Absolute expiry timestamp for retention. */
  expiresAt: z.coerce.date().optional(),
});
export type CreateSessionRequestBody = z.infer<typeof CreateSessionRequestBody>;

export const SessionItem = z.object({
  id: z.string(),
  externalId: z.string().nullable(),
  type: z.string(),
  taskIdentifier: z.string(),
  /**
   * Optional on the wire because some surfaces (the list endpoint backed
   * by ClickHouse, list-page rendering) don't carry triggerConfig.
   * Always populated on `POST /sessions` and `GET /sessions/:id`.
   */
  triggerConfig: SessionTriggerConfig.optional(),
  /**
   * Friendly id of the live run for this session, if any. Optional on
   * the wire — list surfaces may not include it. Routes that emit
   * `SessionItem` are responsible for resolving the friendly form
   * from the underlying cuid before returning.
   */
  currentRunId: z.string().nullable().optional(),
  tags: z.array(z.string()),
  metadata: z.record(z.unknown()).nullable(),
  closedAt: z.coerce.date().nullable(),
  closedReason: z.string().nullable(),
  expiresAt: z.coerce.date().nullable(),
  createdAt: z.coerce.date(),
  updatedAt: z.coerce.date(),
});
export type SessionItem = z.infer<typeof SessionItem>;

export const CreatedSessionResponseBody = SessionItem.extend({
  /** Friendly id of the first run triggered alongside session create. */
  runId: z.string(),
  /** Session-scoped public access token: `read:sessions:{ext} + write:sessions:{ext}`. */
  publicAccessToken: z.string(),
  /** True if the session existed already (idempotent upsert), false if newly created. */
  isCached: z.boolean(),
});
export type CreatedSessionResponseBody = z.infer<typeof CreatedSessionResponseBody>;

export const RetrieveSessionResponseBody = SessionItem;
export type RetrieveSessionResponseBody = z.infer<typeof RetrieveSessionResponseBody>;

/**
 * Body for `POST /api/v1/sessions/:session/end-and-continue`. Used by the
 * running agent to request a clean handoff to a fresh run on the latest
 * deployed version (typical use case: `chat.requestUpgrade`). The
 * server triggers a new run, atomically swaps `currentRunId`, and the
 * caller exits.
 */
export const EndAndContinueSessionRequestBody = z.object({
  /** The friendlyId of the run requesting the handoff. */
  callingRunId: z.string(),
  /** Free-form label for the SessionRun audit row. e.g. `"upgrade"`. */
  reason: z.string().max(64),
});
export type EndAndContinueSessionRequestBody = z.infer<typeof EndAndContinueSessionRequestBody>;

export const EndAndContinueSessionResponseBody = z.object({
  /** friendlyId of the run that has taken over the session. */
  runId: z.string(),
  /**
   * False when the swap was preempted (a different run was already
   * running by the time we tried to claim). The caller should treat
   * this as "someone else moved on" — exit cleanly without expecting
   * to drive the next run.
   */
  swapped: z.boolean(),
});
export type EndAndContinueSessionResponseBody = z.infer<typeof EndAndContinueSessionResponseBody>;

export const UpdateSessionRequestBody = z.object({
  tags: z.array(z.string().max(128)).max(10).optional(),
  metadata: z.record(z.unknown()).nullable().optional(),
  // Null explicitly clears the externalId; non-null values must be non-empty.
  externalId: z
    .union([
      z.literal(null),
      z
        .string()
        .trim()
        .min(1)
        .max(256)
        .refine((v) => !v.startsWith("session_"), {
          message:
            "externalId cannot start with 'session_' (reserved prefix for internal friendlyIds)",
        }),
    ])
    .optional(),
});
export type UpdateSessionRequestBody = z.infer<typeof UpdateSessionRequestBody>;

export const CloseSessionRequestBody = z.object({
  reason: z.string().max(256).optional(),
});
export type CloseSessionRequestBody = z.infer<typeof CloseSessionRequestBody>;

export const SessionStatus = z.enum(["ACTIVE", "CLOSED", "EXPIRED"]);
export type SessionStatus = z.infer<typeof SessionStatus>;

/**
 * Server-side validation schema for `GET /api/v1/sessions`. Follows the same
 * cursor-pagination convention as runs/waitpoints (`page[size]`,
 * `page[after]`, `page[before]`) and uses the `filter[*]` prefix for
 * narrowing fields — both produced automatically by `zodfetchCursorPage`
 * and the matching client-side search-query helper.
 */
export const ListSessionsQueryParams = z
  .object({
    "page[size]": z.coerce.number().int().min(1).max(100).default(20),
    "page[after]": z.string().optional(),
    "page[before]": z.string().optional(),
    "filter[type]": z.union([z.string(), z.array(z.string())]).optional(),
    "filter[tags]": z.union([z.string(), z.array(z.string())]).optional(),
    "filter[taskIdentifier]": z.union([z.string(), z.array(z.string())]).optional(),
    "filter[externalId]": z.string().optional(),
    "filter[status]": z.union([SessionStatus, z.array(SessionStatus)]).optional(),
    "filter[createdAt][period]": z.string().optional(),
    "filter[createdAt][from]": z.coerce.number().int().optional(),
    "filter[createdAt][to]": z.coerce.number().int().optional(),
  })
  .refine((value) => !(value["page[after]"] && value["page[before]"]), {
    message: "Cannot pass both page[after] and page[before] on the same request",
    path: ["page[before]"],
  });
export type ListSessionsQueryParams = z.infer<typeof ListSessionsQueryParams>;

/**
 * Client-facing list options — flattened shape that
 * {@link ApiClient.listSessions} converts into the `filter[*]` / `page[*]`
 * query string before sending.
 */
export const ListSessionsOptions = z.object({
  limit: z.number().int().min(1).max(100).optional(),
  after: z.string().optional(),
  before: z.string().optional(),
  type: z.union([z.string(), z.array(z.string())]).optional(),
  tag: z.union([z.string(), z.array(z.string())]).optional(),
  taskIdentifier: z.union([z.string(), z.array(z.string())]).optional(),
  externalId: z.string().optional(),
  status: z.union([SessionStatus, z.array(SessionStatus)]).optional(),
  period: z.string().optional(),
  from: z.union([z.number(), z.date()]).optional(),
  to: z.union([z.number(), z.date()]).optional(),
});
export type ListSessionsOptions = z.infer<typeof ListSessionsOptions>;

export const ListedSessionItem = SessionItem;
export type ListedSessionItem = z.infer<typeof ListedSessionItem>;

export const ListSessionsResponseBody = z.object({
  data: z.array(ListedSessionItem),
  pagination: z.object({
    next: z.string().optional(),
    previous: z.string().optional(),
  }),
});
export type ListSessionsResponseBody = z.infer<typeof ListSessionsResponseBody>;

export const CompleteWaitpointTokenResponseBody = z.object({
  success: z.literal(true),
});
export type CompleteWaitpointTokenResponseBody = z.infer<typeof CompleteWaitpointTokenResponseBody>;

export const WaitForWaitpointTokenResponseBody = z.object({
  success: z.boolean(),
});
export type WaitForWaitpointTokenResponseBody = z.infer<typeof WaitForWaitpointTokenResponseBody>;

export const WaitForDurationRequestBody = z.object({
  /**
   * An optional idempotency key for the waitpoint.
   * If you use the same key twice (and the key hasn't expired), you will get the original waitpoint back.
   *
   * Note: This waitpoint may already be complete, in which case when you wait for it, it will immediately continue.
   */
  idempotencyKey: z
    .string()
    // Caps user-supplied keys before they reach the unique idempotency index
    // on the underlying table — values past this fail at the database layer
    // rather than returning a clean 400.
    .max(2048, "idempotencyKey must be 2048 characters or less")
    .optional(),
  /**
   * When set, this means the passed in idempotency key will expire after this time.
   * This means after that time if you pass the same idempotency key again, you will get a new waitpoint.
   */
  idempotencyKeyTTL: z.string().optional(),

  /**
   * The date that the waitpoint will complete.
   */
  date: z.coerce.date(),
});
export type WaitForDurationRequestBody = z.infer<typeof WaitForDurationRequestBody>;

export const WaitForDurationResponseBody = z.object({
  /**
      If you pass an idempotencyKey, you may actually not need to wait.
      Use this date to determine when to continue.
  */
  waitUntil: z.coerce.date(),
  waitpoint: z.object({
    id: z.string(),
  }),
});
export type WaitForDurationResponseBody = z.infer<typeof WaitForDurationResponseBody>;

const WAITPOINT_TIMEOUT_ERROR_CODE = "TRIGGER_WAITPOINT_TIMEOUT";

export function isWaitpointOutputTimeout(output: string): boolean {
  try {
    const json = JSON.parse(output);
    return json.code === WAITPOINT_TIMEOUT_ERROR_CODE;
  } catch (e) {
    return false;
  }
}

export function timeoutError(timeout: Date) {
  return {
    code: WAITPOINT_TIMEOUT_ERROR_CODE,
    message: `Waitpoint timed out at ${timeout.toISOString()}`,
  };
}

const ApiDeploymentCommonShape = {
  from: z.string().describe("The date to start the search from, in ISO 8601 format").optional(),
  to: z.string().describe("The date to end the search, in ISO 8601 format").optional(),
  period: z.string().describe("The period to search within (e.g. 1d, 7d, 3h, etc.)").optional(),
  status: z
    .enum(["PENDING", "BUILDING", "DEPLOYING", "DEPLOYED", "FAILED", "CANCELED", "TIMED_OUT"])
    .describe("Filter deployments that are in this status")
    .optional(),
};

const ApiDeploymentListPaginationCursor = z
  .string()
  .describe("The deployment ID to start the search from, to get the next page")
  .optional();

const ApiDeploymentListPaginationLimit = z.coerce
  .number()
  .describe("The number of deployments to return, defaults to 20 (max 100)")
  .min(1, "Limit must be at least 1")
  .max(100, "Limit must be less than 100")
  .optional();

export const ApiDeploymentListParams = {
  ...ApiDeploymentCommonShape,
  cursor: ApiDeploymentListPaginationCursor,
  limit: ApiDeploymentListPaginationLimit,
};

export const ApiDeploymentListOptions = z.object(ApiDeploymentListParams);

export type ApiDeploymentListOptions = z.infer<typeof ApiDeploymentListOptions>;

export const ApiDeploymentListSearchParams = z.object({
  ...ApiDeploymentCommonShape,
  "page[after]": ApiDeploymentListPaginationCursor,
  "page[size]": ApiDeploymentListPaginationLimit,
});

export type ApiDeploymentListSearchParams = z.infer<typeof ApiDeploymentListSearchParams>;

export const ApiDeploymentListResponseItem = z.object({
  id: z.string(),
  createdAt: z.coerce.date(),
  shortCode: z.string(),
  version: z.string(),
  runtime: z.string().nullable(),
  runtimeVersion: z.string().nullable(),
  status: z.enum([
    "PENDING",
    "BUILDING",
    "DEPLOYING",
    "DEPLOYED",
    "FAILED",
    "CANCELED",
    "TIMED_OUT",
  ]),
  deployedAt: z.coerce.date().optional(),
  git: z.record(z.any()).optional(),
  error: DeploymentErrorData.optional(),
});

export type ApiDeploymentListResponseItem = z.infer<typeof ApiDeploymentListResponseItem>;

export const RetrieveCurrentDeploymentResponseBody = ApiDeploymentListResponseItem;
export type RetrieveCurrentDeploymentResponseBody = ApiDeploymentListResponseItem;

export const ApiBranchListResponseBody = z.object({
  branches: z.array(
    z.object({
      id: z.string(),
      name: z.string(),
      createdAt: z.coerce.date(),
      updatedAt: z.coerce.date(),
      git: z.record(z.any()).optional(),
      isPaused: z.boolean(),
    })
  ),
});

export type ApiBranchListResponseBody = z.infer<typeof ApiBranchListResponseBody>;

export const RetrieveRunTraceSpanSchema = z.object({
  id: z.string(),
  parentId: z.string().optional(),
  runId: z.string(),
  data: z.object({
    message: z.string(),
    taskSlug: z.string().optional(),
    taskPath: z.string().optional(),
    events: z.array(z.any()).optional(),
    startTime: z.coerce.date(),
    duration: z.number(),
    isError: z.boolean(),
    isPartial: z.boolean(),
    isCancelled: z.boolean(),
    level: z.string(),
    workerVersion: z.string().optional(),
    queueName: z.string().optional(),
    machinePreset: z.string().optional(),
    properties: z.record(z.any()).optional(),
    output: z.unknown().optional(),
  }),
});

export type RetrieveRunTraceSpan = z.infer<typeof RetrieveRunTraceSpanSchema> & {
  children: Array<RetrieveRunTraceSpan>;
};

export const RetrieveRunTraceSpan: z.ZodType<RetrieveRunTraceSpan> =
  RetrieveRunTraceSpanSchema.extend({
    children: z.lazy(() => RetrieveRunTraceSpan.array()),
  });

export const RetrieveRunTraceResponseBody = z.object({
  trace: z.object({
    traceId: z.string(),
    rootSpan: RetrieveRunTraceSpan,
  }),
});

export type RetrieveRunTraceResponseBody = z.infer<typeof RetrieveRunTraceResponseBody>;

export const RetrieveSpanDetailResponseBody = z.object({
  spanId: z.string(),
  parentId: z.string().nullable(),
  runId: z.string(),
  message: z.string(),
  isError: z.boolean(),
  isPartial: z.boolean(),
  isCancelled: z.boolean(),
  level: z.string(),
  startTime: z.coerce.date(),
  durationMs: z.number(),
  properties: z.record(z.any()).optional(),
  events: z.array(z.any()).optional(),
  entityType: z.string().optional(),
  ai: z
    .object({
      model: z.string(),
      provider: z.string(),
      operationName: z.string(),
      inputTokens: z.number(),
      outputTokens: z.number(),
      totalTokens: z.number(),
      cachedTokens: z.number().optional(),
      reasoningTokens: z.number().optional(),
      inputCost: z.number().optional(),
      outputCost: z.number().optional(),
      totalCost: z.number().optional(),
      cachedCost: z.number().optional(),
      cacheCreationCost: z.number().optional(),
      tokensPerSecond: z.number().optional(),
      msToFirstChunk: z.number().optional(),
      durationMs: z.number(),
      finishReason: z.string().optional(),
      responseText: z.string().optional(),
    })
    .optional(),
  triggeredRuns: z
    .array(
      z.object({
        runId: z.string(),
        taskIdentifier: z.string(),
        status: z.string(),
        createdAt: z.coerce.date(),
      })
    )
    .optional(),
});

export type RetrieveSpanDetailResponseBody = z.infer<typeof RetrieveSpanDetailResponseBody>;

export const CreateStreamResponseBody = z.object({
  version: z.string(),
});
export type CreateStreamResponseBody = z.infer<typeof CreateStreamResponseBody>;

export const AppendToStreamResponseBody = z.object({
  ok: z.boolean(),
  message: z.string().optional(),
});
export type AppendToStreamResponseBody = z.infer<typeof AppendToStreamResponseBody>;

export const SendInputStreamResponseBody = z.object({
  ok: z.boolean(),
});
export type SendInputStreamResponseBody = z.infer<typeof SendInputStreamResponseBody>;

/**
 * Response body for `GET /realtime/v1/sessions/:id/:io/records`. A non-SSE,
 * `wait=0` drain of a session channel — used at run boot for snapshot
 * replay where the SSE long-poll tax (~1s on empty streams) was the
 * dominant cost.
 *
 * `data` is the parsed chunk body (the SDK writer puts the chunk object
 * directly into the S2 record envelope; the route unwraps the envelope
 * and forwards the inner object as-is). Callers use it directly — no
 * additional JSON.parse step. Schema is `z.unknown()` because chunk
 * shape varies by `chunk.type` (the AI SDK's `UIMessageChunk`
 * discriminated union plus Trigger control records); consumers
 * already runtime-check on the discriminator and tolerate malformed
 * records by skipping them.
 */
export const ReadSessionStreamRecordsResponseBody = z.object({
  records: z.array(
    z.object({
      data: z.unknown(),
      id: z.string(),
      seqNum: z.number(),
      // S2 record headers — present on Trigger control records (e.g.
      // `trigger-control: turn-complete` plus sibling headers). The
      // server has always serialized them; older schemas stripped them
      // client-side, so treat as optional.
      headers: z.array(z.tuple([z.string(), z.string()])).optional(),
    })
  ),
});
export type ReadSessionStreamRecordsResponseBody = z.infer<
  typeof ReadSessionStreamRecordsResponseBody
>;

export const ResolvePromptRequestBody = z.object({
  variables: z.record(z.unknown()).default({}),
  label: z.string().optional(),
  version: z.number().optional(),
});
export type ResolvePromptRequestBody = z.infer<typeof ResolvePromptRequestBody>;

export const ResolvePromptResponseBody = z.object({
  data: z.object({
    promptId: z.string(),
    slug: z.string(),
    version: z.number(),
    labels: z.array(z.string()),
    template: z.string().optional(),
    text: z.string().optional(),
    model: z.string().optional().nullable(),
    config: z.record(z.unknown()).optional().nullable(),
  }),
});
export type ResolvePromptResponseBody = z.infer<typeof ResolvePromptResponseBody>;

export const ListPromptsResponseBody = z.object({
  data: z.array(
    z.object({
      slug: z.string(),
      friendlyId: z.string(),
      description: z.string().nullable(),
      tags: z.array(z.string()),
      defaultModel: z.string().nullable(),
      currentVersion: z.number().nullable(),
      hasOverride: z.boolean(),
      updatedAt: z.string(),
    })
  ),
});
export type ListPromptsResponseBody = z.infer<typeof ListPromptsResponseBody>;

export const ListPromptVersionsResponseBody = z.object({
  data: z.array(
    z.object({
      id: z.string(),
      version: z.number(),
      labels: z.array(z.string()),
      source: z.string(),
      model: z.string().nullable(),
      textContent: z.string().nullable(),
      commitMessage: z.string().nullable(),
      contentHash: z.string(),
      createdAt: z.string(),
    })
  ),
});
export type ListPromptVersionsResponseBody = z.infer<typeof ListPromptVersionsResponseBody>;

export const PromotePromptVersionRequestBody = z.object({
  version: z.number().int().positive(),
});
export type PromotePromptVersionRequestBody = z.infer<typeof PromotePromptVersionRequestBody>;

export const CreatePromptOverrideRequestBody = z.object({
  textContent: z.string(),
  model: z.string().optional(),
  commitMessage: z.string().optional(),
  source: z.string().optional(),
});
export type CreatePromptOverrideRequestBody = z.infer<typeof CreatePromptOverrideRequestBody>;

export const UpdatePromptOverrideRequestBody = z.object({
  textContent: z.string().optional(),
  model: z.string().optional(),
  commitMessage: z.string().optional(),
});
export type UpdatePromptOverrideRequestBody = z.infer<typeof UpdatePromptOverrideRequestBody>;

export const ReactivatePromptOverrideRequestBody = z.object({
  version: z.number().int().positive(),
});
export type ReactivatePromptOverrideRequestBody = z.infer<
  typeof ReactivatePromptOverrideRequestBody
>;

export const PromptOkResponseBody = z.object({ ok: z.boolean() });
export type PromptOkResponseBody = z.infer<typeof PromptOkResponseBody>;

export const PromptOverrideCreatedResponseBody = z.object({
  ok: z.boolean(),
  version: z.number(),
});
export type PromptOverrideCreatedResponseBody = z.infer<typeof PromptOverrideCreatedResponseBody>;
