import {
  ChangeMessage,
  ControlMessage,
  Message,
  NormalizedPgSnapshot,
  Offset,
  PostgresSnapshot,
  Row,
} from './types'

/**
 * Type guard for checking {@link Message} is {@link ChangeMessage}.
 *
 * See [TS docs](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)
 * for information on how to use type guards.
 *
 * @param message - the message to check
 * @returns true if the message is a {@link ChangeMessage}
 *
 * @example
 * ```ts
 * if (isChangeMessage(message)) {
 *   const msgChng: ChangeMessage = message // Ok
 *   const msgCtrl: ControlMessage = message // Err, type mismatch
 * }
 * ```
 */
export function isChangeMessage<T extends Row<unknown> = Row>(
  message: Message<T>
): message is ChangeMessage<T> {
  return message != null && `key` in message
}

/**
 * Type guard for checking {@link Message} is {@link ControlMessage}.
 *
 * See [TS docs](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)
 * for information on how to use type guards.
 *
 * @param message - the message to check
 * @returns true if the message is a {@link ControlMessage}
 *
 *  * @example
 * ```ts
 * if (isControlMessage(message)) {
 *   const msgChng: ChangeMessage = message // Err, type mismatch
 *   const msgCtrl: ControlMessage = message // Ok
 * }
 * ```
 */
export function isControlMessage<T extends Row<unknown> = Row>(
  message: Message<T>
): message is ControlMessage {
  return message != null && `headers` in message && `control` in message.headers
}

export function isUpToDateMessage<T extends Row<unknown> = Row>(
  message: Message<T>
): message is ControlMessage & { up_to_date: true } {
  return isControlMessage(message) && message.headers.control === `up-to-date`
}

/**
 * Parses the LSN from the up-to-date message and turns it into an offset.
 * The LSN is only present in the up-to-date control message when in SSE mode.
 * If we are not in SSE mode this function will return undefined.
 */
export function getOffset(message: ControlMessage): Offset | undefined {
  if (message.headers.control != `up-to-date`) return
  const lsn = message.headers.global_last_seen_lsn
  return lsn ? (`${lsn}_0` as Offset) : undefined
}

function bigintReplacer(_key: string, value: unknown): unknown {
  return typeof value === `bigint` ? value.toString() : value
}

/**
 * BigInt-safe version of JSON.stringify.
 * Converts BigInt values to their string representation (as JSON strings,
 * e.g. `{ id: 42n }` becomes `{"id":"42"}`) instead of throwing.
 * Assumes input is a JSON-serializable value — passing `undefined` at the
 * top level will return `undefined` (matching `JSON.stringify` behavior).
 */
export function bigintSafeStringify(value: unknown): string {
  return JSON.stringify(value, bigintReplacer)
}

/**
 * Canonical, BigInt-safe stringify. Recursively sorts object keys so that
 * permutation-equivalent inputs produce identical output. Suitable for
 * dedup keys and cache lookups.
 */
export function canonicalBigintSafeStringify(value: unknown): string {
  return JSON.stringify(canonicalize(value))
}

function canonicalize(value: unknown): unknown {
  if (typeof value === `bigint`) return value.toString()
  if (value === null || typeof value !== `object`) return value
  if (Array.isArray(value)) return value.map(canonicalize)
  const sorted: Record<string, unknown> = {}
  for (const k of Object.keys(value as Record<string, unknown>).sort()) {
    sorted[k] = canonicalize((value as Record<string, unknown>)[k])
  }
  return sorted
}

/**
 * Checks if a transaction is visible in a snapshot.
 *
 * @param txid - the transaction id to check
 * @param snapshot - the information about the snapshot
 * @returns true if the transaction is visible in the snapshot
 */
export function isVisibleInSnapshot(
  txid: number | bigint | `${bigint}`,
  snapshot: PostgresSnapshot | NormalizedPgSnapshot
): boolean {
  const xid = BigInt(txid)
  const xmin = BigInt(snapshot.xmin)
  const xmax = BigInt(snapshot.xmax)
  const xip = snapshot.xip_list.map(BigInt)

  // If the transaction id is less than the minimum transaction id, it is visible in the snapshot.
  // If the transaction id is less than the maximum transaction id and not in the list of active
  //   transactions at the time of the snapshot, it has been committed before the snapshot was taken
  //   and is therefore visible in the snapshot.
  // Otherwise, it is not visible in the snapshot.

  return xid < xmin || (xid < xmax && !xip.includes(xid))
}
