import crypto from "node:crypto";
import {
  Button,
  ChannelSelectMenu,
  CheckboxGroup,
  Container,
  File,
  Label,
  LinkButton,
  MediaGallery,
  MentionableSelectMenu,
  Modal,
  RadioGroup,
  RoleSelectMenu,
  Row,
  Section,
  Separator,
  StringSelectMenu,
  TextDisplay,
  TextInput,
  Thumbnail,
  UserSelectMenu,
  parseCustomId,
  type ComponentParserResult,
  type TopLevelComponents,
} from "@buape/carbon";
import { ButtonStyle, MessageFlags, TextInputStyle } from "discord-api-types/v10";

export const DISCORD_COMPONENT_CUSTOM_ID_KEY = "occomp";
export const DISCORD_MODAL_CUSTOM_ID_KEY = "ocmodal";
export const DISCORD_COMPONENT_ATTACHMENT_PREFIX = "attachment://";

export type DiscordComponentButtonStyle = "primary" | "secondary" | "success" | "danger" | "link";

export type DiscordComponentSelectType = "string" | "user" | "role" | "mentionable" | "channel";

export type DiscordComponentModalFieldType =
  | "text"
  | "checkbox"
  | "radio"
  | "select"
  | "role-select"
  | "user-select";

export type DiscordComponentButtonSpec = {
  label: string;
  style?: DiscordComponentButtonStyle;
  url?: string;
  emoji?: {
    name: string;
    id?: string;
    animated?: boolean;
  };
  disabled?: boolean;
  /** Optional allowlist of users who can interact with this button (ids or names). */
  allowedUsers?: string[];
};

export type DiscordComponentSelectOption = {
  label: string;
  value: string;
  description?: string;
  emoji?: {
    name: string;
    id?: string;
    animated?: boolean;
  };
  default?: boolean;
};

export type DiscordComponentSelectSpec = {
  type?: DiscordComponentSelectType;
  placeholder?: string;
  minValues?: number;
  maxValues?: number;
  options?: DiscordComponentSelectOption[];
};

export type DiscordComponentSectionAccessory =
  | {
      type: "thumbnail";
      url: string;
    }
  | {
      type: "button";
      button: DiscordComponentButtonSpec;
    };

type DiscordComponentSeparatorSpacing = "small" | "large" | 1 | 2;

export type DiscordComponentBlock =
  | {
      type: "text";
      text: string;
    }
  | {
      type: "section";
      text?: string;
      texts?: string[];
      accessory?: DiscordComponentSectionAccessory;
    }
  | {
      type: "separator";
      spacing?: DiscordComponentSeparatorSpacing;
      divider?: boolean;
    }
  | {
      type: "actions";
      buttons?: DiscordComponentButtonSpec[];
      select?: DiscordComponentSelectSpec;
    }
  | {
      type: "media-gallery";
      items: Array<{ url: string; description?: string; spoiler?: boolean }>;
    }
  | {
      type: "file";
      file: `attachment://${string}`;
      spoiler?: boolean;
    };

export type DiscordModalFieldSpec = {
  type: DiscordComponentModalFieldType;
  name?: string;
  label: string;
  description?: string;
  placeholder?: string;
  required?: boolean;
  options?: DiscordComponentSelectOption[];
  minValues?: number;
  maxValues?: number;
  minLength?: number;
  maxLength?: number;
  style?: "short" | "paragraph";
};

export type DiscordModalSpec = {
  title: string;
  triggerLabel?: string;
  triggerStyle?: DiscordComponentButtonStyle;
  fields: DiscordModalFieldSpec[];
};

export type DiscordComponentMessageSpec = {
  text?: string;
  reusable?: boolean;
  container?: {
    accentColor?: string | number;
    spoiler?: boolean;
  };
  blocks?: DiscordComponentBlock[];
  modal?: DiscordModalSpec;
};

export type DiscordComponentEntry = {
  id: string;
  kind: "button" | "select" | "modal-trigger";
  label: string;
  selectType?: DiscordComponentSelectType;
  options?: Array<{ value: string; label: string }>;
  modalId?: string;
  sessionKey?: string;
  agentId?: string;
  accountId?: string;
  reusable?: boolean;
  allowedUsers?: string[];
  messageId?: string;
  createdAt?: number;
  expiresAt?: number;
};

