import type { StandardSchemaV1 } from "@standard-schema/spec";
import {
  createJobs,
  type JobDef,
  type JobDispatcher,
  type JobHook,
  type JobRetryOptions,
  type JobTimeoutDuration,
  retry,
} from "../jobs/index.js";
import type { SendMailOptions } from "../mail/index.js";
import type { ProviderInstrumentationTarget } from "../providers/index.js";
import {
  createProvider,
  createProviderInstrumentation,
} from "../providers/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>;

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

/**
 * Minimal notification definition shape accepted by notification ports.
 */
export interface NotificationPayloadDef<
  Name extends string = string,
  Payload extends StandardSchema = StandardSchema,
> {
  /**
   * Stable notification name used by dispatchers, tests, and tooling.
   */
  readonly name: Name;
  /**
   * Standard Schema payload validator.
   */
  readonly payload: Payload;
  /**
   * Optional human-readable description for docs and tooling.
   */
  readonly description?: string;
}

/**
 * Infer the parsed payload type for a notification definition.
 */
export type InferNotificationPayload<N extends NotificationPayloadDef> =
  N["payload"] extends StandardSchemaV1<unknown, infer Output> ? Output : never;

/**
 * Result for one notification channel.
 */
export interface NotificationChannelResult {
  /**
   * Channel name, such as `email`, `sms`, `push`, or `inApp`.
   */
  channel: string;
  /**
   * Delivery outcome for this channel.
   */
  status: "queued" | "sent" | "skipped" | "failed";
  /**
   * Provider delivery ID when available.
   */
  id?: string;
  /**
   * Provider name when available.
   */
  provider?: string;
  /**
   * Human-readable skip or failure reason.
   */
  reason?: string;
  /**
   * Channel-specific metadata. Dispatchers should keep this safe to log.
   */
  details?: Record<string, unknown>;
}

/**
 * Original error captured for one failed notification channel.
 */
export interface NotificationChannelError {
  channel: string;
  error: unknown;
}

/**
 * Arguments passed to a notification channel handler.
 */
export interface NotificationChannelHandleArgs<
  Payload extends StandardSchema,
  Ctx,
> {
  /**
   * Notification definition being delivered.
   */
  notification: NotificationDef<string, Payload, Ctx>;
  /**
   * Parsed notification payload.
   */
  payload: InferSchemaOutput<Payload>;
  /**
   * Handler context.
   */
  ctx: Ctx;
  /**
   * Channel name being delivered.
   */
  channel: string;
}

/**
 * Handler for one notification channel.
 */
export type NotificationChannelHandler<Payload extends StandardSchema, Ctx> = (
  args: NotificationChannelHandleArgs<Payload, Ctx>,
) => MaybePromise<NotificationChannelResult | undefined>;

/**
 * Notification channel handlers keyed by channel name.
 */
export type NotificationChannels<Payload extends StandardSchema, Ctx> = Record<
  string,
  NotificationChannelHandler<Payload, Ctx>
>;

/**
 * Arguments passed to an app-owned notification preference evaluator.
 */
export interface NotificationPreferenceArgs<
  Payload extends StandardSchema = StandardSchema,
  Ctx = unknown,
> extends NotificationChannelHandleArgs<Payload, Ctx> {
  /**
   * Optional metadata supplied by the notification sender.
   */
  metadata?: Record<string, unknown>;
}

/**
 * App-owned decision for one notification channel.
 */
export interface NotificationPreferenceDecision {
  /**
   * Whether this channel should deliver.
   */
  deliver: boolean;
  /**
   * Optional reason recorded when delivery is skipped.
   */
  reason?: string;
}

/**
 * Optional app-facing port for notification channel preferences and opt-outs.
 */
export interface NotificationPreferencesPort<Ctx = unknown> {
  /**
   * Evaluate the current preference immediately before channel delivery.
   */
  evaluate(
    args: NotificationPreferenceArgs<StandardSchema, Ctx>,
  ): MaybePromise<NotificationPreferenceDecision>;
}

/**
 * Notification definition created by `defineNotification(...)`.
 */
export interface NotificationDef<
  Name extends string = string,
  Payload extends StandardSchema = StandardSchema,
  Ctx = unknown,
