/**
 * `@platform-modules/notifications` — multi-channel delivery seam (zero channel code).
 */

/** Mirrors `@platform-modules/jobs` `IdempotencyStore` — host wires one store to both. */
export interface DedupStore {
  seen(key: string): Promise<boolean>
  mark(key: string, ttl?: number): Promise<void>
}

export interface PreferenceStore {
  getEnabledChannels(userId: string, eventType: string): Promise<string[]>
}

export type NotifyEvent = {
  id?: string
  type: string
  userId: string
  dedupKey?: string
  template: { subject?: string; body: string; html?: string }
  data: Record<string, string>
  recipients: Record<string, unknown>
}

export type RenderedMessage = {
  subject?: string
  body: string
  html?: string
}

export type ChannelError = {
  message: string
  retryable?: boolean
  code?: string
}

export type ChannelResult =
  | { ok: true; id?: string }
  | { ok: false; error: ChannelError }

export interface ChannelAdapter {
  channel: string
  send(rendered: RenderedMessage, recipient: unknown): Promise<ChannelResult>
  supports?(event: NotifyEvent): boolean
}

export type DeliveryResult = {
  channel: string
  status: 'sent' | 'skipped' | 'deduped' | 'failed'
  id?: string
  error?: ChannelError
}

export type NotifyContext = {
  adapters: ChannelAdapter[]
  preferences: PreferenceStore
  dedup: DedupStore
  /**
   * Wraps a per-channel delivery attempt. Within-call idempotence is a local
   * per-channel success flag (independent of the store): once an attempt
   * succeeds, further attempts short-circuit — so a retry after a successful
   * first attempt does not double-deliver, even for an unkeyed event. The
   * persistent dedup record is written on success only, so a retry after a
   * FAILED first attempt re-sends.
   */
  retry?: (attempt: () => Promise<ChannelResult>) => Promise<ChannelResult>
}

/** Zero-dep `{{key}}` interpolation — missing keys become empty strings. */
export function render(tpl: string, data: Record<string, string>): string {
  return tpl.replace(/\{\{(\w+)\}\}/g, (_match, key: string) => data[key] ?? '')
}

export function deriveDedupKey(
  event: NotifyEvent,
  channel: string,
  recipientKey: string,
): string | null {
  const base = event.dedupKey ?? event.id
  if (base === undefined) return null
  return `${base}:${channel}:${recipientKey}`
}

export function resolvePreferences(
  store: PreferenceStore,
  userId: string,
  eventType: string,
): Promise<string[]> {
  return store.getEnabledChannels(userId, eventType)
}

function recipientKey(recipient: unknown): string {
  if (typeof recipient === 'string') return recipient
  return JSON.stringify(recipient)
}

function renderEvent(event: NotifyEvent): RenderedMessage {
  return {
    subject: event.template.subject
      ? render(event.template.subject, event.data)
      : undefined,
    body: render(event.template.body, event.data),
    html: event.template.html ? render(event.template.html, event.data) : undefined,
  }
}

export async function notify(
  event: NotifyEvent,
  ctx: NotifyContext,
): Promise<DeliveryResult[]> {
  const enabled = await resolvePreferences(ctx.preferences, event.userId, event.type)
  const rendered = renderEvent(event)
  const results: DeliveryResult[] = []

  for (const adapter of ctx.adapters) {
    if (!enabled.includes(adapter.channel)) {
      results.push({ channel: adapter.channel, status: 'skipped' })
      continue
    }

    if (adapter.supports && !adapter.supports(event)) {
      results.push({ channel: adapter.channel, status: 'skipped' })
      continue
    }

    const recipient = event.recipients[adapter.channel]
    if (recipient === undefined) {
      results.push({ channel: adapter.channel, status: 'skipped' })
      continue
    }

    const dedupKey = deriveDedupKey(event, adapter.channel, recipientKey(recipient))

    if (dedupKey !== null && (await ctx.dedup.seen(dedupKey))) {
      results.push({ channel: adapter.channel, status: 'deduped' })
      continue
    }

    let delivered = false
    const attempt = async (): Promise<ChannelResult> => {
      if (delivered) {
        return { ok: true }
      }
      const result = await adapter.send(rendered, recipient)
      if (result.ok) {
        delivered = true
        if (dedupKey !== null) {
          await ctx.dedup.mark(dedupKey)
        }
      }
      return result
    }

    try {
      const sendResult = ctx.retry ? await ctx.retry(attempt) : await attempt()
      if (sendResult.ok) {
        results.push({
          channel: adapter.channel,
          status: 'sent',
          id: sendResult.id,
        })
      } else {
        results.push({
          channel: adapter.channel,
          status: 'failed',
          error: sendResult.error,
        })
      }
    } catch (err) {
      results.push({
        channel: adapter.channel,
        status: 'failed',
        error: {
          message: err instanceof Error ? err.message : String(err),
          retryable: true,
        },
      })
    }
  }

  return results
}