export type DiscordModalFieldDefinition = {
  id: string;
  name: string;
  label: string;
  type: DiscordComponentModalFieldType;
  description?: string;
  placeholder?: string;
  required?: boolean;
  options?: DiscordComponentSelectOption[];
  minValues?: number;
  maxValues?: number;
  minLength?: number;
  maxLength?: number;
  style?: "short" | "paragraph";
};

export type DiscordModalEntry = {
  id: string;
  title: string;
  fields: DiscordModalFieldDefinition[];
  sessionKey?: string;
  agentId?: string;
  accountId?: string;
  reusable?: boolean;
  messageId?: string;
  createdAt?: number;
  expiresAt?: number;
};

export type DiscordComponentBuildResult = {
  components: TopLevelComponents[];
  entries: DiscordComponentEntry[];
  modals: DiscordModalEntry[];
};

const BLOCK_ALIASES = new Map<string, DiscordComponentBlock["type"]>([
  ["row", "actions"],
  ["action-row", "actions"],
]);

function createShortId(prefix: string) {
  return `${prefix}${crypto.randomBytes(6).toString("base64url")}`;
}

function requireObject(value: unknown, label: string): Record<string, unknown> {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new Error(`${label} must be an object`);
  }
  return value as Record<string, unknown>;
}

function readString(value: unknown, label: string, opts?: { allowEmpty?: boolean }): string {
  if (typeof value !== "string") {
    throw new Error(`${label} must be a string`);
  }
  const trimmed = value.trim();
  if (!opts?.allowEmpty && !trimmed) {
    throw new Error(`${label} cannot be empty`);
  }
  return opts?.allowEmpty ? value : trimmed;
}

function readOptionalString(value: unknown): string | undefined {
  if (typeof value !== "string") {
    return undefined;
  }
  const trimmed = value.trim();
  return trimmed ? trimmed : undefined;
}

function readOptionalStringArray(value: unknown, label: string): string[] | undefined {
  if (value === undefined) {
    return undefined;
  }
  if (!Array.isArray(value)) {
    throw new Error(`${label} must be an array`);
  }
  if (value.length === 0) {
    return undefined;
  }
  return value.map((entry, index) => readString(entry, `${label}[${index}]`));
}

function readOptionalNumber(value: unknown): number | undefined {
  if (typeof value !== "number" || !Number.isFinite(value)) {
    return undefined;
  }
  return value;
}

function normalizeModalFieldName(value: string | undefined, index: number) {
  const trimmed = value?.trim();
  if (trimmed) {
    return trimmed;
  }
  return `field_${index + 1}`;
}

function normalizeAttachmentRef(value: string, label: string): `attachment://${string}` {
  const trimmed = value.trim();
  if (!trimmed.startsWith(DISCORD_COMPONENT_ATTACHMENT_PREFIX)) {
    throw new Error(`${label} must start with "${DISCORD_COMPONENT_ATTACHMENT_PREFIX}"`);
  }
  const attachmentName = trimmed.slice(DISCORD_COMPONENT_ATTACHMENT_PREFIX.length).trim();
  if (!attachmentName) {
    throw new Error(`${label} must include an attachment filename`);
  }
  return `${DISCORD_COMPONENT_ATTACHMENT_PREFIX}${attachmentName}`;
}

export function resolveDiscordComponentAttachmentName(value: string): string {
  const trimmed = value.trim();
  if (!trimmed.startsWith(DISCORD_COMPONENT_ATTACHMENT_PREFIX)) {
    throw new Error(
      `Attachment reference must start with "${DISCORD_COMPONENT_ATTACHMENT_PREFIX}"`,
    );
  }
  const attachmentName = trimmed.slice(DISCORD_COMPONENT_ATTACHMENT_PREFIX.length).trim();
  if (!attachmentName) {
    throw new Error("Attachment reference must include a filename");
  }
  return attachmentName;
}

function mapButtonStyle(style?: DiscordComponentButtonStyle): ButtonStyle {
  switch ((style ?? "primary").toLowerCase()) {
    case "secondary":
      return ButtonStyle.Secondary;
    case "success":
      return ButtonStyle.Success;
    case "danger":
      return ButtonStyle.Danger;
    case "link":
      return ButtonStyle.Link;
    case "primary":
    default:
      return ButtonStyle.Primary;
  }
}

function mapTextInputStyle(style?: DiscordModalFieldSpec["style"]) {
  return style === "paragraph" ? TextInputStyle.Paragraph : TextInputStyle.Short;
}