> extends NotificationPayloadDef<Name, Payload> {
  /**
   * Discriminator for notification definitions.
   */
  readonly kind: "notification";
  /**
   * Channel handlers that deliver the notification.
   */
  readonly channels: NotificationChannels<Payload, Ctx>;
}

/**
 * Options for declaring a typed notification.
 */
export interface DefineNotificationOptions<
  Payload extends StandardSchema,
  Ctx,
> {
  /**
   * Standard Schema payload validator.
   */
  payload: Payload;
  /**
   * Optional human-readable description for docs and tooling.
   */
  description?: string;
  /**
   * Channel handlers that deliver the notification.
   */
  channels: NotificationChannels<Payload, Ctx>;
}

/**
 * Options passed when sending a notification.
 */
export interface SendNotificationOptions {
  /**
   * Subset of channels to deliver. Defaults to all channels on the definition.
   */
  channels?: readonly string[];
  /**
   * Optional app metadata attached to memory deliveries and instrumentation.
   */
  metadata?: Record<string, unknown>;
  /**
   * Request correlation ID for instrumentation.
   */
  requestId?: string;
  /**
   * Trace identifier for instrumentation.
   */
  traceId?: string;
  /**
   * Span identifier for instrumentation.
   */
  spanId?: string;
  /**
   * Parent span identifier for instrumentation.
   */
  parentSpanId?: string;
  /**
   * W3C traceparent header value for instrumentation.
   */
  traceparent?: string;
}

/**
 * Result returned after a notification send attempt.
 */
export interface SendNotificationResult {
  /**
   * Notification name.
   */
  notificationName: string;
  /**
   * Parsed notification payload.
   */
  payload: unknown;
  /**
   * Channels selected for delivery.
   */
  channels: readonly string[];
  /**
   * Per-channel delivery results.
   */
  results: readonly NotificationChannelResult[];
}

/**
 * App-facing notification port.
 */
export interface NotificationPort {
  /**
   * Send a typed notification.
   */
  send<N extends NotificationDef>(
    notification: N,
    payload: InferNotificationPayload<N>,
    options?: SendNotificationOptions,
  ): Promise<SendNotificationResult>;
}

/**
 * Options for the inline notification dispatcher.
 */
export interface InlineNotificationDispatcherOptions<Ctx> {
  /**
   * Static notification context or factory evaluated for each send.
   */
  ctx?: Ctx | (() => MaybePromise<Ctx>);
  /**
   * Called when a channel handler or preference check fails. A returned result
   * replaces the default failed result. Observer failures are ignored so the
   * remaining channels still run.
   */
  onError?: (
    error: unknown,
    args: NotificationChannelHandleArgs<StandardSchema, Ctx>,
  ) => MaybePromise<NotificationChannelResult | undefined>;
  /**
   * How completed channel failures are surfaced. Defaults to `"report"`.
   * `"throw"` still runs every selected channel before rejecting.
   */
  failureMode?: "report" | "throw";
  /**
   * Optional app-owned notification preference evaluator.
   */
  preferences?: NotificationPreferencesPort<Ctx>;
  /**
   * Optional devtools/provider instrumentation target.
   */
  instrumentation?: ProviderInstrumentationTarget;
}

/**
 * Delivery captured by the memory notification port.
 */
export interface MemoryNotificationDelivery {
  /**
   * Generated delivery ID.
   */
  id: string;
  /**
   * Notification name.
   */
  notificationName: string;
  /**
   * Parsed payload that would have been sent.
   */
  payload: unknown;
  /**
   * Selected channels.
   */
  channels: readonly string[];
  /**
   * Optional app metadata supplied by the caller.
   */
  metadata?: Record<string, unknown>;
  /**
   * Timestamp assigned by the memory port.
   */
  sentAt: Date;
}

/**
 * In-memory notification port for tests and local examples.
 */
export interface MemoryNotificationPort extends NotificationPort {
  /**
   * Captured notification sends.
   */
  readonly deliveries: readonly MemoryNotificationDelivery[];
  /**
   * Clear captured notification sends.
   */
  clear(): void;
}

/**
 * Options for `createMemoryNotificationPort(...)`.
 */
export interface CreateMemoryNotificationPortOptions {
  /**
   * Clock used for captured deliveries.
   */
  now?: () => Date;
  /**
   * ID factory used for captured deliveries.
   */
  id?: () => string;
  /**
   * Observer called after a delivery is captured.
   */
  onSend?: (delivery: MemoryNotificationDelivery) => MaybePromise<void>;
}

