import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { LeaseHandle, LeaseMetadata, LocksPort } from "../locks/index.js";
import { runWithResolvedTracingContext } from "../tracing/execution.js";
import {
  parseTraceCarrier,
  type TraceCarrier,
  type TracingPort,
} from "../tracing/index.js";

/**
 * Any Standard Schema compatible validator.
 */
export type StandardSchema = StandardSchemaV1<unknown, unknown>;

/**
 * Value or promise of that value.
 */
export type MaybePromise<T> = T | Promise<T>;

type NoInferJob<T> = [T][T extends unknown ? 0 : never];

/**
 * Infer the parsed output type from a Standard Schema.
 */
export type InferSchemaOutput<T extends StandardSchemaV1> =
  StandardSchemaV1.InferOutput<T>;

/**
 * Duration accepted by job helpers. Numbers are milliseconds.
 */
export type JobDuration =
  | number
  | `${number}ms`
  | `${number}s`
  | `${number}m`
  | `${number}h`;

/**
 * Duration accepted by retry helpers. Numbers are milliseconds.
 */
export type JobRetryDuration = JobDuration;

/**
 * Duration accepted by job timeout helpers. Numbers are milliseconds.
 */
export type JobTimeoutDuration = JobDuration;

/**
 * Duration accepted by job execution lease helpers. Numbers are milliseconds.
 */
export type JobExecutionLeaseDuration = JobDuration;

/**
 * Retry strategy understood by Beignet job adapters.
 */
export type JobRetryStrategy = "none" | "fixed" | "exponential";

/**
 * Arguments passed to a retry predicate.
 */
export interface JobRetryPredicateArgs {
  /**
   * Error thrown by the previous attempt.
   */
  error: unknown;
  /**
   * One-based attempt number that just failed.
   */
  attempt: number;
  /**
   * Maximum attempts allowed for this delivery.
   */
  maxAttempts: number;
  /**
   * Job name when the retry decision is for a job.
   */
  jobName?: string;
}

/**
 * Return whether a failed attempt should be retried.
 */
export type JobRetryPredicate = (args: JobRetryPredicateArgs) => boolean;

/**
 * Dispatch-time uniqueness metadata for a job.
 *
 * Unique jobs are suppressed while the resolved key's lease is active.
 * The lease is intentionally left to expire after a successful dispatch.
 */
export interface JobUniqueOptions {
  /**
   * Logical uniqueness key within this job name.
   */
  key: string;
  /**
   * How long duplicate dispatches should be suppressed.
   */
  ttl: JobDuration;
}

/**
 * Arguments passed to a per-payload unique job resolver.
 */
export interface JobUniqueResolverArgs<Payload extends StandardSchema> {
  /**
   * Stable job name.
   */
  jobName: string;
  /**
   * Parsed job payload.
   */
  payload: StandardSchemaV1.InferOutput<Payload>;
}

/**
 * Per-payload resolver for dispatch-time uniqueness.
 */
export type JobUniqueResolver<Payload extends StandardSchema> = {
  bivarianceHack(
    args: JobUniqueResolverArgs<Payload>,
  ): MaybePromise<JobUniqueOptions | null | undefined>;
}["bivarianceHack"];

/**
 * Unique job declaration value.
 */
export type JobUniqueConfig<Payload extends StandardSchema = StandardSchema> =
  | JobUniqueOptions
  | JobUniqueResolver<Payload>;

/**
 * Resolved uniqueness metadata used by dispatchers.
 */
export interface ResolvedJobUniqueOptions {
  /**
   * Logical uniqueness key returned by the job declaration.
   */
  key: string;
  /**
   * Concrete lock key used by `LocksPort`.
   */
  lockKey: string;
  /**
   * Lease TTL in milliseconds.
   */
  ttlMs: number;
}

/**
 * Job definition created by `defineJob(...)`.
 */
export interface JobDef<
  Name extends string = string,
  Payload extends StandardSchema = StandardSchema,
  Ctx = unknown,
> {
  /**
   * Discriminator for job definitions.
   */
  readonly kind: "job";
  /**
   * Stable job name used by dispatchers and provider adapters.
   */
  readonly name: Name;
  /**
   * Standard Schema payload validator.
   */
  readonly payload: Payload;
  /**
   * Optional human-readable description for docs and tooling.
   */
  readonly description?: string;
  /**
   * Retry metadata for durable job providers.
   */
  readonly retry?: JobRetryOptions;
  /**
   * Optional dispatch-time uniqueness guard.
   */
  readonly unique?: JobUniqueConfig<Payload>;
  /**
   * Optional maximum execution time for each job handler attempt.
   */
  readonly timeout?: JobTimeoutDuration;
  /**
   * Optional hooks that wrap each handler attempt for this job.
   */
  readonly hooks?: readonly JobHook<JobDef<Name, Payload, Ctx>, Ctx>[];
  /**
   * Handle a parsed job payload.
   */
  handle(
    args: JobHandleArgs<JobDef<Name, Payload, Ctx>, Ctx>,
  ): MaybePromise<void>;
}

/**
 * Infer the parsed payload type for a job definition.
 */
export type InferJobPayload<J extends JobDef> =
  J["payload"] extends StandardSchemaV1<unknown, infer Output> ? Output : never;

/**
 * Arguments passed to a job handler.
 */
export interface JobHandleArgs<J extends JobDef, Ctx> {
  /**
   * Job definition being handled.
   */
  job: J;
  /**
   * Parsed job payload.
   */
  payload: InferJobPayload<J>;
  /** Handler context. */
  ctx: Ctx;
  /**
   * Abort signal that fires when the job's declared timeout expires.
   *
   * Beignet dispatchers and worker helpers provide this signal. It is optional
   * in the type so direct handler tests can stay lightweight.
   *
   * JavaScript cannot forcibly stop arbitrary async work, so handlers that
   * call cancellable APIs should pass this signal through.
   */
  signal?: AbortSignal;
}

/**
 * Arguments passed to job execution hooks.
 */