function normalizeBlockType(raw: string) {
  const lowered = raw.trim().toLowerCase();
  return BLOCK_ALIASES.get(lowered) ?? (lowered as DiscordComponentBlock["type"]);
}

function parseSelectOptions(
  raw: unknown,
  label: string,
): DiscordComponentSelectOption[] | undefined {
  if (raw === undefined) {
    return undefined;
  }
  if (!Array.isArray(raw)) {
    throw new Error(`${label} must be an array`);
  }
  return raw.map((entry, index) => {
    const obj = requireObject(entry, `${label}[${index}]`);
    return {
      label: readString(obj.label, `${label}[${index}].label`),
      value: readString(obj.value, `${label}[${index}].value`),
      description: readOptionalString(obj.description),
      emoji:
        typeof obj.emoji === "object" && obj.emoji && !Array.isArray(obj.emoji)
          ? {
              name: readString(
                (obj.emoji as { name?: unknown }).name,
                `${label}[${index}].emoji.name`,
              ),
              id: readOptionalString((obj.emoji as { id?: unknown }).id),
              animated:
                typeof (obj.emoji as { animated?: unknown }).animated === "boolean"
                  ? (obj.emoji as { animated?: boolean }).animated
                  : undefined,
            }
          : undefined,
      default: typeof obj.default === "boolean" ? obj.default : undefined,
    };
  });
}

function parseButtonSpec(raw: unknown, label: string): DiscordComponentButtonSpec {
  const obj = requireObject(raw, label);
  const style = readOptionalString(obj.style) as DiscordComponentButtonStyle | undefined;
  const url = readOptionalString(obj.url);
  if ((style === "link" || url) && !url) {
    throw new Error(`${label}.url is required for link buttons`);
  }
  return {
    label: readString(obj.label, `${label}.label`),
    style,
    url,
    emoji:
      typeof obj.emoji === "object" && obj.emoji && !Array.isArray(obj.emoji)
        ? {
            name: readString((obj.emoji as { name?: unknown }).name, `${label}.emoji.name`),
            id: readOptionalString((obj.emoji as { id?: unknown }).id),
            animated:
              typeof (obj.emoji as { animated?: unknown }).animated === "boolean"
                ? (obj.emoji as { animated?: boolean }).animated
                : undefined,
          }
        : undefined,
    disabled: typeof obj.disabled === "boolean" ? obj.disabled : undefined,
    allowedUsers: readOptionalStringArray(obj.allowedUsers, `${label}.allowedUsers`),
  };
}

function parseSelectSpec(raw: unknown, label: string): DiscordComponentSelectSpec {
  const obj = requireObject(raw, label);
  const type = readOptionalString(obj.type) as DiscordComponentSelectType | undefined;
  const allowedTypes: DiscordComponentSelectType[] = [
    "string",
    "user",
    "role",
    "mentionable",
    "channel",
  ];
  if (type && !allowedTypes.includes(type)) {
    throw new Error(`${label}.type must be one of ${allowedTypes.join(", ")}`);
  }
  return {
    type,
    placeholder: readOptionalString(obj.placeholder),
    minValues: readOptionalNumber(obj.minValues),
    maxValues: readOptionalNumber(obj.maxValues),
    options: parseSelectOptions(obj.options, `${label}.options`),
  };
}

function parseModalField(raw: unknown, label: string, index: number): DiscordModalFieldSpec {
  const obj = requireObject(raw, label);
  const type = readString(
    obj.type,
    `${label}.type`,
  ).toLowerCase() as DiscordComponentModalFieldType;
  const supported: DiscordComponentModalFieldType[] = [
    "text",
    "checkbox",
    "radio",
    "select",
    "role-select",
    "user-select",
  ];
  if (!supported.includes(type)) {
    throw new Error(`${label}.type must be one of ${supported.join(", ")}`);
  }
  const options = parseSelectOptions(obj.options, `${label}.options`);
  if (["checkbox", "radio", "select"].includes(type) && (!options || options.length === 0)) {
    throw new Error(`${label}.options is required for ${type} fields`);
  }
  return {
    type,
    name: normalizeModalFieldName(readOptionalString(obj.name), index),
    label: readString(obj.label, `${label}.label`),
    description: readOptionalString(obj.description),
    placeholder: readOptionalString(obj.placeholder),
    required: typeof obj.required === "boolean" ? obj.required : undefined,
    options,
    minValues: readOptionalNumber(obj.minValues),
    maxValues: readOptionalNumber(obj.maxValues),
    minLength: readOptionalNumber(obj.minLength),
    maxLength: readOptionalNumber(obj.maxLength),
    style: readOptionalString(obj.style) as DiscordModalFieldSpec["style"],
  };
}