/**
 * Context shape required by `defineMailNotificationChannel(...)`.
 */
export interface MailNotificationContext {
  ports: {
    mailer: {
      send(message: SendMailOptions): MaybePromise<{
        id?: string;
        provider?: string;
      }>;
    };
  };
}

/**
 * Render a mail message for one notification payload.
 */
export type MailNotificationRenderer<
  Payload extends StandardSchema,
  Ctx extends MailNotificationContext,
> = (
  args: NotificationChannelHandleArgs<Payload, Ctx>,
) => MaybePromise<SendMailOptions | undefined>;

/**
 * Context-bound notification helper factory.
 */
export interface Notifications<Ctx> {
  /**
   * Define a notification with the bound context type.
   */
  defineNotification<Name extends string, Payload extends StandardSchema>(
    name: Name,
    options: DefineNotificationOptions<Payload, Ctx>,
  ): NotificationDef<Name, Payload, Ctx>;
}

/**
 * Notification definitions available to durable delivery workers.
 */
export interface NotificationRegistry<Ctx = unknown> {
  /**
   * Registered definitions in declaration order.
   */
  readonly definitions: readonly NotificationDef<string, StandardSchema, Ctx>[];
  /**
   * Resolve a notification definition by its stable name.
   */
  get(name: string): NotificationDef<string, StandardSchema, Ctx> | undefined;
}

/**
 * Payload carried by the first-party notification delivery job.
 */
export interface NotificationDeliveryJobPayload {
  notificationName: string;
  channel: string;
  payload: unknown;
  options: Omit<SendNotificationOptions, "channels">;
}

type NotificationDeliveryPayloadSchema = StandardSchemaV1<
  unknown,
  NotificationDeliveryJobPayload
>;

/**
 * Job definition used by queued notification dispatchers and workers.
 */
export interface NotificationDeliveryJob<
  Name extends string = string,
  Ctx = unknown,
> extends JobDef<Name, NotificationDeliveryPayloadSchema, Ctx> {
  /**
   * Registry used by both enqueue-time checks and worker delivery.
   */
  readonly registry: NotificationRegistry<Ctx>;
}

/**
 * Options for the first-party notification delivery job.
 */
export interface DefineNotificationDeliveryJobOptions<
  Name extends string,
  Ctx,
> {
  /**
   * Stable job name. Defaults to `"notifications.deliver"`.
   */
  name?: Name;
  /**
   * Notification definitions available to the worker.
   */
  registry: NotificationRegistry<Ctx>;
  /**
   * Optional app-owned preferences evaluated when the job runs.
   */
  preferences?: NotificationPreferencesPort<Ctx>;
  /**
   * Retry policy. Defaults to exponential backoff with three attempts.
   */
  retry?: JobRetryOptions;
  /**
   * Optional maximum duration for each channel delivery attempt.
   */
  timeout?: JobTimeoutDuration;
  /**
   * Optional execution hooks applied to each delivery attempt.
   */
  hooks?: readonly JobHook<
    JobDef<Name, NotificationDeliveryPayloadSchema, Ctx>,
    Ctx
  >[];
}

/**
 * Options for a notification dispatcher backed by Beignet jobs.
 */
export interface QueuedNotificationDispatcherOptions<Name extends string, Ctx> {
  /**
   * Job dispatcher used to enqueue one delivery job per channel.
   */
  jobs: JobDispatcher;
  /**
   * Registered notification delivery job.
   */
  deliveryJob: NotificationDeliveryJob<Name, Ctx>;
  /**
   * Optional devtools/provider instrumentation target.
   */
  instrumentation?: ProviderInstrumentationTarget;
}

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

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

/**
 * Error thrown when notification delivery fails.
 */
export class NotificationDeliveryError extends Error {
  /**
   * Notification name.
   */
  readonly notificationName: string;
  /**
   * First channel that failed, retained for concise error handling.
   */
  readonly channel: string;
  /**
   * Original error for the first failed channel when available.
   */
  readonly cause: unknown;
  /**
   * Complete notification result after every selected channel ran.
   */
  readonly result: SendNotificationResult;
  /**
   * Failed channel results.
   */
  readonly failures: readonly NotificationChannelResult[];
  /**
   * Original channel errors in delivery order.
   */
  readonly errors: readonly NotificationChannelError[];