export interface JobHookArgs<J extends JobDef, Ctx>
  extends JobHandleArgs<J, Ctx> {
  /**
   * Abort signal provided by the Beignet execution runner.
   */
  signal: AbortSignal;
  /**
   * One-based execution attempt when the runner can report it.
   */
  attempt?: number;
  /**
   * Maximum execution attempts when the runner can report it.
   */
  maxAttempts?: number;
}

/**
 * Hook that wraps one job handler attempt.
 *
 * Hooks run only through Beignet dispatchers and worker helpers. Direct calls
 * to `job.handle(...)` bypass hooks, which keeps lightweight unit tests
 * possible.
 */
export type JobHook<J extends JobDef = JobDef, Ctx = unknown> = {
  bivarianceHack(
    args: JobHookArgs<J, Ctx>,
    next: () => Promise<void>,
  ): MaybePromise<void>;
}["bivarianceHack"];

/**
 * Arguments passed to an execution lease key resolver.
 */
export type JobExecutionLeaseResolverArgs<J extends JobDef, Ctx> = JobHookArgs<
  J,
  Ctx
>;

/**
 * Resolver for the logical execution lease key within a job name.
 */
export type JobExecutionLeaseKeyResolver<J extends JobDef, Ctx> = {
  bivarianceHack(
    args: JobExecutionLeaseResolverArgs<J, Ctx>,
  ): MaybePromise<string>;
}["bivarianceHack"];

/**
 * Resolver for the locks port used by an execution lease hook.
 */
export type JobExecutionLeaseLocksResolver<J extends JobDef, Ctx> =
  | LocksPort
  | {
      bivarianceHack(
        args: JobExecutionLeaseResolverArgs<J, Ctx>,
      ): MaybePromise<LocksPort>;
    }["bivarianceHack"];

/**
 * Resolver for optional diagnostics metadata attached to lease acquisition.
 */
export type JobExecutionLeaseMetadataResolver<J extends JobDef, Ctx> =
  | LeaseMetadata
  | {
      bivarianceHack(
        args: JobExecutionLeaseResolverArgs<J, Ctx>,
      ): MaybePromise<LeaseMetadata | undefined>;
    }["bivarianceHack"];

/**
 * Arguments passed when an execution lease cannot be acquired.
 */
export interface JobExecutionLeaseUnavailableArgs<J extends JobDef, Ctx>
  extends JobHookArgs<J, Ctx> {
  /**
   * Logical lease key returned by the hook configuration.
   */
  key: string;
  /**
   * Concrete lock key passed to `LocksPort`.
   */
  lockKey: string;
  /**
   * Acquisition failure reason returned by `LocksPort`.
   */
  reason: "unavailable" | "timeout";
}

/**
 * Behavior when an execution lease cannot be acquired.
 *
 * `"skip"` treats the attempt as successful without running the handler.
 * `"throw"` fails the attempt with `JobExecutionLeaseUnavailableError` so the
 * job retry policy can classify it. A function can log or throw custom errors;
 * returning from it skips the handler.
 */
export type JobExecutionLeaseUnavailableBehavior<J extends JobDef, Ctx> =
  | "skip"
  | "throw"
  | {
      bivarianceHack(
        args: JobExecutionLeaseUnavailableArgs<J, Ctx>,
      ): MaybePromise<void>;
    }["bivarianceHack"];

/**
 * Options for `createJobExecutionLeaseHook(...)`.
 */
export interface JobExecutionLeaseHookOptions<
  J extends JobDef = JobDef,
  Ctx = unknown,
> {
  /**
   * Locks port or resolver used to acquire the execution lease.
   */
  locks: JobExecutionLeaseLocksResolver<J, Ctx>;
  /**
   * Logical lease key within this job name.
   */
  key: string | JobExecutionLeaseKeyResolver<J, Ctx>;
  /**
   * Lease time-to-live. This is the real safety boundary in serverless runtimes
   * where an invocation may terminate before `finally` runs.
   */
  ttl: JobExecutionLeaseDuration;
  /**
   * How long to wait for an existing lease before applying `onUnavailable`.
   *
   * Defaults to no wait.
   */
  wait?: JobExecutionLeaseDuration;
  /**
   * Delay between acquisition attempts while waiting.
   */
  retryDelay?: JobExecutionLeaseDuration;
  /**
   * Prefix used to build the concrete lock key.
   *
   * Defaults to `"jobs:lease"`.
   */
  keyPrefix?: string;
  /**
   * Optional diagnostics metadata attached to the lease acquire call.
   */
  metadata?: JobExecutionLeaseMetadataResolver<J, Ctx>;
  /**
   * Behavior when the lease cannot be acquired.
   *
   * Defaults to `"skip"` so overlapping executions do not create retry storms.
   */
  onUnavailable?: JobExecutionLeaseUnavailableBehavior<J, Ctx>;
}

/**
 * Retry metadata that durable job providers can map to their own retry model.
 */
export interface JobRetryOptions {
  /**
   * Retry strategy. Raw objects without a strategy default to exponential
   * backoff so existing `{ attempts }` style definitions stay meaningful.
   */
  strategy?: JobRetryStrategy;
  /**
   * Maximum total attempts, including the first attempt.
   */
  attempts?: number;
  /**
   * Delay between attempts for fixed retry policies.
   */
  delay?: JobRetryDuration;
  /**
   * Initial delay for exponential retry policies.
   */
  initialDelay?: JobRetryDuration;
  /**
   * Maximum delay for exponential retry policies.
   */
  maxDelay?: JobRetryDuration;
  /**
   * Exponential multiplier. Defaults to `2`.
   */
  factor?: number;
  /**
   * Whether adapters that compute delays should add jitter.
   */
  jitter?: boolean;
  /**
   * Optional app-owned retry classifier.
   */
  retryIf?: JobRetryPredicate;
}

/**
 * Options for declaring a typed job.
 */
export interface DefineJobOptions<
  Name extends string,
  Payload extends StandardSchema,
  Ctx,