function parseComponentBlock(raw: unknown, label: string): DiscordComponentBlock {
  const obj = requireObject(raw, label);
  const typeRaw = readString(obj.type, `${label}.type`).toLowerCase();
  const type = normalizeBlockType(typeRaw);
  switch (type) {
    case "text":
      return {
        type: "text",
        text: readString(obj.text, `${label}.text`),
      };
    case "section": {
      const text = readOptionalString(obj.text);
      const textsRaw = obj.texts;
      const texts = Array.isArray(textsRaw)
        ? textsRaw.map((entry, idx) => readString(entry, `${label}.texts[${idx}]`))
        : undefined;
      if (!text && (!texts || texts.length === 0)) {
        throw new Error(`${label}.text or ${label}.texts is required for section blocks`);
      }
      let accessory: DiscordComponentSectionAccessory | undefined;
      if (obj.accessory !== undefined) {
        const accessoryObj = requireObject(obj.accessory, `${label}.accessory`);
        const accessoryType = readString(
          accessoryObj.type,
          `${label}.accessory.type`,
        ).toLowerCase();
        if (accessoryType === "thumbnail") {
          accessory = {
            type: "thumbnail",
            url: readString(accessoryObj.url, `${label}.accessory.url`),
          };
        } else if (accessoryType === "button") {
          accessory = {
            type: "button",
            button: parseButtonSpec(accessoryObj.button, `${label}.accessory.button`),
          };
        } else {
          throw new Error(`${label}.accessory.type must be "thumbnail" or "button"`);
        }
      }
      return {
        type: "section",
        text,
        texts,
        accessory,
      };
    }
    case "separator": {
      const spacingRaw = obj.spacing;
      let spacing: DiscordComponentSeparatorSpacing | undefined;
      if (spacingRaw === "small" || spacingRaw === "large") {
        spacing = spacingRaw;
      } else if (spacingRaw === 1 || spacingRaw === 2) {
        spacing = spacingRaw;
      } else if (spacingRaw !== undefined) {
        throw new Error(`${label}.spacing must be "small", "large", 1, or 2`);
      }
      const divider = typeof obj.divider === "boolean" ? obj.divider : undefined;
      return {
        type: "separator",
        spacing,
        divider,
      };
    }
    case "actions": {
      const buttonsRaw = obj.buttons;
      const buttons = Array.isArray(buttonsRaw)
        ? buttonsRaw.map((entry, idx) => parseButtonSpec(entry, `${label}.buttons[${idx}]`))
        : undefined;
      const select = obj.select ? parseSelectSpec(obj.select, `${label}.select`) : undefined;
      if ((!buttons || buttons.length === 0) && !select) {
        throw new Error(`${label} requires buttons or select`);
      }
      if (buttons && select) {
        throw new Error(`${label} cannot include both buttons and select`);
      }
      return {
        type: "actions",
        buttons,
        select,
      };
    }
    case "media-gallery": {
      const itemsRaw = obj.items;
      if (!Array.isArray(itemsRaw) || itemsRaw.length === 0) {
        throw new Error(`${label}.items must be a non-empty array`);
      }
      const items = itemsRaw.map((entry, idx) => {
        const itemObj = requireObject(entry, `${label}.items[${idx}]`);
        return {
          url: readString(itemObj.url, `${label}.items[${idx}].url`),
          description: readOptionalString(itemObj.description),
          spoiler: typeof itemObj.spoiler === "boolean" ? itemObj.spoiler : undefined,
        };
      });
      return {
        type: "media-gallery",
        items,
      };
    }
    case "file": {
      const file = readString(obj.file, `${label}.file`);
      return {
        type: "file",
        file: normalizeAttachmentRef(file, `${label}.file`),
        spoiler: typeof obj.spoiler === "boolean" ? obj.spoiler : undefined,
      };
    }
    default:
      throw new Error(`${label}.type must be a supported component block`);
  }
}

