// Discord plugin module implements send.message request behavior.
import { randomBytes } from "node:crypto";
import { MessageFlags, type APIAllowedMentions, type APIEmbed } from "discord-api-types/v10";
import {
  Embed,
  serializePayload,
  type MessagePayloadFile,
  type MessagePayloadObject,
  type TopLevelComponents,
} from "./internal/discord.js";
import { stripUndefinedFields } from "./internal/undefined-fields.js";

export { stripUndefinedFields };

const SUPPRESS_EMBEDS_FLAG = MessageFlags.SuppressEmbeds;
export const SUPPRESS_NOTIFICATIONS_FLAG = MessageFlags.SuppressNotifications;

type DiscordSendComponentFactory = (text: string) => TopLevelComponents[];
export type DiscordSendComponents = TopLevelComponents[] | DiscordSendComponentFactory;
export type DiscordSendEmbeds = Array<APIEmbed | Embed>;
export type DiscordAllowedMentions = APIAllowedMentions;

export function createDiscordMessageNonce(): string {
  return randomBytes(12).toString("hex");
}

export function resolveDiscordSendComponents(params: {
  components?: DiscordSendComponents;
  text: string;
  isFirst: boolean;
}): TopLevelComponents[] | undefined {
  if (!params.components || !params.isFirst) {
    return undefined;
  }
  return typeof params.components === "function"
    ? params.components(params.text)
    : params.components;
}

function normalizeDiscordEmbeds(embeds?: DiscordSendEmbeds): Embed[] | undefined {
  if (!embeds?.length) {
    return undefined;
  }
  return embeds.map((embed) => (embed instanceof Embed ? embed : new Embed(embed)));
}

export function resolveDiscordSendEmbeds(params: {
  embeds?: DiscordSendEmbeds;
  isFirst: boolean;
}): Embed[] | undefined {
  if (!params.embeds || !params.isFirst) {
    return undefined;
  }
  return normalizeDiscordEmbeds(params.embeds);
}

function buildDiscordMessagePayload(params: {
  text: string;
  components?: TopLevelComponents[];
  embeds?: Embed[];
  allowedMentions?: DiscordAllowedMentions;
  flags?: number;
  files?: MessagePayloadFile[];
}): MessagePayloadObject {
  const payload: MessagePayloadObject = {};
  const hasV2 = hasV2Components(params.components);
  const trimmed = params.text.trim();
  if (!hasV2 && trimmed) {
    payload.content = params.text;
  }
  if (params.components?.length) {
    payload.components = params.components;
  }
  if (!hasV2 && params.embeds?.length) {
    payload.embeds = params.embeds;
  }
  if (params.allowedMentions) {
    payload.allowed_mentions = params.allowedMentions;
  }
  if (params.flags !== undefined) {
    payload.flags = params.flags;
  }
  if (params.files?.length) {
    payload.files = params.files;
  }
  return payload;
}

export function resolveDiscordMessageFlags(params: {
  silent?: boolean;
  suppressEmbeds?: boolean;
}): number | undefined {
  let flags = 0;
  if (params.suppressEmbeds) {
    flags |= SUPPRESS_EMBEDS_FLAG;
  }
  if (params.silent) {
    flags |= SUPPRESS_NOTIFICATIONS_FLAG;
  }
  return flags || undefined;
}

export function resolveDiscordSuppressEmbeds(params: {
  configured?: boolean;
  override?: boolean;
}): boolean {
  return params.override ?? params.configured ?? true;
}

type DiscordMessageRequestParams = {
  text: string;
  components?: TopLevelComponents[];
  embeds?: Embed[];
  allowedMentions?: DiscordAllowedMentions;
  files?: MessagePayloadFile[];
  flags?: number;
  replyTo?: string;
} & ({ endpoint: "create-message"; nonce?: string } | { endpoint: "forum-thread"; nonce?: never });

export function buildDiscordMessageRequest(params: DiscordMessageRequestParams) {
  const payload = buildDiscordMessagePayload(params);
  const nonce =
    params.endpoint === "create-message"
      ? (params.nonce ?? createDiscordMessageNonce())
      : undefined;
  return stripUndefinedFields({
    ...serializePayload(payload),
    ...(params.replyTo
      ? { message_reference: { message_id: params.replyTo, fail_if_not_exists: false } }
      : {}),
    nonce,
    enforce_nonce: nonce ? true : undefined,
  });
}

function hasV2Components(components?: TopLevelComponents[]): boolean {
  return Boolean(components?.some((component) => "isV2" in component && component.isV2));
}