> {
  /**
   * Standard Schema payload validator.
   */
  payload: Payload;
  /**
   * Optional human-readable description for docs and tooling.
   */
  description?: string;
  /**
   * Retry metadata for durable job providers.
   */
  retry?: JobRetryOptions;
  /**
   * Optional dispatch-time uniqueness guard.
   */
  unique?: JobUniqueConfig<Payload>;
  /**
   * Optional maximum execution time for each job handler attempt.
   */
  timeout?: JobTimeoutDuration;
  /**
   * Optional hooks that wrap each handler attempt for this job.
   */
  hooks?: readonly JobHook<
    JobDef<NoInferJob<Name>, NoInferJob<Payload>, Ctx>,
    Ctx
  >[];
  /**
   * Handle a parsed job payload.
   */
  handle(
    args: JobHandleArgs<JobDef<Name, Payload, Ctx>, Ctx>,
  ): MaybePromise<void>;
}

/**
 * Options for a fixed job retry policy.
 */
export interface FixedJobRetryOptions {
  /**
   * Maximum total attempts, including the first attempt.
   */
  attempts: number;
  /**
   * Delay between attempts.
   */
  delay: JobRetryDuration;
  /**
   * Optional app-owned retry classifier.
   */
  retryIf?: JobRetryPredicate;
}

/**
 * Options for an exponential job retry policy.
 */
export interface ExponentialJobRetryOptions {
  /**
   * Maximum total attempts, including the first attempt.
   */
  attempts: number;
  /**
   * Initial delay. Defaults to `1s`.
   */
  initialDelay?: JobRetryDuration;
  /**
   * Maximum delay. Defaults to `1m`.
   */
  maxDelay?: JobRetryDuration;
  /**
   * Exponential multiplier. Defaults to `2`.
   */
  factor?: number;
  /**
   * Whether computed delays should include jitter.
   */
  jitter?: boolean;
  /**
   * Optional app-owned retry classifier.
   */
  retryIf?: JobRetryPredicate;
}

/**
 * Retry helper namespace for job definitions.
 */
export const retry = {
  /**
   * Disable retries. The first failure is terminal.
   */
  none(): JobRetryOptions {
    return {
      strategy: "none",
      attempts: 1,
    };
  },

  /**
   * Retry with the same delay between attempts.
   */
  fixed(options: FixedJobRetryOptions): JobRetryOptions {
    return validateJobRetryOptions({
      strategy: "fixed",
      attempts: options.attempts,
      delay: options.delay,
      retryIf: options.retryIf,
    });
  },

  /**
   * Retry with exponential backoff.
   */
  exponential(options: ExponentialJobRetryOptions): JobRetryOptions {
    return validateJobRetryOptions({
      strategy: "exponential",
      attempts: options.attempts,
      initialDelay: options.initialDelay,
      maxDelay: options.maxDelay,
      factor: options.factor,
      jitter: options.jitter,
      retryIf: options.retryIf,
    });
  },
} as const;

/**
 * Options for the inline job dispatcher.
 */
export interface InlineJobDispatcherOptions<Ctx> {
  /**
   * Static job context or factory evaluated for each dispatched job.
   */
  ctx?: Ctx | (() => MaybePromise<Ctx>);
  /**
   * Called when a dispatched inline job fails all attempts allowed by its
   * retry policy. When omitted, the final error is rethrown to the caller.
   */
  onError?: (error: unknown, job: JobDef<string, StandardSchema, Ctx>) => void;
  /**
   * Sleep implementation used between retry attempts. Defaults to a real
   * `setTimeout` delay; inject a fake in tests to keep retries instant.
   */
  sleep?: (ms: number) => Promise<void>;
  /**
   * Honor the job's declared retry policy inline. Defaults to `true`. Set
   * `false` when another layer owns execution retries for every dispatch
   * through this dispatcher.
   */
  retry?: boolean;
  /**
   * Hooks that wrap every job attempt executed by this dispatcher. Runner hooks
   * wrap job-local hooks.
   */
  hooks?: readonly JobHook<JobDef<string, StandardSchema, Ctx>, Ctx>[];
}

/** Metadata propagated when a job is dispatched. */
export interface JobDispatchOptions {
  /** Versioned trace context captured by the job producer. */
  trace?: TraceCarrier;
}

const JOB_TRANSPORT_ENVELOPE_TYPE = "beignet.job";
const JOB_TRANSPORT_ENVELOPE_VERSION = 1;

/** Parsed payload and propagation metadata from a job transport envelope. */
export interface ParsedJobTransportEnvelope {
  payload: unknown;
  trace?: TraceCarrier;
}

/**
 * Wrap a job payload with transport metadata when a trace is present.
 * Payloads without metadata retain their legacy wire shape.
 */
export function createJobTransportEnvelope(
  payload: unknown,
  options?: JobDispatchOptions,
): unknown {
  const trace = parseTraceCarrier(options?.trace);
  if (!trace) return payload;

  return {
    __beignet: {
      type: JOB_TRANSPORT_ENVELOPE_TYPE,
      version: JOB_TRANSPORT_ENVELOPE_VERSION,
      trace,
    },
    payload,
  };
}

/**
 * Decode a Beignet job transport envelope while accepting legacy raw payloads.
 * Unknown or malformed trace metadata is ignored without dropping the payload.
 */
export function parseJobTransportEnvelope(
  value: unknown,
): ParsedJobTransportEnvelope {
  if (typeof value !== "object" || value === null || !("payload" in value)) {
    return { payload: value };
  }

  const metadata = "__beignet" in value ? value.__beignet : undefined;
  if (
    typeof metadata !== "object" ||
    metadata === null ||
    !("type" in metadata) ||
    metadata.type !== JOB_TRANSPORT_ENVELOPE_TYPE ||
    !("version" in metadata) ||
    metadata.version !== JOB_TRANSPORT_ENVELOPE_VERSION
  ) {
    return { payload: value };
  }

  const trace =
    "trace" in metadata ? parseTraceCarrier(metadata.trace) : undefined;
  return {
    payload: value.payload,
    ...(trace ? { trace } : {}),
  };
}

/**
 * Well-known symbol under which the inline dispatcher exposes a
 * single-attempt dispatch. Delivery systems that own execution retries
 * themselves — the outbox drain — call it instead of `dispatch(...)` so a
 * job's retry policy runs in exactly one layer. Registered with `Symbol.for`
 * so multiple core copies in one process agree on the key.
 */