export function readDiscordComponentSpec(raw: unknown): DiscordComponentMessageSpec | null {
  if (raw === undefined || raw === null) {
    return null;
  }
  const obj = requireObject(raw, "components");
  const blocksRaw = obj.blocks;
  const blocks = Array.isArray(blocksRaw)
    ? blocksRaw.map((entry, idx) => parseComponentBlock(entry, `components.blocks[${idx}]`))
    : undefined;
  const modalRaw = obj.modal;
  const reusable = typeof obj.reusable === "boolean" ? obj.reusable : undefined;
  let modal: DiscordModalSpec | undefined;
  if (modalRaw !== undefined) {
    const modalObj = requireObject(modalRaw, "components.modal");
    const fieldsRaw = modalObj.fields;
    if (!Array.isArray(fieldsRaw) || fieldsRaw.length === 0) {
      throw new Error("components.modal.fields must be a non-empty array");
    }
    if (fieldsRaw.length > 5) {
      throw new Error("components.modal.fields supports up to 5 inputs");
    }
    const fields = fieldsRaw.map((entry, idx) =>
      parseModalField(entry, `components.modal.fields[${idx}]`, idx),
    );
    modal = {
      title: readString(modalObj.title, "components.modal.title"),
      triggerLabel: readOptionalString(modalObj.triggerLabel),
      triggerStyle: readOptionalString(modalObj.triggerStyle) as DiscordComponentButtonStyle,
      fields,
    };
  }
  return {
    text: readOptionalString(obj.text),
    reusable,
    container:
      typeof obj.container === "object" && obj.container && !Array.isArray(obj.container)
        ? {
            accentColor: (obj.container as { accentColor?: unknown }).accentColor as
              | string
              | number
              | undefined,
            spoiler:
              typeof (obj.container as { spoiler?: unknown }).spoiler === "boolean"
                ? ((obj.container as { spoiler?: boolean }).spoiler as boolean)
                : undefined,
          }
        : undefined,
    blocks,
    modal,
  };
}

export function buildDiscordComponentCustomId(params: {
  componentId: string;
  modalId?: string;
}): string {
  const base = `${DISCORD_COMPONENT_CUSTOM_ID_KEY}:cid=${params.componentId}`;
  return params.modalId ? `${base};mid=${params.modalId}` : base;
}

export function buildDiscordModalCustomId(modalId: string): string {
  return `${DISCORD_MODAL_CUSTOM_ID_KEY}:mid=${modalId}`;
}

export function parseDiscordComponentCustomId(
  id: string,
): { componentId: string; modalId?: string } | null {
  const parsed = parseCustomId(id);
  if (parsed.key !== DISCORD_COMPONENT_CUSTOM_ID_KEY) {
    return null;
  }
  const componentId = parsed.data.cid;
  if (typeof componentId !== "string" || !componentId.trim()) {
    return null;
  }
  const modalId = parsed.data.mid;
  return {
    componentId,
    modalId: typeof modalId === "string" && modalId.trim() ? modalId : undefined,
  };
}

export function parseDiscordModalCustomId(id: string): string | null {
  const parsed = parseCustomId(id);
  if (parsed.key !== DISCORD_MODAL_CUSTOM_ID_KEY) {
    return null;
  }
  const modalId = parsed.data.mid;
  if (typeof modalId !== "string" || !modalId.trim()) {
    return null;
  }
  return modalId;
}

function isDiscordComponentWildcardRegistrationId(id: string): boolean {
  return /^__openclaw_discord_component_[a-z_]+_wildcard__$/.test(id);
}

export function parseDiscordComponentCustomIdForCarbon(id: string): ComponentParserResult {
  if (id === "*" || isDiscordComponentWildcardRegistrationId(id)) {
    return { key: "*", data: {} };
  }
  const parsed = parseCustomId(id);
  if (parsed.key !== DISCORD_COMPONENT_CUSTOM_ID_KEY) {
    return parsed;
  }
  return { key: "*", data: parsed.data };
}

export function parseDiscordModalCustomIdForCarbon(id: string): ComponentParserResult {
  if (id === "*" || isDiscordComponentWildcardRegistrationId(id)) {
    return { key: "*", data: {} };
  }
  const parsed = parseCustomId(id);
  if (parsed.key !== DISCORD_MODAL_CUSTOM_ID_KEY) {
    return parsed;
  }
  return { key: "*", data: parsed.data };
}

function buildTextDisplays(text?: string, texts?: string[]): TextDisplay[] {
  if (texts && texts.length > 0) {
    return texts.map((entry) => new TextDisplay(entry));
  }
  if (text) {
    return [new TextDisplay(text)];
  }
  return [];
}

