// Discord plugin module implements components.parse behavior.
import { ButtonStyle, TextInputStyle } from "discord-api-types/v10";
import {
  normalizeLowercaseStringOrEmpty,
  normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type {
  DiscordComponentBlock,
  DiscordComponentButtonSpec,
  DiscordComponentButtonStyle,
  DiscordComponentCallbackDataKind,
  DiscordComponentMessageSpec,
  DiscordComponentModalFieldType,
  DiscordComponentSectionAccessory,
  DiscordComponentSelectOption,
  DiscordComponentSelectSpec,
  DiscordComponentSelectType,
  DiscordModalFieldSpec,
  DiscordModalSpec,
} from "./components.types.js";

export const DISCORD_COMPONENT_ATTACHMENT_PREFIX = "attachment://";

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

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

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 readRequiredString(
  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 readOptionalCallbackDataKind(
  value: unknown,
  label: string,
): DiscordComponentCallbackDataKind | undefined {
  const kind = normalizeOptionalString(value);
  if (kind === undefined) {
    return undefined;
  }
  if (kind === "command" || kind === "callback") {
    return kind;
  }
  throw new Error(`${label} must be one of command, callback`);
}

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) => readRequiredString(entry, `${label}[${index}]`));
}

function readOptionalInteger(
  value: unknown,
  label: string,
  bounds?: { min?: number; max?: number },
): number | undefined {
  if (value == null) {
    return undefined;
  }
  if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
    throw new Error(`${label} must be an integer`);
  }
  if (bounds?.min !== undefined && value < bounds.min) {
    throw new Error(`${label} must be at least ${bounds.min}`);
  }
  if (bounds?.max !== undefined && value > bounds.max) {
    throw new Error(`${label} must be at most ${bounds.max}`);
  }
  return value;
}

function readOptionalEmoji(value: unknown, label: string) {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return undefined;
  }
  const obj = value as { name?: unknown; id?: unknown; animated?: unknown };
  return {
    name: readRequiredString(obj.name, `${label}.name`),
    id: normalizeOptionalString(obj.id),
    animated: typeof obj.animated === "boolean" ? obj.animated : undefined,
  };
}

export 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;
}

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

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

function normalizeBlockType(raw: string) {
  const lowered = normalizeLowercaseStringOrEmpty(raw);
  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: readRequiredString(obj.label, `${label}[${index}].label`),
      value: readRequiredString(obj.value, `${label}[${index}].value`),
      description: normalizeOptionalString(obj.description),
      emoji: readOptionalEmoji(obj.emoji, `${label}[${index}].emoji`),
      default: typeof obj.default === "boolean" ? obj.default : undefined,
    };
  });
}

function parseButtonSpec(raw: unknown, label: string): DiscordComponentButtonSpec {
  const obj = requireObject(raw, label);
  const style = normalizeOptionalString(obj.style) as DiscordComponentButtonStyle | undefined;
  const url = normalizeOptionalString(obj.url);
  if ((style === "link" || url) && !url) {
    throw new Error(`${label}.url is required for link buttons`);
  }
  return {
    label: readRequiredString(obj.label, `${label}.label`),
    style,
    url,
    callbackData: normalizeOptionalString(obj.callbackData),
    callbackDataKind: readOptionalCallbackDataKind(
      obj.callbackDataKind,
      `${label}.callbackDataKind`,
    ),
    emoji: readOptionalEmoji(obj.emoji, `${label}.emoji`),
    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 = normalizeOptionalString(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,
    callbackData: normalizeOptionalString(obj.callbackData),
    callbackDataKind: readOptionalCallbackDataKind(
      obj.callbackDataKind,
      `${label}.callbackDataKind`,
    ),
    placeholder: normalizeOptionalString(obj.placeholder),
    minValues: readOptionalInteger(obj.minValues, `${label}.minValues`, { min: 0, max: 25 }),
    maxValues: readOptionalInteger(obj.maxValues, `${label}.maxValues`, { min: 1, max: 25 }),
    options: parseSelectOptions(obj.options, `${label}.options`),
    allowedUsers: readOptionalStringArray(obj.allowedUsers, `${label}.allowedUsers`),
  };
}

function parseModalField(raw: unknown, label: string, index: number): DiscordModalFieldSpec {
  const obj = requireObject(raw, label);
  const type = normalizeLowercaseStringOrEmpty(
    readRequiredString(obj.type, `${label}.type`),
  ) 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`);
  }
  if (type === "radio" && (obj.minValues != null || obj.maxValues != null)) {
    throw new Error(`${label}.minValues/maxValues are not supported for radio fields`);
  }
  const required = typeof obj.required === "boolean" ? obj.required : undefined;
  const maxValues = type === "checkbox" ? 10 : 25;
  return {
    type,
    name: normalizeModalFieldName(normalizeOptionalString(obj.name), index),
    label: readRequiredString(obj.label, `${label}.label`),
    description: normalizeOptionalString(obj.description),
    placeholder: normalizeOptionalString(obj.placeholder),
    required,
    options,
    minValues: readOptionalInteger(obj.minValues, `${label}.minValues`, {
      min: required === false ? 0 : 1,
      max: maxValues,
    }),
    maxValues: readOptionalInteger(obj.maxValues, `${label}.maxValues`, {
      min: 1,
      max: maxValues,
    }),
    minLength: readOptionalInteger(obj.minLength, `${label}.minLength`, { min: 0, max: 4000 }),
    maxLength: readOptionalInteger(obj.maxLength, `${label}.maxLength`, { min: 1, max: 4000 }),
    style: normalizeOptionalString(obj.style) as DiscordModalFieldSpec["style"],
  };
}

function parseComponentBlock(raw: unknown, label: string): DiscordComponentBlock {
  const obj = requireObject(raw, label);
  const typeRaw = normalizeLowercaseStringOrEmpty(readRequiredString(obj.type, `${label}.type`));
  const type = normalizeBlockType(typeRaw);
  switch (type) {
    case "text":
      return {
        type: "text",
        text: readRequiredString(obj.text, `${label}.text`),
      };
    case "section": {
      const text = normalizeOptionalString(obj.text);
      const textsRaw = obj.texts;
      const texts = Array.isArray(textsRaw)
        ? textsRaw.map((entry, idx) => readRequiredString(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 = normalizeLowercaseStringOrEmpty(
          readRequiredString(accessoryObj.type, `${label}.accessory.type`),
        );
        if (accessoryType === "thumbnail") {
          accessory = {
            type: "thumbnail",
            url: readRequiredString(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: readRequiredString(itemObj.url, `${label}.items[${idx}].url`),
          description: normalizeOptionalString(itemObj.description),
          spoiler: typeof itemObj.spoiler === "boolean" ? itemObj.spoiler : undefined,
        };
      });
      return {
        type: "media-gallery",
        items,
      };
    }
    case "file": {
      const file = readRequiredString(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: readRequiredString(modalObj.title, "components.modal.title"),
      callbackData: normalizeOptionalString(modalObj.callbackData),
      triggerLabel: normalizeOptionalString(modalObj.triggerLabel),
      triggerStyle: normalizeOptionalString(modalObj.triggerStyle) as DiscordComponentButtonStyle,
      allowedUsers: readOptionalStringArray(modalObj.allowedUsers, "components.modal.allowedUsers"),
      fields,
    };
  }
  return {
    text: normalizeOptionalString(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,
  };
}