export const SINGLE_ATTEMPT_DISPATCH: unique symbol = Symbol.for(
  "beignet.jobs.singleAttemptDispatch",
);

/**
 * Metadata passed to a single-attempt dispatch when another delivery layer
 * owns retry scheduling.
 */
export interface SingleAttemptJobDispatchOptions extends JobDispatchOptions {
  /**
   * One-based delivery attempt.
   */
  attempt?: number;
  /**
   * Maximum delivery attempts.
   */
  maxAttempts?: number;
}

/**
 * Shape of the single-attempt dispatch exposed under
 * `SINGLE_ATTEMPT_DISPATCH`.
 */
export type SingleAttemptJobDispatch = <J extends JobDef>(
  job: J,
  payload: InferJobPayload<J>,
  options?: SingleAttemptJobDispatchOptions,
) => Promise<void>;

/**
 * Job dispatcher shape accepted by job wrappers.
 */
export interface JobDispatcher {
  dispatch<J extends JobDef>(
    job: J,
    payload: InferJobPayload<J>,
    options?: JobDispatchOptions,
  ): MaybePromise<void>;
}

/**
 * Arguments passed when a unique job dispatch is suppressed.
 */
export interface UniqueJobDuplicateArgs<J extends JobDef = JobDef> {
  /**
   * Job definition whose dispatch was suppressed.
   */
  job: J;
  /**
   * Parsed job payload.
   */
  payload: InferJobPayload<J>;
  /**
   * Logical uniqueness key returned by the job declaration.
   */
  key: string;
  /**
   * Concrete lock key used by `LocksPort`.
   */
  lockKey: string;
  /**
   * Lease TTL in milliseconds.
   */
  ttlMs: number;
  /**
   * Lock acquisition reason returned by `LocksPort`.
   */
  reason: "unavailable" | "timeout";
}

/**
 * Options for `createUniqueJobDispatcher(...)`.
 */
export interface UniqueJobDispatcherOptions {
  /**
   * Dispatcher to call when a job is not unique or the unique lease is acquired.
   */
  jobs: JobDispatcher;
  /**
   * Lease-backed lock port used as the uniqueness guard.
   */
  locks: LocksPort;
  /**
   * Prefix for concrete lock keys.
   *
   * Defaults to `"jobs:unique"`.
   */
  keyPrefix?: string;
  /**
   * Called when a duplicate dispatch is suppressed.
   */
  onDuplicate?: (args: UniqueJobDuplicateArgs) => MaybePromise<void>;
}

/**
 * Local/test job dispatcher that executes job handlers inline.
 */
export interface InlineJobDispatcher<Ctx = unknown> {
  /**
   * Validate a payload and run the job handler inline, honoring the job's
   * declared retry policy before failing.
   */
  dispatch<J extends JobDef<string, StandardSchema, Ctx>>(
    job: J,
    payload: InferJobPayload<J>,
    options?: JobDispatchOptions,
  ): Promise<void>;
}

/**
 * Context-bound job helper factory.
 */
export interface Jobs<Ctx> {
  /**
   * Define a job with the bound context type.
   */
  defineJob<Name extends string, Payload extends StandardSchema>(
    name: Name,
    options: DefineJobOptions<Name, Payload, Ctx>,
  ): JobDef<Name, Payload, Ctx>;
}

/**
 * Error thrown when job payload validation fails.
 */
export class JobValidationError extends Error {
  /**
   * Raw Standard Schema validation issues.
   */
  readonly issues: readonly StandardSchemaV1.Issue[];

  constructor(args: {
    name: string;
    issues: readonly StandardSchemaV1.Issue[];
  }) {
    super(
      `Job "${args.name}" payload validation failed: ${formatIssues(args.issues)}`,
    );
    this.name = "JobValidationError";
    this.issues = args.issues;
  }
}

/**
 * Error thrown when a job handler exceeds its declared timeout.
 */
export class JobTimeoutError extends Error {
  /**
   * Stable job name that timed out.
   */
  readonly jobName: string;
  /**
   * Timeout in milliseconds.
   */
  readonly timeoutMs: number;

  constructor(args: { jobName: string; timeoutMs: number }) {
    super(`Job "${args.jobName}" timed out after ${args.timeoutMs}ms.`);
    this.name = "JobTimeoutError";
    this.jobName = args.jobName;
    this.timeoutMs = args.timeoutMs;
  }
}

/**
 * Error thrown when an execution lease hook is configured to fail on an
 * unavailable lease.
 */
export class JobExecutionLeaseUnavailableError extends Error {
  /**
   * Stable job name whose execution lease was unavailable.
   */
  readonly jobName: string;
  /**
   * Logical lease key returned by the hook configuration.
   */
  readonly key: string;
  /**
   * Concrete lock key passed to `LocksPort`.
   */
  readonly lockKey: string;
  /**
   * Acquisition failure reason returned by `LocksPort`.
   */
  readonly reason: "unavailable" | "timeout";

  constructor(args: {
    jobName: string;
    key: string;
    lockKey: string;
    reason: "unavailable" | "timeout";
  }) {
    super(
      `Job "${args.jobName}" execution lease "${args.key}" is unavailable (${args.reason}).`,
    );
    this.name = "JobExecutionLeaseUnavailableError";
    this.jobName = args.jobName;
    this.key = args.key;
    this.lockKey = args.lockKey;
    this.reason = args.reason;
  }
}

function formatPath(path: StandardSchemaV1.Issue["path"]): string {
  if (!path?.length) return "";

  return path
    .map((segment) =>
      typeof segment === "object" && segment !== null && "key" in segment
        ? String(segment.key)
        : String(segment),
    )
    .join(".");
}

function formatIssues(issues: readonly StandardSchemaV1.Issue[]): string {
  return issues
    .map((issue) => {
      const path = formatPath(issue.path);
      return path ? `${path}: ${issue.message}` : issue.message;
    })
    .join("; ");
}

function assertPositiveInteger(name: string, value: number): void {
  if (!Number.isInteger(value) || value <= 0) {
    throw new Error(`${name} must be a positive integer`);
  }
}