function createButtonComponent(params: {
  spec: DiscordComponentButtonSpec;
  componentId?: string;
  modalId?: string;
}): { component: Button | LinkButton; entry?: DiscordComponentEntry } {
  const style = mapButtonStyle(params.spec.style);
  const isLink = style === ButtonStyle.Link || Boolean(params.spec.url);
  if (isLink) {
    if (!params.spec.url) {
      throw new Error("Link buttons require a url");
    }
    const linkUrl = params.spec.url;
    class DynamicLinkButton extends LinkButton {
      label = params.spec.label;
      url = linkUrl;
    }
    return { component: new DynamicLinkButton() };
  }
  const componentId = params.componentId ?? createShortId("btn_");
  const customId = buildDiscordComponentCustomId({
    componentId,
    modalId: params.modalId,
  });
  class DynamicButton extends Button {
    label = params.spec.label;
    customId = customId;
    style = style;
    emoji = params.spec.emoji;
    disabled = params.spec.disabled ?? false;
  }
  return {
    component: new DynamicButton(),
    entry: {
      id: componentId,
      kind: params.modalId ? "modal-trigger" : "button",
      label: params.spec.label,
      modalId: params.modalId,
      allowedUsers: params.spec.allowedUsers,
    },
  };
}

function createSelectComponent(params: {
  spec: DiscordComponentSelectSpec;
  componentId?: string;
}): {
  component:
    | StringSelectMenu
    | UserSelectMenu
    | RoleSelectMenu
    | MentionableSelectMenu
    | ChannelSelectMenu;
  entry: DiscordComponentEntry;
} {
  const type = (params.spec.type ?? "string").toLowerCase() as DiscordComponentSelectType;
  const componentId = params.componentId ?? createShortId("sel_");
  const customId = buildDiscordComponentCustomId({ componentId });
  if (type === "string") {
    const options = params.spec.options ?? [];
    if (options.length === 0) {
      throw new Error("String select menus require options");
    }
    class DynamicStringSelect extends StringSelectMenu {
      customId = customId;
      options = options;
      minValues = params.spec.minValues;
      maxValues = params.spec.maxValues;
      placeholder = params.spec.placeholder;
      disabled = false;
    }
    return {
      component: new DynamicStringSelect(),
      entry: {
        id: componentId,
        kind: "select",
        label: params.spec.placeholder ?? "select",
        selectType: "string",
        options: options.map((option) => ({ value: option.value, label: option.label })),
      },
    };
  }
  if (type === "user") {
    class DynamicUserSelect extends UserSelectMenu {
      customId = customId;
      minValues = params.spec.minValues;
      maxValues = params.spec.maxValues;
      placeholder = params.spec.placeholder;
      disabled = false;
    }
    return {
      component: new DynamicUserSelect(),
      entry: {
        id: componentId,
        kind: "select",
        label: params.spec.placeholder ?? "user select",
        selectType: "user",
      },
    };
  }
  if (type === "role") {
    class DynamicRoleSelect extends RoleSelectMenu {
      customId = customId;
      minValues = params.spec.minValues;
      maxValues = params.spec.maxValues;
      placeholder = params.spec.placeholder;
      disabled = false;
    }
    return {
      component: new DynamicRoleSelect(),
      entry: {
        id: componentId,
        kind: "select",
        label: params.spec.placeholder ?? "role select",
        selectType: "role",
      },
    };
  }
  if (type === "mentionable") {
    class DynamicMentionableSelect extends MentionableSelectMenu {
      customId = customId;
      minValues = params.spec.minValues;
      maxValues = params.spec.maxValues;
      placeholder = params.spec.placeholder;
      disabled = false;
    }
    return {
      component: new DynamicMentionableSelect(),
      entry: {
        id: componentId,
        kind: "select",
        label: params.spec.placeholder ?? "mentionable select",
        selectType: "mentionable",
      },
    };
  }
  class DynamicChannelSelect extends ChannelSelectMenu {
    customId = customId;
    minValues = params.spec.minValues;
    maxValues = params.spec.maxValues;
    placeholder = params.spec.placeholder;
    disabled = false;
  }
  return {
    component: new DynamicChannelSelect(),
    entry: {
      id: componentId,
      kind: "select",
      label: params.spec.placeholder ?? "channel select",
      selectType: "channel",
    },
  };
}

