// Channel-adapter / DI ports the host app implements and injects via
// defineModule({ adapters }). Consumed by the internal dispatch engine
// (internal/plan* and internal/deliver*) and the event executors.

/** Recipient profile resolved by the user-management adapter at dispatch time, with one `addresses` entry per channelId the recipient owns. */
export interface RecipientProfile {
  userId: string;
  locale: string;
  addresses: Record<string, string | null>;
}

// Recipient rows (User) live in the host app's namespace, so the DB handle the
// adapter reads from is opaque to this module — typed `unknown` so the adapter
// implementation narrows it to its own Kysely instance. The caller's handle is
// passed through so recipients created earlier in the same uncommitted
// transaction (e.g. invitation bootstrap users) are visible to the lookup.
type RecipientLookupDB = unknown;

/** Port the host app implements to resolve a {@link RecipientProfile} for a userId, or `null` when the user cannot be resolved. */
export interface ResolveRecipientFn {
  (db: RecipientLookupDB, userId: string): Promise<RecipientProfile | null>;
}

/** Channel adapter the host app implements to deliver a Notification to the in-app inbox channel. */
export interface InAppAdapter {
  send: (args: {
    /** Id of the Notification row being delivered. */
    notificationId: string;
    /** Stable per-row dedup token; forward it to the provider so a redrain re-send collapses to one delivery. */
    idempotencyKey: string;
    /** Channel-specific recipient address resolved from the {@link RecipientProfile}. */
    recipientAddress: string;
    /** Rendered subject. */
    subject: string;
    /** Rendered plain-text body. */
    body: string;
  }) => Promise<
    | { ok: true }
    | {
        ok: false;
        /** Stable failure category (audited). */
        errorClass: string;
        /** Human-readable failure detail (audited). */
        errorDetail: string;
      }
  >;
}

/** Channel adapter the host app implements to deliver a Notification to the EMAIL channel. */
export interface EmailAdapter {
  send: (args: {
    /** Id of the Notification row being delivered. */
    notificationId: string;
    /** Stable per-row dedup token; forward it to the provider so a redrain re-send collapses to one delivery. */
    idempotencyKey: string;
    /** Channel-specific recipient address resolved from the {@link RecipientProfile}. */
    recipientAddress: string;
    /** Rendered subject. */
    subject: string;
    /** Rendered plain-text body. */
    body: string;
    /** Rendered HTML body, or `null` when none was rendered. */
    htmlBody: string | null;
  }) => Promise<
    | {
        ok: true;
        /** Provider message id returned on success. */
        messageId: string;
      }
    | {
        ok: false;
        /** Stable failure category (audited). */
        errorClass: string;
        /** Human-readable failure detail (audited). */
        errorDetail: string;
      }
  >;
}

/** Channel adapter the host app implements to post a message to a shared DESTINATION surface (e.g. a Slack channel), independent of any recipient set. */
export interface DestinationAdapter {
  send: (args: {
    /** Id of the DESTINATION channel this binding posts on. */
    channelId: string;
    /** Stable per-binding dedup token; forward it to providers that support idempotency so a redrain re-send collapses to one delivery. Slack `chat.postMessage` has no such parameter, so destination delivery is at-least-once. */
    idempotencyKey: string;
    /** Provider-side reference of the shared surface to post to (e.g. a Slack channel id). */
    externalChannelRef: string;
    /** Rendered subject. */
    subject: string;
    /** Rendered plain-text body. */
    body: string;
    /** Optional extra context the adapter may use when composing the message. */
    metadata?: Record<string, unknown>;
  }) => Promise<
    | {
        ok: true;
        /** Provider message id returned on success. */
        externalMessageId?: string;
        /** Redacted provider response persisted on the delivery log. */
        providerResponse?: string;
      }
    | {
        ok: false;
        /** Stable failure category (audited). */
        errorClass: string;
        /** Human-readable failure detail (audited). */
        errorDetail: string;
        /** Redacted provider response persisted on the delivery log. */
        providerResponse?: string;
      }
  >;
}

/** Bundle of channel-delivery ports the host app injects via `defineModule({ adapters })`. */
export interface DispatchAdapters {
  /** Required adapter for the in-app inbox channel. */
  inApp: InAppAdapter;
  /** Optional EMAIL adapter; when omitted, EMAIL-channel pairs fail as audited `CHANNEL_ADAPTER_FAILED`. */
  email?: EmailAdapter;
  /** Required port resolving a recipient profile from a userId. */
  resolveRecipient: ResolveRecipientFn;
  /** Optional DESTINATION adapter (Slack/Teams); when omitted, the DESTINATION stage is a no-op. */
  destination?: DestinationAdapter;
  /** Optional clock override so tests can pin the dayBucket boundary. */
  now?: () => Date;
}