function assertPositiveNumber(name: string, value: number): void {
  if (!Number.isFinite(value) || value <= 0) {
    throw new Error(`${name} must be a positive number`);
  }
}

function assertNonEmptyString(name: string, value: string): void {
  if (typeof value !== "string" || value.trim().length === 0) {
    throw new Error(`${name} must be a non-empty string`);
  }
}

function durationToMs(name: string, value: JobDuration | unknown): number {
  if (typeof value === "number") {
    assertPositiveInteger(name, value);
    return value;
  }

  if (typeof value !== "string") {
    throw new Error(
      `${name} must be a positive millisecond value or duration string like "500ms", "30s", "5m", or "1h".`,
    );
  }

  const match = /^(\d+)(ms|s|m|h)$/.exec(value);
  if (!match) {
    throw new Error(
      `${name} must be a positive millisecond value or duration string like "500ms", "30s", "5m", or "1h".`,
    );
  }

  const amount = Number(match[1]);
  assertPositiveInteger(name, amount);

  switch (match[2]) {
    case "ms":
      return amount;
    case "s":
      return amount * 1000;
    case "m":
      return amount * 60_000;
    case "h":
      return amount * 3_600_000;
    default:
      throw new Error(`${name} has an unsupported duration unit.`);
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function validateJobUniqueOptions(options: JobUniqueOptions): JobUniqueOptions {
  if (!isRecord(options)) {
    throw new Error("unique must be an object or resolver function");
  }

  assertNonEmptyString("unique.key", options.key);
  durationToMs("unique.ttl", options.ttl);
  return options;
}

function validateJobUniqueConfig<Payload extends StandardSchema>(
  config: JobUniqueConfig<Payload> | undefined,
): JobUniqueConfig<Payload> | undefined {
  if (config === undefined) return undefined;
  if (typeof config === "function") return config;
  return validateJobUniqueOptions(config);
}

function validateJobTimeout(
  timeout: JobTimeoutDuration | undefined,
): JobTimeoutDuration | undefined {
  if (timeout === undefined) return undefined;
  durationToMs("timeout", timeout);
  return timeout;
}

const VALIDATED_RETRY_OPTIONS = Symbol("beignet.jobs.validatedRetryOptions");

function validateJobRetryOptions(options: JobRetryOptions): JobRetryOptions {
  // Options returned by this function are branded so repeated validation —
  // per attempt in the retry helpers — short-circuits instead of re-parsing
  // duration strings and reallocating.
  if (
    (options as { [VALIDATED_RETRY_OPTIONS]?: boolean })[
      VALIDATED_RETRY_OPTIONS
    ]
  ) {
    return options;
  }

  const strategy = options.strategy ?? "exponential";

  if (!["none", "fixed", "exponential"].includes(strategy)) {
    throw new Error("retry.strategy must be none, fixed, or exponential");
  }

  const attempts = options.attempts ?? (strategy === "none" ? 1 : undefined);
  if (attempts === undefined) {
    throw new Error("retry.attempts is required");
  }
  assertPositiveInteger("retry.attempts", attempts);

  if (strategy === "none" && attempts !== 1) {
    throw new Error("retry.none() must use exactly one attempt");
  }

  if (strategy === "fixed") {
    if (options.delay === undefined) {
      throw new Error("retry.delay is required for fixed retry policies");
    }
    durationToMs("retry.delay", options.delay);
  }

  if (strategy === "exponential") {
    if (options.initialDelay !== undefined) {
      durationToMs("retry.initialDelay", options.initialDelay);
    }
    if (options.maxDelay !== undefined) {
      durationToMs("retry.maxDelay", options.maxDelay);
    }
    if (options.factor !== undefined) {
      assertPositiveNumber("retry.factor", options.factor);
    }
  }

  const validated = {
    ...options,
    strategy,
    attempts,
  };
  Object.defineProperty(validated, VALIDATED_RETRY_OPTIONS, {
    value: true,
    enumerable: false,
  });
  return validated;
}

/**
 * Return the maximum total attempts configured by a retry policy.
 */
export function getJobRetryMaxAttempts(
  options: JobRetryOptions | undefined,
): number | undefined {
  return options ? validateJobRetryOptions(options).attempts : undefined;
}

/**
 * Return whether a failed job attempt should be retried.
 */
export function shouldRetryJob(
  options: JobRetryOptions | undefined,
  args: JobRetryPredicateArgs,
): boolean {
  if (!options) return args.attempt < args.maxAttempts;

  const retryOptions = validateJobRetryOptions(options);
  const maxAttempts = Math.min(
    args.maxAttempts,
    retryOptions.attempts ?? args.maxAttempts,
  );
  if (retryOptions.strategy === "none") return false;
  if (args.attempt >= maxAttempts) return false;

  return retryOptions.retryIf?.({ ...args, maxAttempts }) ?? true;
}

/**
 * Compute the next retry delay in milliseconds for a failed job attempt.
 */
export function getJobRetryDelayMs(
  options: JobRetryOptions | undefined,
  args: Pick<JobRetryPredicateArgs, "attempt" | "error" | "jobName">,
): number {
  const retryOptions = options
    ? validateJobRetryOptions(options)
    : retry.exponential({ attempts: 3 });

  let delayMs: number;
  if (retryOptions.strategy === "fixed") {
    delayMs = durationToMs("retry.delay", retryOptions.delay ?? "1s");
  } else if (retryOptions.strategy === "none") {
    delayMs = 0;
  } else {
    const initialDelayMs = durationToMs(
      "retry.initialDelay",
      retryOptions.initialDelay ?? "1s",
    );
    const maxDelayMs = durationToMs(
      "retry.maxDelay",
      retryOptions.maxDelay ?? "1m",
    );
    const factor = retryOptions.factor ?? 2;
    delayMs = Math.min(
      maxDelayMs,
      initialDelayMs * factor ** Math.max(0, args.attempt - 1),
    );
  }

  if (retryOptions.jitter && delayMs > 0) {
    delayMs = Math.ceil(delayMs * (0.5 + Math.random()));
    if (retryOptions.strategy === "exponential") {
      delayMs = Math.min(
        delayMs,
        durationToMs("retry.maxDelay", retryOptions.maxDelay ?? "1m"),
      );
    }
  }

  return delayMs;
}

/**
 * Return the execution timeout in milliseconds configured by a job.
 */
export function getJobTimeoutMs(
  job: Pick<JobDef, "timeout">,
): number | undefined {
  return job.timeout === undefined
    ? undefined
    : durationToMs("timeout", job.timeout);
}

function jobExecutionLeaseLockKey(
  jobName: string,
  key: string,
  keyPrefix = "jobs:lease",
): string {
  return `${keyPrefix}:${jobName}:${key}`;
}

function validateExecutionLeaseKey(key: string): string {
  assertNonEmptyString("executionLease.key", key);
  return key;
}

function abortReason(signal: AbortSignal): unknown {
  return signal.reason ?? new Error("Job execution aborted.");
}

async function resolveExecutionLeaseLocks<J extends JobDef, Ctx>(
  locks: JobExecutionLeaseLocksResolver<J, Ctx>,
  args: JobExecutionLeaseResolverArgs<J, Ctx>,
): Promise<LocksPort> {
  return typeof locks === "function" ? await locks(args) : locks;
}

async function resolveExecutionLeaseMetadata<J extends JobDef, Ctx>(
  metadata: JobExecutionLeaseMetadataResolver<J, Ctx> | undefined,
  args: JobExecutionLeaseResolverArgs<J, Ctx>,
): Promise<LeaseMetadata | undefined> {
  return typeof metadata === "function" ? await metadata(args) : metadata;
}

async function releaseExecutionLease(lease: LeaseHandle): Promise<void> {
  try {
    await lease.release();
  } catch {
    // The lease TTL is the correctness boundary. Release is best effort so a
    // provider outage after handler side effects does not turn success into a
    // retry.
  }
}

async function handleUnavailableExecutionLease<J extends JobDef, Ctx>(
  behavior: JobExecutionLeaseUnavailableBehavior<J, Ctx> | undefined,
  args: JobExecutionLeaseUnavailableArgs<J, Ctx>,
): Promise<void> {
  if (behavior === undefined || behavior === "skip") return;

  if (behavior === "throw") {
    throw new JobExecutionLeaseUnavailableError({
      jobName: args.job.name,
      key: args.key,
      lockKey: args.lockKey,
      reason: args.reason,
    });
  }

  await behavior(args);
}

/**
 * Create a job hook that prevents overlapping handler attempts for the same
 * logical execution key.
 *
 * The hook uses one bounded `LocksPort.acquire(...)` call and never starts
 * renewal loops, so it can run in serverless entrypoints as long as `locks`
 * points at shared storage. `ttl` is the real safety boundary when a runtime
 * terminates before best-effort release runs.
 */
export function createJobExecutionLeaseHook<
  J extends JobDef = JobDef,
  Ctx = unknown,
>(options: JobExecutionLeaseHookOptions<J, Ctx>): JobHook<J, Ctx> {
  const ttlMs = durationToMs("executionLease.ttl", options.ttl);
  const waitMs =
    options.wait === undefined
      ? undefined
      : durationToMs("executionLease.wait", options.wait);
  const retryDelayMs =
    options.retryDelay === undefined
      ? undefined
      : durationToMs("executionLease.retryDelay", options.retryDelay);
  const keyPrefix = options.keyPrefix ?? "jobs:lease";
  assertNonEmptyString("executionLease.keyPrefix", keyPrefix);

  if (typeof options.key === "string") {
    validateExecutionLeaseKey(options.key);
  }

  return async (args, next) => {
    if (args.signal.aborted) throw abortReason(args.signal);

    const key = validateExecutionLeaseKey(
      typeof options.key === "function" ? await options.key(args) : options.key,
    );
    if (args.signal.aborted) throw abortReason(args.signal);

    const lockKey = jobExecutionLeaseLockKey(args.job.name, key, keyPrefix);
    const locks = await resolveExecutionLeaseLocks(options.locks, args);
    if (args.signal.aborted) throw abortReason(args.signal);

    const metadata = await resolveExecutionLeaseMetadata(
      options.metadata,
      args,
    );
    if (args.signal.aborted) throw abortReason(args.signal);

    const result = await locks.acquire(lockKey, {
      ttlMs,
      ...(waitMs === undefined ? {} : { waitMs }),
      ...(retryDelayMs === undefined ? {} : { retryDelayMs }),
      metadata: {
        ...(metadata ?? {}),
        capability: "jobs",
        jobName: args.job.name,
        leaseKey: key,
        attempt: args.attempt ?? null,
        maxAttempts: args.maxAttempts ?? null,
      },
    });

    if (!result.acquired) {
      if (args.signal.aborted) throw abortReason(args.signal);

      await handleUnavailableExecutionLease(options.onUnavailable, {
        ...args,
        key,
        lockKey,
        reason: result.reason,
      });
      return;
    }

    if (args.signal.aborted) {
      await releaseExecutionLease(result.lease);
      throw abortReason(args.signal);
    }

    try {
      await next();
    } finally {
      await releaseExecutionLease(result.lease);
    }
  };
}

async function parsePayload<Schema extends StandardSchemaV1>(
  schema: Schema,
  input: unknown,
  args: { name: string },
): Promise<InferSchemaOutput<Schema>> {
  const result = await schema["~standard"].validate(input);

  if (result.issues?.length) {
    throw new JobValidationError({
      name: args.name,
      issues: result.issues,
    });
  }

  if ("value" in result) {
    return result.value as InferSchemaOutput<Schema>;
  }

  throw new Error("Invalid Standard Schema result: missing value");
}

async function resolveCtx<Ctx>(
  ctx: Ctx | (() => MaybePromise<Ctx>) | undefined,
): Promise<Ctx> {
  if (typeof ctx === "function") {
    return (ctx as () => MaybePromise<Ctx>)();
  }

  return ctx as Ctx;
}

function defineJobImpl<
  Name extends string,
  Payload extends StandardSchema,
  Ctx = unknown,
>(
  name: Name,
  options: DefineJobOptions<Name, Payload, Ctx>,
): JobDef<Name, Payload, Ctx> {
  const retryOptions = options.retry
    ? validateJobRetryOptions(options.retry)
    : undefined;
  const uniqueOptions = validateJobUniqueConfig(options.unique);
  const timeout = validateJobTimeout(options.timeout);

  return {
    kind: "job",
    name,
    payload: options.payload,
    description: options.description,
    retry: retryOptions,
    unique: uniqueOptions,
    timeout,
    hooks: options.hooks,
    handle: options.handle as JobDef<Name, Payload, Ctx>["handle"],
  };
}

/**
 * Validate and parse a job payload with the job's Standard Schema.
 */
export async function parseJobPayload<J extends JobDef>(
  job: J,
  payload: unknown,
): Promise<InferJobPayload<J>> {
  return (await parsePayload(job.payload, payload, {
    name: job.name,
  })) as InferJobPayload<J>;
}

/**
 * Options for running one parsed job handler attempt.
 */
export interface RunJobHandlerOptions<
  J extends JobDef<string, StandardSchema, Ctx>,
  Ctx,
> {
  /**
   * Job definition to execute.
   */
  job: J;
  /**
   * Parsed job payload.
   */
  payload: InferJobPayload<J>;
  /** Handler context or factory resolved inside the job span. */
  ctx: Ctx | (() => MaybePromise<Ctx>);
  /** Runtime tracing port used before a lazy context factory runs. */
  tracing?: TracingPort;
  /** Trace context captured by the dispatching process. */
  trace?: TraceCarrier;
  /**
   * Runner-level hooks. These wrap job-local hooks.
   */
  hooks?: readonly JobHook<JobDef<string, StandardSchema, Ctx>, Ctx>[];
  /**
   * One-based execution attempt when the runner can report it.
   */
  attempt?: number;
  /**
   * Maximum execution attempts when the runner can report it.
   */
  maxAttempts?: number;
}

function normalizeJobHooks<Ctx>(
  job: JobDef<string, StandardSchema, Ctx>,
  hooks: readonly JobHook<JobDef<string, StandardSchema, Ctx>, Ctx>[] = [],
): readonly JobHook<JobDef<string, StandardSchema, Ctx>, Ctx>[] {
  return [
    ...hooks,
    ...((job.hooks ?? []) as readonly JobHook<
      JobDef<string, StandardSchema, Ctx>,
      Ctx
    >[]),
  ];
}

/**
 * Run a parsed job handler once, enforcing hooks and the job's declared
 * timeout.
 */
export async function runJobHandler<
  J extends JobDef<string, StandardSchema, Ctx>,
  Ctx,
>(args: RunJobHandlerOptions<J, Ctx>): Promise<void> {
  const traceAttributes = {
    "beignet.job.name": args.job.name,
    ...(args.attempt === undefined
      ? {}
      : { "beignet.job.attempt": args.attempt }),
    ...(args.maxAttempts === undefined
      ? {}
      : { "beignet.job.max_attempts": args.maxAttempts }),
  } as const;

  await runWithResolvedTracingContext({
    tracing: args.tracing,
    ctx: args.ctx,
    operation: {
      name: `beignet.job ${args.job.name}`,
      type: "job",
      kind: "consumer",
      parent: parseTraceCarrier(args.trace),
      attributes: traceAttributes,
      metricAttributes: traceAttributes,
    },
    run: async (ctx) => {
      const timeoutMs = getJobTimeoutMs(args.job);
      const controller = new AbortController();
      const hooks = normalizeJobHooks(args.job, args.hooks);
      const hookArgs = {
        job: args.job,
        payload: args.payload,
        ctx,
        signal: controller.signal,
        attempt: args.attempt,
        maxAttempts: args.maxAttempts,
      } satisfies JobHookArgs<J, Ctx>;

      const run = Promise.resolve().then(async () => {
        let index = -1;
        const dispatch = async (nextIndex: number): Promise<void> => {
          if (nextIndex <= index) {
            throw new Error(
              `Job "${args.job.name}" hook called next() multiple times.`,
            );
          }
          index = nextIndex;

          const hook = hooks[nextIndex];
          if (!hook) {
            await args.job.handle({
              job: args.job,
              payload: args.payload,
              ctx,
              signal: controller.signal,
            });
            return;
          }

          await hook(hookArgs, () => dispatch(nextIndex + 1));
        };

        await dispatch(0);
      });

      if (timeoutMs === undefined) {
        await run;
        return;
      }

      const timeoutError = new JobTimeoutError({
        jobName: args.job.name,
        timeoutMs,
      });
      let timeout: ReturnType<typeof setTimeout> | undefined;

      try {
        await Promise.race([
          run,
          new Promise<void>((_, reject) => {
            timeout = setTimeout(() => {
              controller.abort(timeoutError);
              reject(timeoutError);
            }, timeoutMs);
          }),
        ]);
      } finally {
        if (timeout !== undefined) {
          clearTimeout(timeout);
        }
      }
    },
  });
}

function jobUniqueLockKey(
  jobName: string,
  key: string,
  keyPrefix = "jobs:unique",
): string {
  return `${keyPrefix}:${jobName}:${key}`;
}

/**
 * Resolve a job's dispatch-time uniqueness metadata for a parsed payload.
 */
export async function resolveJobUnique<J extends JobDef>(
  job: J,
  payload: InferJobPayload<J>,
  options: { keyPrefix?: string } = {},
): Promise<ResolvedJobUniqueOptions | undefined> {
  const config = job.unique;
  if (!config) return undefined;

  const unique =
    typeof config === "function"
      ? await config({ jobName: job.name, payload })
      : config;
  if (unique == null) return undefined;

  const validated = validateJobUniqueOptions(unique);
  return {
    key: validated.key,
    lockKey: jobUniqueLockKey(job.name, validated.key, options.keyPrefix),
    ttlMs: durationToMs("unique.ttl", validated.ttl),
  };
}

/**
 * Create a local/test dispatcher that runs job handlers inline.
 *
 * Dispatch honors the job's declared retry policy: failed attempts retry with
 * the policy's delays until the policy is exhausted. Payload validation
 * failures never retry. Jobs without a retry policy run exactly once.
 */
export function createInlineJobDispatcher<Ctx>(
  options: InlineJobDispatcherOptions<Ctx> = {},
): InlineJobDispatcher<Ctx> {
  const sleep =
    options.sleep ??
    ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));

  async function run<J extends JobDef<string, StandardSchema, Ctx>>(
    job: J,
    payload: InferJobPayload<J>,
    retryEnabled: boolean,
    dispatchOptions: SingleAttemptJobDispatchOptions = {},
    propagateFailure = false,
  ): Promise<void> {
    const fail = (error: unknown): void => {
      try {
        options.onError?.(error, job);
      } catch (observerError) {
        if (!propagateFailure) throw observerError;
      }
      if (propagateFailure || !options.onError) throw error;
    };

    let parsed: InferJobPayload<J>;
    let ctx: Ctx;
    try {
      parsed = await parseJobPayload(job, payload);
      // Resolve once per dispatch so a ctx factory does not run (and cannot
      // produce different contexts) across retry attempts.
      ctx = await resolveCtx(options.ctx);
    } catch (error) {
      fail(error);
      return;
    }

    const policy = retryEnabled ? job.retry : undefined;
    const maxAttempts = policy ? (getJobRetryMaxAttempts(policy) ?? 1) : 1;

    for (let attempt = 1; ; attempt += 1) {
      try {
        await runJobHandler({
          job,
          payload: parsed,
          ctx,
          hooks: options.hooks,
          trace: dispatchOptions.trace,
          attempt: dispatchOptions.attempt ?? attempt,
          maxAttempts: dispatchOptions.maxAttempts ?? maxAttempts,
        });
        return;
      } catch (error) {
        const willRetry = shouldRetryJob(policy, {
          error,
          attempt,
          maxAttempts,
          jobName: job.name,
        });
        if (!willRetry) {
          fail(error);
          return;
        }

        const delayMs = getJobRetryDelayMs(policy, {
          error,
          attempt,
          jobName: job.name,
        });
        if (delayMs > 0) await sleep(delayMs);
      }
    }
  }

  const dispatcher: InlineJobDispatcher<Ctx> = {
    async dispatch<J extends JobDef<string, StandardSchema, Ctx>>(
      job: J,
      payload: InferJobPayload<J>,
      dispatchOptions?: JobDispatchOptions,
    ) {
      await run(job, payload, options.retry !== false, dispatchOptions);
    },
  };

  // Non-enumerable so spreads and serialization keep treating the dispatcher
  // as a plain port; the outbox drain discovers it by symbol.
  Object.defineProperty(dispatcher, SINGLE_ATTEMPT_DISPATCH, {
    value: <J extends JobDef<string, StandardSchema, Ctx>>(
      job: J,
      payload: InferJobPayload<J>,
      dispatchOptions?: SingleAttemptJobDispatchOptions,
    ) => run(job, payload, false, dispatchOptions, true),
    enumerable: false,
  });

  return dispatcher;
}