function isSelectComponent(
  component: unknown,
): component is
  | StringSelectMenu
  | UserSelectMenu
  | RoleSelectMenu
  | MentionableSelectMenu
  | ChannelSelectMenu {
  return (
    component instanceof StringSelectMenu ||
    component instanceof UserSelectMenu ||
    component instanceof RoleSelectMenu ||
    component instanceof MentionableSelectMenu ||
    component instanceof ChannelSelectMenu
  );
}

function createModalFieldComponent(
  field: DiscordModalFieldDefinition,
): TextInput | StringSelectMenu | UserSelectMenu | RoleSelectMenu | CheckboxGroup | RadioGroup {
  if (field.type === "text") {
    class DynamicTextInput extends TextInput {
      customId = field.id;
      style = mapTextInputStyle(field.style);
      placeholder = field.placeholder;
      required = field.required;
      minLength = field.minLength;
      maxLength = field.maxLength;
    }
    return new DynamicTextInput();
  }
  if (field.type === "select") {
    const options = field.options ?? [];
    class DynamicModalSelect extends StringSelectMenu {
      customId = field.id;
      options = options;
      required = field.required;
      minValues = field.minValues;
      maxValues = field.maxValues;
      placeholder = field.placeholder;
    }
    return new DynamicModalSelect();
  }
  if (field.type === "role-select") {
    class DynamicModalRoleSelect extends RoleSelectMenu {
      customId = field.id;
      required = field.required;
      minValues = field.minValues;
      maxValues = field.maxValues;
      placeholder = field.placeholder;
    }
    return new DynamicModalRoleSelect();
  }
  if (field.type === "user-select") {
    class DynamicModalUserSelect extends UserSelectMenu {
      customId = field.id;
      required = field.required;
      minValues = field.minValues;
      maxValues = field.maxValues;
      placeholder = field.placeholder;
    }
    return new DynamicModalUserSelect();
  }
  if (field.type === "checkbox") {
    const options = field.options ?? [];
    class DynamicCheckboxGroup extends CheckboxGroup {
      customId = field.id;
      options = options;
      required = field.required;
      minValues = field.minValues;
      maxValues = field.maxValues;
    }
    return new DynamicCheckboxGroup();
  }
  const options = field.options ?? [];
  class DynamicRadioGroup extends RadioGroup {
    customId = field.id;
    options = options;
    required = field.required;
    minValues = field.minValues;
    maxValues = field.maxValues;
  }
  return new DynamicRadioGroup();
}

