/**
 * Server side of channels (docs/backend-plan.md §3.3): typed pub/sub, server → client only.
 *
 *   type ChatEvents = { message: { from: string; text: string }; typing: { from: string } }
 *   export const chat = channel<ChatEvents>({ onJoin: room => { auth.requireUser() } })
 *   chat.publish(room, "message", { from, text })
 *
 * A channel's identity is its export (`<path>#<name>`), assigned by the runtime when the bundle is
 * loaded (`loadServerModules`); `publish` before that is an error. Delivery goes through the
 * `globalThis.__lecodesPublish` seam the runner installs (same reason as ./context.ts: the bundle
 * carries its own SDK copy).
 */

export type ChannelEvents = Record<string, unknown>

export type ChannelOptions = {
  /** Called when a client subscribes to `topic`; throw (e.g. `ApiError(403)`) to refuse. */
  onJoin?: (topic: string) => void | Promise<void>
}

export interface Channel<E extends ChannelEvents> {
  /** @internal set by the runtime: `<path>#<export>` */
  __id: string | null
  /** @internal */
  readonly __channel: true
  readonly options: ChannelOptions
  /** Deliver `event` with `payload` to every subscriber of `topic` (at-most-once). */
  publish<K extends keyof E & string>(topic: string, event: K, payload: E[K]): void
}

type Publisher = (channelId: string, topic: string, event: string, payload: unknown) => void
const seam = globalThis as unknown as { __lecodesPublish?: Publisher }

/** Host hook (runner / test harness): where `publish` delivers to. */
export const setChannelPublisher = (publisher: Publisher | null) => {
  if (publisher) seam.__lecodesPublish = publisher
  else delete seam.__lecodesPublish
}

export const channel = <E extends ChannelEvents>(options: ChannelOptions = {}): Channel<E> => {
  const ch: Channel<E> = {
    __id: null,
    __channel: true,
    options,
    publish(topic, event, payload) {
      if (!ch.__id) throw new Error("channel.publish: the channel is not registered — export it from a *.server.ts file")
      const publish = seam.__lecodesPublish
      if (!publish) throw new Error("channel.publish: no host publisher installed")
      publish(ch.__id, topic, event, payload)
    },
  }
  return ch
}

export const isChannel = (v: unknown): v is Channel<any> =>
  typeof v === "object" && v !== null && (v as Channel<any>).__channel === true
