import { db } from "@/lib/db/client"
import { domainEvents, type DomainEventType } from "@/lib/db/schema"

/**
 * The outbox, write side.
 *
 * ## What the seam actually is
 *
 * `architecture.md` §4 draws it as event → pg-boss → worker → adapter, and it
 * is easy to read that as being about the queue. It is not. The seam is the
 * event: application code records that something happened, in the transaction
 * that made it happen, and stops. It does not know that notifications exist, it
 * does not know that ntfy exists, and it does not know what pulls from here.
 *
 * That is what makes the queue a later decision rather than a rewrite. Adding
 * pg-boss replaces what pulls from `domain_events` — today `dispatchPending` in
 * lib/services/notifications.ts, called at commit and again by the nightly job
 * — and touches nothing that writes. No call site of `recordEvent` changes, no
 * table changes, and the fan-out rules do not move.
 *
 * ## Why it takes a transaction and never opens its own
 *
 * An event that can be written without the change that caused it is not an
 * outbox, it is a log. The parameter is the enforcement: there is no way to
 * call this outside a transaction, so there is no way to record `task.checked`
 * for an approval that then failed, and no way for an approval to commit
 * without the event that tells its assignee.
 */

/** The transaction handle, taken from `db.transaction` so it cannot drift. */
export type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]

export type NewEvent = {
  type: DomainEventType
  /** The task it concerns. Every type there is today carries one. */
  taskInstanceId: string | null
  /**
   * Who acted, and therefore who is not told. Null when nobody did: the
   * nightly sweep marks work missed and no person pressed anything.
   */
  actorPersonId: string | null
  /** The words the act carried, when it carried any. Never a sentence about it. */
  payload?: { note?: string }
}

/** One event, in the caller's transaction. Returns the id, to dispatch on. */
export async function recordEvent(tx: Tx, event: NewEvent): Promise<string> {
  const [row] = await tx
    .insert(domainEvents)
    .values({
      type: event.type,
      taskInstanceId: event.taskInstanceId,
      actorPersonId: event.actorPersonId,
      payload: event.payload ?? null,
    })
    .returning({ id: domainEvents.id })
  return row!.id
}

/**
 * Several at once, for a sweep that changes many rows in one statement.
 *
 * One insert rather than a loop, because it shares the transaction of an update
 * that touched every one of those tasks: a hundred round trips inside a held
 * transaction is a hundred chances to be the slow query somebody is waiting
 * behind.
 */
export async function recordEvents(
  tx: Tx,
  events: NewEvent[],
): Promise<string[]> {
  if (events.length === 0) return []
  const rows = await tx
    .insert(domainEvents)
    .values(
      events.map((e) => ({
        type: e.type,
        taskInstanceId: e.taskInstanceId,
        actorPersonId: e.actorPersonId,
        payload: e.payload ?? null,
      })),
    )
    .returning({ id: domainEvents.id })
  return rows.map((r) => r.id)
}