export function buildDiscordComponentMessage(params: {
  spec: DiscordComponentMessageSpec;
  fallbackText?: string;
  sessionKey?: string;
  agentId?: string;
  accountId?: string;
}): DiscordComponentBuildResult {
  const entries: DiscordComponentEntry[] = [];
  const modals: DiscordModalEntry[] = [];
  const components: TopLevelComponents[] = [];
  const containerChildren: Array<
    | Row<
        | Button
        | LinkButton
        | StringSelectMenu
        | UserSelectMenu
        | RoleSelectMenu
        | MentionableSelectMenu
        | ChannelSelectMenu
      >
    | TextDisplay
    | Section
    | MediaGallery
    | Separator
    | File
  > = [];

  const addEntry = (entry: DiscordComponentEntry) => {
    entries.push({
      ...entry,
      sessionKey: params.sessionKey,
      agentId: params.agentId,
      accountId: params.accountId,
      reusable: entry.reusable ?? params.spec.reusable,
    });
  };

  const text = params.spec.text ?? params.fallbackText;
  if (text) {
    containerChildren.push(new TextDisplay(text));
  }

  for (const block of params.spec.blocks ?? []) {
    if (block.type === "text") {
      containerChildren.push(new TextDisplay(block.text));
      continue;
    }
    if (block.type === "section") {
      const displays = buildTextDisplays(block.text, block.texts);
      if (displays.length > 3) {
        throw new Error("Section blocks support up to 3 text displays");
      }
      let accessory: Thumbnail | Button | LinkButton | undefined;
      if (block.accessory?.type === "thumbnail") {
        accessory = new Thumbnail(block.accessory.url);
      } else if (block.accessory?.type === "button") {
        const { component, entry } = createButtonComponent({ spec: block.accessory.button });
        accessory = component;
        if (entry) {
          addEntry(entry);
        }
      }
      containerChildren.push(new Section(displays, accessory));
      continue;
    }
    if (block.type === "separator") {
      containerChildren.push(new Separator({ spacing: block.spacing, divider: block.divider }));
      continue;
    }
    if (block.type === "media-gallery") {
      containerChildren.push(new MediaGallery(block.items));
      continue;
    }
    if (block.type === "file") {
      containerChildren.push(new File(block.file, block.spoiler));
      continue;
    }
    if (block.type === "actions") {
      const rowComponents: Array<
        | Button
        | LinkButton
        | StringSelectMenu
        | UserSelectMenu
        | RoleSelectMenu
        | MentionableSelectMenu
        | ChannelSelectMenu
      > = [];
      if (block.buttons) {
        if (block.buttons.length > 5) {
          throw new Error("Action rows support up to 5 buttons");
        }
        for (const button of block.buttons) {
          const { component, entry } = createButtonComponent({ spec: button });
          rowComponents.push(component);
          if (entry) {
            addEntry(entry);
          }
        }
      } else if (block.select) {
        const { component, entry } = createSelectComponent({ spec: block.select });
        rowComponents.push(component);
        addEntry(entry);
      }
      containerChildren.push(new Row(rowComponents));
    }
  }

  if (params.spec.modal) {
    const modalId = createShortId("mdl_");
    const fields = params.spec.modal.fields.map((field, index) => ({
      id: createShortId("fld_"),
      name: normalizeModalFieldName(field.name, index),
      label: field.label,
      type: field.type,
      description: field.description,
      placeholder: field.placeholder,
      required: field.required,
      options: field.options,
      minValues: field.minValues,
      maxValues: field.maxValues,
      minLength: field.minLength,
      maxLength: field.maxLength,
      style: field.style,
    }));
    modals.push({
      id: modalId,
      title: params.spec.modal.title,
      fields,
      sessionKey: params.sessionKey,
      agentId: params.agentId,
      accountId: params.accountId,
      reusable: params.spec.reusable,
    });

    const triggerSpec: DiscordComponentButtonSpec = {
      label: params.spec.modal.triggerLabel ?? "Open form",
      style: params.spec.modal.triggerStyle ?? "primary",
    };

    const { component, entry } = createButtonComponent({
      spec: triggerSpec,
      modalId,
    });

    if (entry) {
      addEntry(entry);
    }

    const lastChild = containerChildren.at(-1);
    if (lastChild instanceof Row) {
      const row = lastChild;
      const hasSelect = row.components.some((entry) => isSelectComponent(entry));
      if (row.components.length < 5 && !hasSelect) {
        row.addComponent(component as Button);
      } else {
        containerChildren.push(new Row([component as Button]));
      }
    } else {
      containerChildren.push(new Row([component as Button]));
    }
  }

  if (containerChildren.length === 0) {
    throw new Error("components must include at least one block, text, or modal trigger");
  }

  const container = new Container(containerChildren, params.spec.container);
  components.push(container);
  return { components, entries, modals };
}

export function buildDiscordComponentMessageFlags(
  components: TopLevelComponents[],
): number | undefined {
  const hasV2 = components.some((component) => component.isV2);
  return hasV2 ? MessageFlags.IsComponentsV2 : undefined;
}

export class DiscordFormModal extends Modal {
  title: string;
  customId: string;
  components: Array<Label | TextDisplay>;
  customIdParser = parseDiscordModalCustomIdForCarbon;

  constructor(params: { modalId: string; title: string; fields: DiscordModalFieldDefinition[] }) {
    super();
    this.title = params.title;
    this.customId = buildDiscordModalCustomId(params.modalId);
    this.components = params.fields.map((field) => {
      const component = createModalFieldComponent(field);
      class DynamicLabel extends Label {
        label = field.label;
        description = field.description;
        component = component;
        customId = field.id;
      }
      return new DynamicLabel(component);
    });
  }

  async run(): Promise<void> {
    throw new Error("Modal handler is not registered for dynamic forms");
  }
}

export function createDiscordFormModal(entry: DiscordModalEntry): Modal {
  return new DiscordFormModal({
    modalId: entry.id,
    title: entry.title,
    fields: entry.fields,
  });
}

export function formatDiscordComponentEventText(params: {
  kind: "button" | "select";
  label: string;
  values?: string[];
}): string {
  if (params.kind === "button") {
    return `Clicked "${params.label}".`;
  }
  const values = params.values ?? [];
  if (values.length === 0) {
    return `Updated "${params.label}".`;
  }
  return `Selected ${values.join(", ")} from "${params.label}".`;
}