/**
 * Wrap any job dispatcher with dispatch-time unique job suppression.
 *
 * When a job has no `unique` declaration, dispatch passes through unchanged.
 * When it does, the wrapper validates the payload, resolves the unique key,
 * acquires the matching lease, and calls the underlying dispatcher only when
 * the lease is acquired. Successful dispatches intentionally keep the lease
 * until its TTL expires; failed dispatches release it so callers can retry.
 */
export function createUniqueJobDispatcher(
  options: UniqueJobDispatcherOptions,
): JobDispatcher {
  const dispatcher: JobDispatcher = {
    async dispatch<J extends JobDef>(
      job: J,
      payload: InferJobPayload<J>,
      dispatchOptions?: JobDispatchOptions,
    ): Promise<void> {
      if (!job.unique) {
        await options.jobs.dispatch(job, payload, dispatchOptions);
        return;
      }

      const parsed = await parseJobPayload(job, payload);
      const unique = await resolveJobUnique(job, parsed, {
        keyPrefix: options.keyPrefix,
      });
      if (!unique) {
        await options.jobs.dispatch(job, payload, dispatchOptions);
        return;
      }

      const result = await options.locks.acquire(unique.lockKey, {
        ttlMs: unique.ttlMs,
        waitMs: 0,
        metadata: {
          capability: "jobs",
          jobName: job.name,
          uniqueKey: unique.key,
        },
      });
      if (!result.acquired) {
        await options.onDuplicate?.({
          job,
          payload: parsed,
          key: unique.key,
          lockKey: unique.lockKey,
          ttlMs: unique.ttlMs,
          reason: result.reason,
        });
        return;
      }

      try {
        await options.jobs.dispatch(job, payload, dispatchOptions);
      } catch (error) {
        try {
          await result.lease.release();
        } catch {
          // Preserve the dispatch failure; lease release is best effort and
          // the TTL still bounds duplicate suppression if release fails.
        }
        throw error;
      }
    },
  };

  const singleAttempt = (
    options.jobs as {
      [SINGLE_ATTEMPT_DISPATCH]?: SingleAttemptJobDispatch;
    }
  )[SINGLE_ATTEMPT_DISPATCH];

  if (singleAttempt) {
    Object.defineProperty(dispatcher, SINGLE_ATTEMPT_DISPATCH, {
      value: singleAttempt,
      enumerable: false,
    });
  }

  return dispatcher;
}

/**
 * Create job helper methods bound to an application context type.
 *
 * Call it once in `lib/jobs.ts`:
 *
 * ```ts
 * export const { defineJob } = createJobs<AppContext>();
 * ```
 *
 * Retry options describe the job's retry policy. Inline dispatchers run the
 * policy in-process with real delays; durable providers map the policy onto
 * their own runtime and reject options they cannot honor.
 */
export function createJobs<Ctx>(): Jobs<Ctx> {
  return {
    defineJob<Name extends string, Payload extends StandardSchema>(
      name: Name,
      options: DefineJobOptions<Name, Payload, Ctx>,
    ): JobDef<Name, Payload, Ctx> {
      return defineJobImpl(name, options);
    },
  };
}