  constructor(args: {
    result: SendNotificationResult;
    errors?: readonly NotificationChannelError[];
  }) {
    const failures = args.result.results.filter(
      (result) => result.status === "failed",
    );
    const firstFailure = failures[0];
    const errors = args.errors ?? [];
    super(
      `Notification "${args.result.notificationName}" failed on ${failures.length} channel${failures.length === 1 ? "" : "s"}: ${failures.map((failure) => failure.channel).join(", ")}.`,
    );
    this.name = "NotificationDeliveryError";
    this.notificationName = args.result.notificationName;
    this.channel = firstFailure?.channel ?? "unknown";
    this.cause = errors.find(
      (entry) => entry.channel === firstFailure?.channel,
    )?.error;
    this.result = args.result;
    this.failures = failures;
    this.errors = errors;
  }
}

/**
 * Error thrown when a notification registry cannot safely resolve a delivery.
 */
export class NotificationRegistryError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "NotificationRegistryError";
  }
}

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("; ");
}

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 NotificationValidationError({
      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 resolveChannelNames(
  notification: {
    name: string;
    channels: Record<string, unknown>;
  },
  options:
    | {
        channels?: readonly string[];
      }
    | undefined,
): readonly string[] {
  const available = Object.keys(notification.channels);
  const selected = options?.channels ?? available;

  for (const channel of selected) {
    if (typeof notification.channels[channel] !== "function") {
      throw new Error(
        `Notification "${notification.name}" does not define channel "${channel}".`,
      );
    }
  }

  return selected;
}

function summarizeResults(results: readonly NotificationChannelResult[]) {
  const queued = results.filter((result) => result.status === "queued").length;
  const sent = results.filter((result) => result.status === "sent").length;
  const skipped = results.filter(
    (result) => result.status === "skipped",
  ).length;
  const failed = results.filter((result) => result.status === "failed").length;

  return { queued, sent, skipped, failed };
}

function resultSummary(results: readonly NotificationChannelResult[]): string {
  const summary = summarizeResults(results);
  const parts = [
    summary.queued ? `${summary.queued} queued` : undefined,
    `${summary.sent} sent`,
    `${summary.skipped} skipped`,
    `${summary.failed} failed`,
  ].filter((part): part is string => Boolean(part));

  return parts.join(", ");
}

function recordChannelResult(
  instrumentation: ReturnType<typeof createProviderInstrumentation>,
  notificationName: string,
  result: NotificationChannelResult,
  options: SendNotificationOptions,
): void {
  instrumentation.custom({
    name: `notification.channel.${result.status}`,
    label: `Notification channel ${result.status}`,
    summary: `${notificationName} (${result.channel})`,
    requestId: options.requestId,
    traceId: options.traceId,
    spanId: options.spanId,
    parentSpanId: options.parentSpanId,
    traceparent: options.traceparent,
    details: {
      notificationName,
      channel: result.channel,
      status: result.status,
      id: result.id,
      provider: result.provider,
      reason: result.reason,
      resultDetails: result.details,
      metadata: options.metadata,
    },
  });
}

async function failedChannelResult<Ctx>(
  error: unknown,
  args: NotificationChannelHandleArgs<StandardSchema, Ctx>,
  onError: InlineNotificationDispatcherOptions<Ctx>["onError"],
): Promise<NotificationChannelResult> {
  if (onError) {
    try {
      const handled = await onError(error, args);
      if (handled) return { ...handled, channel: args.channel };
    } catch {
      // Error observers cannot interrupt delivery of the remaining channels.
    }
  }

  return {
    channel: args.channel,
    status: "failed",
    reason: error instanceof Error ? error.message : String(error),
  };
}

function defineNotificationImpl<
  Name extends string,
  Payload extends StandardSchema,
  Ctx = unknown,
>(
  name: Name,
  options: DefineNotificationOptions<Payload, Ctx>,
): NotificationDef<Name, Payload, Ctx> {
  return {
    kind: "notification",
    name,
    payload: options.payload,
    description: options.description,
    channels: options.channels,
  };
}

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

function contextInstrumentationTarget(
  ctx: unknown,
): ProviderInstrumentationTarget {
  if (!isRecord(ctx)) return undefined;
  return isRecord(ctx.ports) ? ctx.ports : ctx;
}

const notificationDeliveryPayloadSchema: NotificationDeliveryPayloadSchema = {
  "~standard": {
    version: 1,
    vendor: "beignet",
    validate(value: unknown) {
      if (!isRecord(value)) {
        return { issues: [{ message: "Expected an object." }] };
      }

      const issues: StandardSchemaV1.Issue[] = [];
      if (
        typeof value.notificationName !== "string" ||
        value.notificationName.length === 0
      ) {
        issues.push({
          message: "Expected a non-empty string.",
          path: ["notificationName"],
        });
      }
      if (typeof value.channel !== "string" || value.channel.length === 0) {
        issues.push({
          message: "Expected a non-empty string.",
          path: ["channel"],
        });
      }
      if (!("payload" in value)) {
        issues.push({ message: "Required.", path: ["payload"] });
      }
      if (!isRecord(value.options)) {
        issues.push({ message: "Expected an object.", path: ["options"] });
      }

      const deliveryOptions = isRecord(value.options) ? value.options : {};
      const correlationFields = [
        "requestId",
        "traceId",
        "spanId",
        "parentSpanId",
        "traceparent",
      ] as const;
      for (const field of correlationFields) {
        if (
          deliveryOptions[field] !== undefined &&
          typeof deliveryOptions[field] !== "string"
        ) {
          issues.push({
            message: "Expected a string.",
            path: ["options", field],
          });
        }
      }
      if (
        deliveryOptions.metadata !== undefined &&
        !isRecord(deliveryOptions.metadata)
      ) {
        issues.push({
          message: "Expected an object.",
          path: ["options", "metadata"],
        });
      }

      if (issues.length > 0) return { issues };

      return {
        value: {
          notificationName: value.notificationName as string,
          channel: value.channel as string,
          payload: value.payload,
          options: queuedSendOptions(deliveryOptions),
        },
      };
    },
  },
};

/**
 * Define the notification catalog available to durable delivery workers.
 * Duplicate names throw because queued delivery resolves definitions by name.
 */
export function defineNotificationRegistry<Ctx>(
  definitions: readonly NotificationDef<string, StandardSchema, Ctx>[],
): NotificationRegistry<Ctx> {
  const byName = new Map<
    string,
    NotificationDef<string, StandardSchema, Ctx>
  >();

  for (const notification of definitions) {
    if (byName.has(notification.name)) {
      throw new NotificationRegistryError(
        `Duplicate notification definition "${notification.name}" in notification registry.`,
      );
    }
    byName.set(notification.name, notification);
  }

  return {
    definitions: [...definitions],
    get(name) {
      return byName.get(name);
    },
  };
}

/**
 * Define the generic job that resolves and delivers one notification channel.
 * Register the returned job with every worker or outbox registry that can
 * receive queued notifications.
 */
export function defineNotificationDeliveryJob<
  Ctx,
  Name extends string = "notifications.deliver",
>(
  options: DefineNotificationDeliveryJobOptions<Name, Ctx>,
): NotificationDeliveryJob<Name, Ctx> {
  const name = (options.name ?? "notifications.deliver") as Name;
  const { defineJob } = createJobs<Ctx>();
  const job = defineJob(name, {
    payload: notificationDeliveryPayloadSchema,
    description: "Deliver one notification channel.",
    retry: options.retry ?? retry.exponential({ attempts: 3 }),
    timeout: options.timeout,
    hooks: options.hooks,
    async handle({ payload, ctx }) {
      const notification = options.registry.get(payload.notificationName);
      if (!notification) {
        throw new NotificationRegistryError(
          `Notification "${payload.notificationName}" is not registered for queued delivery.`,
        );
      }
      if (!notification.channels[payload.channel]) {
        throw new NotificationRegistryError(
          `Notification "${payload.notificationName}" does not define queued channel "${payload.channel}".`,
        );
      }

      const dispatcher = createInlineNotificationDispatcher<Ctx>({
        ctx,
        preferences: options.preferences,
        failureMode: "throw",
        instrumentation: contextInstrumentationTarget(ctx),
      });
      await dispatcher.send(notification as NotificationDef, payload.payload, {
        ...payload.options,
        channels: [payload.channel],
      });
    },
  });

  return Object.assign(job, {
    registry: options.registry,
  });
}

/**
 * Validate and parse a notification payload with the notification's Standard
 * Schema.
 */
export async function parseNotificationPayload<
  N extends NotificationPayloadDef,
>(notification: N, payload: unknown): Promise<InferNotificationPayload<N>> {
  return (await parsePayload(notification.payload, payload, {
    name: notification.name,
  })) as InferNotificationPayload<N>;
}

/**
 * Create an inline notification dispatcher.
 *
 * The dispatcher validates payloads and runs selected channel handlers
 * immediately. Channel failures are isolated and reported after every selected
 * channel runs. Use this directly in tests and local apps, or use
 * `createQueuedNotificationDispatcher(...)` for background execution.
 */
export function createInlineNotificationDispatcher<Ctx>(
  options: InlineNotificationDispatcherOptions<Ctx> = {},
): NotificationPort {
  const instrumentation = createProviderInstrumentation(
    options.instrumentation,
    {
      providerName: "notifications",
      watcher: "notifications",
    },
  );

  return {
    async send<N extends NotificationDef<string, StandardSchema, Ctx>>(
      notification: N,
      payload: InferNotificationPayload<N>,
      sendOptions: SendNotificationOptions = {},
    ) {
      const parsed = await parseNotificationPayload(notification, payload);
      const channels = resolveChannelNames(notification, sendOptions);
      const ctx = await resolveCtx(options.ctx);
      const results: NotificationChannelResult[] = [];
      const errors: NotificationChannelError[] = [];

      instrumentation.custom({
        name: "notification.send.started",
        label: "Notification started",
        summary: notification.name,
        requestId: sendOptions.requestId,
        traceId: sendOptions.traceId,
        spanId: sendOptions.spanId,
        parentSpanId: sendOptions.parentSpanId,
        traceparent: sendOptions.traceparent,
        details: {
          notificationName: notification.name,
          channels,
          metadata: sendOptions.metadata,
        },
      });

      for (const channel of channels) {
        const handler = notification.channels[channel];
        const args = {
          notification,
          payload: parsed,
          ctx,
          channel,
        } satisfies NotificationChannelHandleArgs<StandardSchema, Ctx>;

        try {
          const preference = await options.preferences?.evaluate({
            ...args,
            metadata: sendOptions.metadata,
          });
          if (preference && !preference.deliver) {
            const result = {
              channel,
              status: "skipped",
              reason:
                preference.reason ?? "Disabled by notification preferences.",
              details: {
                source: "preferences",
              },
            } satisfies NotificationChannelResult;
            results.push(result);
            recordChannelResult(
              instrumentation,
              notification.name,
              result,
              sendOptions,
            );
            continue;
          }

          const handled = await handler(args);
          const result = handled
            ? { ...handled, channel }
            : ({
                channel,
                status: "sent",
              } satisfies NotificationChannelResult);

          results.push(result);
          recordChannelResult(
            instrumentation,
            notification.name,
            result,
            sendOptions,
          );
        } catch (error) {
          errors.push({ channel, error });
          const result = await failedChannelResult(
            error,
            args,
            options.onError,
          );
          results.push(result);
          recordChannelResult(
            instrumentation,
            notification.name,
            result,
            sendOptions,
          );
        }
      }

      const result = {
        notificationName: notification.name,
        payload: parsed,
        channels,
        results,
      } satisfies SendNotificationResult;
      instrumentation.custom({
        name: "notification.send.completed",
        label: "Notification completed",
        summary: `${notification.name} (${resultSummary(results)})`,
        requestId: sendOptions.requestId,
        traceId: sendOptions.traceId,
        spanId: sendOptions.spanId,
        parentSpanId: sendOptions.parentSpanId,
        traceparent: sendOptions.traceparent,
        details: {
          notificationName: notification.name,
          channels,
          results,
          metadata: sendOptions.metadata,
        },
      });

      if (
        options.failureMode === "throw" &&
        results.some((channelResult) => channelResult.status === "failed")
      ) {
        throw new NotificationDeliveryError({ result, errors });
      }

      return result;
    },
  };
}

/**
 * Options for the inline notifications provider.
 */
export interface InlineNotificationsProviderOptions
  extends Omit<
    InlineNotificationDispatcherOptions<unknown>,
    "ctx" | "instrumentation"
  > {
  /**
   * Provider name. Defaults to "inline-notifications".
   */
  name?: string;
}

/**
 * Ports contributed by the inline notifications provider.
 */
export interface InlineNotificationsProviderPorts {
  /**
   * Beignet notification port.
   */
  notifications: NotificationPort;
}

/**
 * Create a provider that contributes an inline notification dispatcher.
 *
 * Use it as the dev-default `notifications` port in `server/providers.ts`.
 * Channel handlers run with an app service context built lazily through the
 * server context blueprint on each send, so the provider is safe to register
 * before all providers have started. Sends are recorded as devtools events
 * through the `notifications` watcher when an instrumentation port is
 * installed.
 */
export function createInlineNotificationsProvider(
  options: InlineNotificationsProviderOptions = {},
) {
  const { name = "inline-notifications", ...dispatcherOptions } = options;

  return createProvider({
    name,
    setup({ ports, createServiceContext }) {
      const notifications = createInlineNotificationDispatcher({
        ...dispatcherOptions,
        ctx: () => createServiceContext(),
        instrumentation: ports,
      });

      return {
        ports: {
          notifications,
        } satisfies InlineNotificationsProviderPorts,
      };
    },
  });
}

function queuedSendOptions(
  options: Partial<Record<keyof SendNotificationOptions, unknown>>,
): Omit<SendNotificationOptions, "channels"> {
  const queued: Omit<SendNotificationOptions, "channels"> = {};
  if (isRecord(options.metadata)) queued.metadata = options.metadata;
  if (typeof options.requestId === "string")
    queued.requestId = options.requestId;
  if (typeof options.traceId === "string") queued.traceId = options.traceId;
  if (typeof options.spanId === "string") queued.spanId = options.spanId;
  if (typeof options.parentSpanId === "string") {
    queued.parentSpanId = options.parentSpanId;
  }
  if (typeof options.traceparent === "string")
    queued.traceparent = options.traceparent;
  return queued;
}

/**
 * Create a notification dispatcher that enqueues one delivery job per channel.
 * Separate jobs keep provider retries from resending channels that already
 * completed successfully.
 */
export function createQueuedNotificationDispatcher<Name extends string, Ctx>(
  options: QueuedNotificationDispatcherOptions<Name, Ctx>,
): NotificationPort {
  const instrumentation = createProviderInstrumentation(
    options.instrumentation,
    {
      providerName: "queued-notifications",
      watcher: "notifications",
    },
  );

  return {
    async send<N extends NotificationDef>(
      notification: N,
      payload: InferNotificationPayload<N>,
      sendOptions: SendNotificationOptions = {},
    ) {
      const registered = options.deliveryJob.registry.get(notification.name);
      if (!registered) {
        throw new NotificationRegistryError(
          `Notification "${notification.name}" is not registered for queued delivery.`,
        );
      }

      const parsed = await parseNotificationPayload(registered, payload);
      const channels = resolveChannelNames(registered, sendOptions);
      const results: NotificationChannelResult[] = [];

      instrumentation.custom({
        name: "notification.enqueue.started",
        label: "Notification enqueue started",
        summary: notification.name,
        requestId: sendOptions.requestId,
        traceId: sendOptions.traceId,
        spanId: sendOptions.spanId,
        parentSpanId: sendOptions.parentSpanId,
        traceparent: sendOptions.traceparent,
        details: {
          notificationName: notification.name,
          channels,
          metadata: sendOptions.metadata,
          jobName: options.deliveryJob.name,
        },
      });

      for (const channel of channels) {
        let result: NotificationChannelResult;
        try {
          await options.jobs.dispatch(options.deliveryJob, {
            notificationName: notification.name,
            channel,
            payload,
            options: queuedSendOptions(sendOptions),
          });
          result = {
            channel,
            status: "queued",
            provider: "jobs",
            details: {
              jobName: options.deliveryJob.name,
            },
          };
        } catch (error) {
          result = {
            channel,
            status: "failed",
            reason: error instanceof Error ? error.message : String(error),
            details: {
              phase: "enqueue",
              jobName: options.deliveryJob.name,
            },
          };
        }

        results.push(result);
        recordChannelResult(
          instrumentation,
          notification.name,
          result,
          sendOptions,
        );
      }

      const result = {
        notificationName: notification.name,
        payload: parsed,
        channels,
        results,
      } satisfies SendNotificationResult;
      instrumentation.custom({
        name: "notification.enqueue.completed",
        label: "Notification enqueue completed",
        summary: `${notification.name} (${resultSummary(results)})`,
        requestId: sendOptions.requestId,
        traceId: sendOptions.traceId,
        spanId: sendOptions.spanId,
        parentSpanId: sendOptions.parentSpanId,
        traceparent: sendOptions.traceparent,
        details: {
          notificationName: notification.name,
          channels,
          results,
          metadata: sendOptions.metadata,
          jobName: options.deliveryJob.name,
        },
      });

      return result;
    },
  };
}

/**
 * Options for the queued notifications provider.
 */
export interface QueuedNotificationsProviderOptions<Name extends string, Ctx> {
  /**
   * Registered notification delivery job.
   */
  deliveryJob: NotificationDeliveryJob<Name, Ctx>;
  /**
   * Provider name. Defaults to `"queued-notifications"`.
   */
  name?: string;
}

/**
 * Create a provider that contributes a job-backed notification dispatcher.
 */
export function createQueuedNotificationsProvider<Name extends string, Ctx>(
  options: QueuedNotificationsProviderOptions<Name, Ctx>,
) {
  return createProvider<{ jobs: JobDispatcher }>()({
    name: options.name ?? "queued-notifications",
    setup({ ports }) {
      return {
        ports: {
          notifications: createQueuedNotificationDispatcher({
            jobs: ports.jobs,
            deliveryJob: options.deliveryJob,
            instrumentation: ports,
          }),
        } satisfies InlineNotificationsProviderPorts,
      };
    },
  });
}

/**
 * Define a mail-backed notification channel.
 *
 * Return `undefined` from the renderer when the channel should be skipped, for
 * example when a recipient does not have an email address.
 */
export function defineMailNotificationChannel<
  Payload extends StandardSchema,
  Ctx extends MailNotificationContext,
>(
  render: MailNotificationRenderer<Payload, Ctx>,
): NotificationChannelHandler<Payload, Ctx> {
  return async (args) => {
    const message = await render(args);
    if (!message) {
      return {
        channel: args.channel,
        status: "skipped",
        reason: "No mail message was returned.",
      };
    }

    const result = await args.ctx.ports.mailer.send(message);
    return {
      channel: args.channel,
      status: "sent",
      id: result.id,
      provider: result.provider,
    };
  };
}

/**
 * Create an in-memory notification port for tests and examples.
 *
 * The memory port validates payloads and records notification intent without
 * running channel handlers.
 */
export function createMemoryNotificationPort(
  options: CreateMemoryNotificationPortOptions = {},
): MemoryNotificationPort {
  const deliveries: MemoryNotificationDelivery[] = [];
  const now = options.now ?? (() => new Date());
  const id = options.id ?? (() => crypto.randomUUID());

  return {
    get deliveries() {
      return deliveries;
    },

    async send<N extends NotificationDef>(
      notification: N,
      payload: InferNotificationPayload<N>,
      sendOptions: SendNotificationOptions = {},
    ) {
      const parsed = await parseNotificationPayload(notification, payload);
      const channels = resolveChannelNames(notification, sendOptions);
      const delivery: MemoryNotificationDelivery = {
        id: id(),
        notificationName: notification.name,
        payload: parsed,
        channels,
        metadata: sendOptions.metadata,
        sentAt: now(),
      };

      deliveries.push(delivery);
      await options.onSend?.(delivery);

      return {
        notificationName: notification.name,
        payload: parsed,
        channels,
        results: channels.map((channel) => ({
          channel,
          status: "sent",
          id: delivery.id,
          provider: "memory",
          details: {
            mode: "intent",
          },
        })),
      };
    },

    clear() {
      deliveries.length = 0;
    },
  };
}

/**
 * Create notification helper methods bound to an application context type.
 *
 * Call it once in `lib/notifications.ts`:
 *
 * ```ts
 * export const { defineNotification } = createNotifications<AppContext>();
 * ```
 *
 * Notifications represent user-facing communication intent. Channel handlers
 * decide how that intent becomes mail, SMS, push, in-app delivery, or another
 * app-owned channel.
 */
export function createNotifications<Ctx>(): Notifications<Ctx> {
  return {
    defineNotification<Name extends string, Payload extends StandardSchema>(
      name: Name,
      options: DefineNotificationOptions<Payload, Ctx>,
    ): NotificationDef<Name, Payload, Ctx> {
      return defineNotificationImpl(name, options);
    },
  };
}
