text: 'use strict';
  
  const Collection = require('../util/Collection');
  const Base = require('./Base');
  const BaseMessageComponent = require('./BaseMessageComponent');
  const ClientApplication = require('./ClientApplication');
  const InteractionCollector = require('./InteractionCollector');
  const MessageAttachment = require('./MessageAttachment');
  const Embed = require('./MessageEmbed');
  const Mentions = require('./MessageMentions');
  const MessagePayload = require('./MessagePayload');
  const ReactionCollector = require('./ReactionCollector');
  const { Error } = require('../errors');
  const ReactionManager = require('../managers/ReactionManager');
  const { InteractionTypes, MessageTypes, SystemMessageTypes } = require('../util/Constants');
  const MessageFlags = require('../util/MessageFlags');
  const Permissions = require('../util/Permissions');
  const SnowflakeUtil = require('../util/SnowflakeUtil');
  const Util = require('../util/Util');
  
  let Structures;
  
  /**
   * Represents a message on Discord.
   * @extends {Base}
   */
  class Message extends Base {
    constructor(client, data) {
      super(client);
  
      /**
       * The id of the channel the message was sent in
       * @type {Snowflake}
       */
      this.channelId = data.channel_id;
  
      /**
       * The id of the guild the message was sent in, if any
       * @type {?Snowflake}
       */
      this.guildId = data.guild_id ?? this.channel?.guild?.id ?? null;
  
      /**
       * Whether this message has been deleted
       * @type {boolean}
       */
      this.deleted = false;
  
      this._patch(data);
    }
  
    _patch(data) {
      /**
       * The message's id
       * @type {Snowflake}
       */
      this.id = data.id;
  
      /**
       * The timestamp the message was sent at
       * @type {number}
       */
      this.createdTimestamp = SnowflakeUtil.deconstruct(this.id).timestamp;
  
      if ('type' in data) {
        /**
         * The type of the message
         * @type {?MessageType}
         */
        this.type = MessageTypes[data.type];
  
        /**
         * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
         * @type {?boolean}
         */
        this.system = SystemMessageTypes.includes(this.type);
      } else {
        this.system ??= null;
        this.type ??= null;
      }
  
      if ('content' in data) {
        /**
         * The content of the message
         * @type {?string}
         */
        this.content = data.content;
      } else {
        this.content ??= null;
      }
  
      if ('author' in data) {
        /**
         * The author of the message
         * @type {?User}
         */
        this.author = this.client.users._add(data.author, !data.webhook_id);
      } else {
        this.author ??= null;
      }
  
      if ('pinned' in data) {
        /**
         * Whether or not this message is pinned
         * @type {?boolean}
         */
        this.pinned = Boolean(data.pinned);
      } else {
        this.pinned ??= null;
      }
  
      if ('tts' in data) {
        /**
         * Whether or not the message was Text-To-Speech
         * @type {?boolean}
         */
        this.tts = data.tts;
      } else {
        this.tts ??= null;
      }
  
      if ('nonce' in data) {
        /**
         * A random number or string used for checking message delivery
         * <warn>This is only received after the message was sent successfully, and
         * lost if re-fetched</warn>
         * @type {?string}
         */
        this.nonce = data.nonce;
      } else {
        this.nonce ??= null;
      }
  
      if ('embeds' in data) {
        /**
         * A list of embeds in the message - e.g. YouTube Player
         * @type {MessageEmbed[]}
         */
        this.embeds = data.embeds.map(e => new Embed(e, true));
      } else {
        this.embeds = this.embeds?.slice() ?? [];
      }
  
      if ('components' in data) {
        /**
         * A list of MessageActionRows in the message
         * @type {MessageActionRow[]}
         */
        this.components = data.components.map(c => BaseMessageComponent.create(c, this.client));
      } else {
        this.components = this.components?.slice() ?? [];
      }
  
      if ('attachments' in data) {
        /**
         * A collection of attachments in the message - e.g. Pictures - mapped by their ids
         * @type {Collection<Snowflake, MessageAttachment>}
         */
        this.attachments = new Collection();
        if (data.attachments) {
          for (const attachment of data.attachments) {
            this.attachments.set(attachment.id, new MessageAttachment(attachment.url, attachment.filename, attachment));
          }
        }
      } else {
        this.attachments = new Collection(this.attachments);
      }
  
      if ('sticker_items' in data || 'stickers' in data) {
        if (!Structures) Structures = require('../util/Structures');
        const Sticker = Structures.get('Sticker');
        /**
         * A collection of stickers in the message
         * @type {Collection<Snowflake, Sticker>}
         */
        this.stickers = new Collection(
          (data.sticker_items ?? data.stickers)?.map(s => [s.id, new Sticker(this.client, s)]),
        );
      } else {
        this.stickers = new Collection(this.stickers);
      }
  
      // Discord sends null if the message has not been edited
      if (data.edited_timestamp) {
        /**
         * The timestamp the message was last edited at (if applicable)
         * @type {?number}
         */
        this.editedTimestamp = new Date(data.edited_timestamp).getTime();
      } else {
        this.editedTimestamp ??= null;
      }
  
      if ('reactions' in data) {
        /**
         * A manager of the reactions belonging to this message
         * @type {ReactionManager}
         */
        this.reactions = new ReactionManager(this);
        if (data.reactions?.length > 0) {
          for (const reaction of data.reactions) {
            this.reactions._add(reaction);
          }
        }
      } else {
        this.reactions ??= new ReactionManager(this);
      }
  
      if (!this.mentions) {
        /**
         * All valid mentions that the message contains
         * @type {MessageMentions}
         */
        this.mentions = new Mentions(
          this,
          data.mentions,
          data.mention_roles,
          data.mention_everyone,
          data.mention_channels,
          data.referenced_message?.author,
        );
      } else {
        this.mentions = new Mentions(
          this,
          data.mentions ?? this.mentions.users,
          data.mention_roles ?? this.mentions.roles,
          data.mention_everyone ?? this.mentions.everyone,
          data.mention_channels ?? this.mentions.crosspostedChannels,
          data.referenced_message?.author ?? this.mentions.repliedUser,
        );
      }
  
      if ('webhook_id' in data) {
        /**
         * The id of the webhook that sent the message, if applicable
         * @type {?Snowflake}
         */
        this.webhookId = data.webhook_id;
      } else {
        this.webhookId ??= null;
      }
  
      if ('application' in data) {
        /**
         * Supplemental application information for group activities
         * @type {?ClientApplication}
         */
        this.groupActivityApplication = new ClientApplication(this.client, data.application);
      } else {
        this.groupActivityApplication ??= null;
      }
  
      if ('application_id' in data) {
        /**
         * The id of the application of the interaction that sent this message, if any
         * @type {?Snowflake}
         */
        this.applicationId = data.application_id;
      } else {
        this.applicationId ??= null;
      }
  
      if ('activity' in data) {
        /**
         * Group activity
         * @type {?MessageActivity}
         */
        this.activity = {
          partyId: data.activity.party_id,
          type: data.activity.type,
        };
      } else {
        this.activity ??= null;
      }
  
      if ('thread' in data) {
        this.client.channels._add(data.thread, this.guild);
      }
  
      if (this.member && data.member) {
        this.member._patch(data.member);
      } else if (data.member && this.guild && this.author) {
        this.guild.members._add(Object.assign(data.member, { user: this.author }));
      }
  
      if ('flags' in data) {
        /**
         * Flags that are applied to the message
         * @type {Readonly<MessageFlags>}
         */
        this.flags = new MessageFlags(data.flags).freeze();
      } else {
        this.flags = new MessageFlags(this.flags).freeze();
      }
  
      /**
       * Reference data sent in a message that contains ids identifying the referenced message.
       * This can be present in the following types of message:
       * * Crossposted messages (IS_CROSSPOST {@link MessageFlags#FLAGS message flag})
       * * CHANNEL_FOLLOW_ADD
       * * CHANNEL_PINNED_MESSAGE
       * * REPLY
       * * THREAD_STARTER_MESSAGE
       * @see {@link https://discord.com/developers/docs/resources/channel#message-types}
       * @typedef {Object} MessageReference
       * @property {Snowflake} channelId The channel's id the message was referenced
       * @property {?Snowflake} guildId The guild's id the message was referenced
       * @property {?Snowflake} messageId The message's id that was referenced
       */
  
      if ('message_reference' in data) {
        /**
         * Message reference data
         * @type {?MessageReference}
         */
        this.reference = {
          channelId: data.message_reference.channel_id,
          guildId: data.message_reference.guild_id,
          messageId: data.message_reference.message_id,
        };
      } else {
        this.reference ??= null;
      }
  
      if (data.referenced_message) {
        this.channel?.messages._add({ guild_id: data.message_reference?.guild_id, ...data.referenced_message });
      }
  
      /**
       * Partial data of the interaction that a message is a reply to
       * @typedef {Object} MessageInteraction
       * @property {Snowflake} id The interaction's id
       * @property {InteractionType} type The type of the interaction
       * @property {string} commandName The name of the interaction's application command
       * @property {User} user The user that invoked the interaction
       */
  
      if (data.interaction) {
        /**
         * Partial data of the interaction that this message is a reply to
         * @type {?MessageInteraction}
         */
        this.interaction = {
          id: data.interaction.id,
          type: InteractionTypes[data.interaction.type],
          commandName: data.interaction.name,
          user: this.client.users._add(data.interaction.user),
        };
      } else {
        this.interaction ??= null;
      }
  
      if (data.member && !this.member && this.author) {
        this._member = this.guild.members._add(Object.assign(data.member, { user: this.author }));
      }
    }
  
    /**
     * The channel that the message was sent in
     * @type {TextChannel|DMChannel|NewsChannel|ThreadChannel}
     * @readonly
     */
    get channel() {
      return Util.getOrCreateChannel(this.client, this.channelId, this.guild);
    }
  
    /**
     * Whether or not this message is a partial
     * @type {boolean}
     * @readonly
     */
    get partial() {
      return typeof this.content !== 'string' || !this.author;
    }
  
    /**
     * Represents the author of the message as a guild member.
     * Only available if the message comes from a guild where the author is still a member
     * @type {?GuildMember}
     * @readonly
     */
    get member() {
      if (!this.guild) {
        return null;
      }
      const id = this.author?.id ?? this._member?.id;
      if (!id) {
        return null;
      }
      return this.guild.members.cache.get(id) ?? this._member ?? null;
    }
  
    /**
     * The time the message was sent at
     * @type {Date}
     * @readonly
     */
    get createdAt() {
      return new Date(this.createdTimestamp);
    }
  
    /**
     * The time the message was last edited at (if applicable)
     * @type {?Date}
     * @readonly
     */
    get editedAt() {
      return this.editedTimestamp ? new Date(this.editedTimestamp) : null;
    }
  
    /**
     * The guild the message was sent in (if in a guild channel)
     * @type {?Guild}
     * @readonly
     */
    get guild() {
      return this.guildId ? Util.getOrCreateGuild(this.client, this.guildId) : null;
    }
  
    /**
     * Whether this message has a thread associated with it
     * @type {boolean}
     * @readonly
     */
    get hasThread() {
      return this.flags.has(MessageFlags.FLAGS.HAS_THREAD);
    }
  
    /**
     * The thread started by this message
     * <info>This property is not suitable for checking whether a message has a thread,
     * use {@link Message#hasThread} instead.</info>
     * @type {?ThreadChannel}
     * @readonly
     */
    get thread() {
      return this.channel?.threads?.resolve(this.id) ?? null;
    }
  
    /**
     * The URL to jump to this message
     * @type {string}
     * @readonly
     */
    get url() {
      return `https://discord.com/channels/${this.guildId ?? '@me'}/${this.channelId}/${this.id}`;
    }
  
    /**
     * The message contents with all mentions replaced by the equivalent text.
     * If mentions cannot be resolved to a name, the relevant mention in the message content will not be converted.
     * @type {?string}
     * @readonly
     */
    get cleanContent() {
      // eslint-disable-next-line eqeqeq
      return this.content != null ? Util.cleanContent(this.content, this.channel) : null;
    }
  
    /**
     * Creates a reaction collector.
     * @param {ReactionCollectorOptions} [options={}] Options to send to the collector
     * @returns {ReactionCollector}
     * @example
     * // Create a reaction collector
     * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId';
     * const collector = message.createReactionCollector({ filter, time: 15_000 });
     * collector.on('collect', r => console.log(`Collected ${r.emoji.name}`));
     * collector.on('end', collected => console.log(`Collected ${collected.size} items`));
     */
    createReactionCollector(options = {}) {
      return new ReactionCollector(this, options);
    }
  
    /**
     * An object containing the same properties as CollectorOptions, but a few more:
     * @typedef {ReactionCollectorOptions} AwaitReactionsOptions
     * @property {string[]} [errors] Stop/end reasons that cause the promise to reject
     */
  
    /**
     * Similar to createReactionCollector but in promise form.
     * Resolves with a collection of reactions that pass the specified filter.
     * @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector
     * @returns {Promise<Collection<string | Snowflake, MessageReaction>>}
     * @example
     * // Create a reaction collector
     * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId'
     * message.awaitReactions({ filter, time: 15_000 })
     *   .then(collected => console.log(`Collected ${collected.size} reactions`))
     *   .catch(console.error);
     */
    awaitReactions(options = {}) {
      return new Promise((resolve, reject) => {
        const collector = this.createReactionCollector(options);
        collector.once('end', (reactions, reason) => {
          if (options.errors?.includes(reason)) reject(reactions);
          else resolve(reactions);
        });
      });
    }
  
    /**
     * @typedef {CollectorOptions} MessageComponentCollectorOptions
     * @property {MessageComponentType} [componentType] The type of component to listen for
     * @property {number} [max] The maximum total amount of interactions to collect
     * @property {number} [maxComponents] The maximum number of components to collect
     * @property {number} [maxUsers] The maximum number of users to interact
     */
  
    /**
     * Creates a message component interaction collector.
     * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector
     * @returns {InteractionCollector}
     * @example
     * // Create a message component interaction collector
     * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId';
     * const collector = message.createMessageComponentCollector({ filter, time: 15_000 });
     * collector.on('collect', i => console.log(`Collected ${i.customId}`));
     * collector.on('end', collected => console.log(`Collected ${collected.size} items`));
     */
    createMessageComponentCollector(options = {}) {
      return new InteractionCollector(this.client, {
        ...options,
        interactionType: InteractionTypes.MESSAGE_COMPONENT,
        message: this,
      });
    }
  
    /**
     * An object containing the same properties as CollectorOptions, but a few more:
     * @typedef {Object} AwaitMessageComponentOptions
     * @property {CollectorFilter} [filter] The filter applied to this collector
     * @property {number} [time] Time to wait for an interaction before rejecting
     * @property {MessageComponentType} [componentType] The type of component interaction to collect
     */
  
    /**
     * Collects a single component interaction that passes the filter.
     * The Promise will reject if the time expires.
     * @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector
     * @returns {Promise<MessageComponentInteraction>}
     * @example
     * // Collect a message component interaction
     * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId';
     * message.awaitMessageComponent({ filter, time: 15_000 })
     *   .then(interaction => console.log(`${interaction.customId} was clicked!`))
     *   .catch(console.error);
     */
    awaitMessageComponent(options = {}) {
      const _options = { ...options, max: 1 };
      return new Promise((resolve, reject) => {
        const collector = this.createMessageComponentCollector(_options);
        collector.once('end', (interactions, reason) => {
          const interaction = interactions.first();
          if (interaction) resolve(interaction);
          else reject(new Error('INTERACTION_COLLECTOR_ERROR', reason));
        });
      });
    }
  
    /**
     * Whether the message is editable by the client user
     * @type {boolean}
     * @readonly
     */
    get editable() {
      return Boolean(this.author.id === this.client.user.id && !this.deleted && (!this.guild || this.channel?.viewable));
    }
  
    /**
     * Whether the message is deletable by the client user
     * @type {boolean}
     * @readonly
     */
    get deletable() {
      if (this.deleted) {
        return false;
      }
      if (!this.guild) {
        return this.author.id === this.client.user.id;
      }
      // DMChannel does not have viewable property, so check viewable after proved that message is on a guild.
      if (!this.channel?.viewable) {
        return false;
      }
      return Boolean(
        this.author.id === this.client.user.id ||
          this.channel?.permissionsFor(this.client.user)?.has(Permissions.FLAGS.MANAGE_MESSAGES, false),
      );
    }
  
    /**
     * Whether the message is pinnable by the client user
     * @type {boolean}
     * @readonly
     */
    get pinnable() {
      const { channel } = this;
      return Boolean(
        !this.system &&
          !this.deleted &&
          (!this.guild ||
            (channel?.viewable &&
              channel?.permissionsFor(this.client.user)?.has(Permissions.FLAGS.MANAGE_MESSAGES, false))),
      );
    }
  
    /**
     * Fetches the Message this crosspost/reply/pin-add references, if available to the client
     * @returns {Promise<Message>}
     */
    async fetchReference() {
      if (!this.reference) throw new Error('MESSAGE_REFERENCE_MISSING');
      const { channelId, messageId } = this.reference;
      const channel = this.client.channels.resolve(channelId);
      if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE');
      const message = await channel.messages.fetch(messageId);
      return message;
    }
  
    /**
     * Whether the message is crosspostable by the client user
     * @type {boolean}
     * @readonly
     */
    get crosspostable() {
      const bitfield =
        Permissions.FLAGS.SEND_MESSAGES |
        (this.author.id === this.client.user.id ? Permissions.defaultBit : Permissions.FLAGS.MANAGE_MESSAGES);
      const { channel } = this;
      return Boolean(
        channel?.type === 'GUILD_NEWS' &&
          !this.flags.has(MessageFlags.FLAGS.CROSSPOSTED) &&
          this.type === 'DEFAULT' &&
          channel.viewable &&
          channel.permissionsFor(this.client.user)?.has(bitfield, false) &&
          !this.deleted,
      );
    }
  
    /**
     * Options that can be passed into {@link Message#edit}.
     * @typedef {Object} MessageEditOptions
     * @property {?string} [content] Content to be edited
     * @property {MessageEmbed[]|APIEmbed[]} [embeds] Embeds to be added/edited
     * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content
     * @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be edited.
     * @property {MessageAttachment[]} [attachments] An array of attachments to keep,
     * all attachments will be kept if omitted
     * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to add to the message
     * @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
     * Action rows containing interactive components for the message (buttons, select menus)
     */
  
    /**
     * Edits the content of the message.
     * @param {string|MessagePayload|MessageEditOptions} options The options to provide
     * @returns {Promise<Message>}
     * @example
     * // Update the content of a message
     * message.edit('This is my new content!')
     *   .then(msg => console.log(`Updated the content of a message to ${msg.content}`))
     *   .catch(console.error);
     */
    edit(options) {
      if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
      return this.channel.messages.edit(this, options);
    }
  
    /**
     * Publishes a message in an announcement channel to all channels following it.
     * @returns {Promise<Message>}
     * @example
     * // Crosspost a message
     * if (message.channel.type === 'GUILD_NEWS') {
     *   message.crosspost()
     *     .then(() => console.log('Crossposted message'))
     *     .catch(console.error);
     * }
     */
    crosspost() {
      if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
      return this.channel.messages.crosspost(this.id);
    }
  
    /**
     * Pins this message to the channel's pinned messages.
     * @returns {Promise<Message>}
     * @example
     * // Pin a message
     * message.pin()
     *   .then(console.log)
     *   .catch(console.error)
     */
    async pin() {
      if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
      await this.channel.messages.pin(this.id);
      return this;
    }
  
    /**
     * Unpins this message from the channel's pinned messages.
     * @returns {Promise<Message>}
     * @example
     * // Unpin a message
     * message.unpin()
     *   .then(console.log)
     *   .catch(console.error)
     */
    async unpin() {
      if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
      await this.channel.messages.unpin(this.id);
      return this;
    }
  
    /**
     * Adds a reaction to the message.
     * @param {EmojiIdentifierResolvable} emoji The emoji to react with
     * @returns {Promise<MessageReaction>}
     * @example
     * // React to a message with a unicode emoji
     * message.react('🤔')
     *   .then(console.log)
     *   .catch(console.error);
     * @example
     * // React to a message with a custom emoji
     * message.react(message.guild.emojis.cache.get('123456789012345678'))
     *   .then(console.log)
     *   .catch(console.error);
     */
    async react(emoji) {
      if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
      await this.channel.messages.react(this.id, emoji);
  
      return this.client.actions.MessageReactionAdd.handle(
        {
          user: this.client.user,
          channel: this.channel,
          message: this,
          emoji: Util.resolvePartialEmoji(emoji),
        },
        true,
      ).reaction;
    }
  
    /**
     * Deletes the message.
     * @returns {Promise<Message>}
     * @example
     * // Delete a message
     * message.delete()
     *   .then(msg => console.log(`Deleted message from ${msg.author.username}`))
     *   .catch(console.error);
     */
    async delete() {
      if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
      await this.channel.messages.delete(this.id);
      return this;
    }
  
    /**
     * Options provided when sending a message as an inline reply.
     * @typedef {BaseMessageOptions} ReplyMessageOptions
     * @property {boolean} [failIfNotExists=true] Whether to error if the referenced message
     * does not exist (creates a standard message in this case when false)
     */
  
    /**
     * Send an inline reply to this message.
     * @param {string|MessagePayload|ReplyMessageOptions} options The options to provide
     * @returns {Promise<Message>}
     * @example
     * // Reply to a message
     * message.reply('This is a reply!')
     *   .then(() => console.log(`Replied to message "${message.content}"`))
     *   .catch(console.error);
     */
    reply(options) {
      if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
      let data;
  
      if (options instanceof MessagePayload) {
        data = options;
      } else {
        data = MessagePayload.create(this, options, {
          reply: {
            messageReference: this,
            failIfNotExists: options?.failIfNotExists ?? this.client.options.failIfNotExists,
          },
        });
      }
      return this.channel.send(data);
    }
  
    /**
     * A number that is allowed to be the duration (in minutes) of inactivity after which a thread is automatically
     * archived. This can be:
     * * `60` (1 hour)
     * * `1440` (1 day)
     * * `4320` (3 days) <warn>This is only available when the guild has the `THREE_DAY_THREAD_ARCHIVE` feature.</warn>
     * * `10080` (7 days) <warn>This is only available when the guild has the `SEVEN_DAY_THREAD_ARCHIVE` feature.</warn>
     * * `'MAX'` Based on the guild's features
     * @typedef {number|string} ThreadAutoArchiveDuration
     */
  
    /**
     * Options for starting a thread on a message.
     * @typedef {Object} StartThreadOptions
     * @property {string} name The name of the new thread
     * @property {ThreadAutoArchiveDuration} [autoArchiveDuration=this.channel.defaultAutoArchiveDuration] The amount of
     * time (in minutes) after which the thread should automatically archive in case of no recent activity
     * @property {string} [reason] Reason for creating the thread
     */
  
    /**
     * Create a new public thread from this message
     * @see ThreadManager#create
     * @param {StartThreadOptions} [options] Options for starting a thread on this message
     * @returns {Promise<ThreadChannel>}
     */
    startThread(options = {}) {
      if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
      if (!['GUILD_TEXT', 'GUILD_NEWS'].includes(this.channel.type)) {
        return Promise.reject(new Error('MESSAGE_THREAD_PARENT'));
      }
      if (this.hasThread) return Promise.reject(new Error('MESSAGE_EXISTING_THREAD'));
      return this.channel.threads.create({ ...options, startMessage: this });
    }
  
    /**
     * Fetch this message.
     * @param {boolean} [force=true] Whether to skip the cache check and request the API
     * @returns {Promise<Message>}
     */
    fetch(force = true) {
      if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
      return this.channel.messages.fetch(this.id, { force });
    }
  
    /**
     * Fetches the webhook used to create this message.
     * @returns {Promise<?Webhook>}
     */
    fetchWebhook() {
      if (!this.webhookId) return Promise.reject(new Error('WEBHOOK_MESSAGE'));
      if (this.webhookId === this.applicationId) return Promise.reject(new Error('WEBHOOK_APPLICATION'));
      return this.client.fetchWebhook(this.webhookId);
    }
  
    /**
     * Suppresses or unsuppresses embeds on a message.
     * @param {boolean} [suppress=true] If the embeds should be suppressed or not
     * @returns {Promise<Message>}
     */
    suppressEmbeds(suppress = true) {
      const flags = new MessageFlags(this.flags.bitfield);
  
      if (suppress) {
        flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS);
      } else {
        flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS);
      }
  
      return this.edit({ flags });
    }
  
    /**
     * Removes the attachments from this message.
     * @returns {Promise<Message>}
     */
    removeAttachments() {
      return this.edit({ attachments: [] });
    }
  
    /**
     * Resolves a component by a custom id.
     * @param {string} customId The custom id to resolve against
     * @returns {?MessageActionRowComponent}
     */
    resolveComponent(customId) {
      return this.components.flatMap(row => row.components).find(component => component.customId === customId) ?? null;
    }
  
    /**
     * Used mainly internally. Whether two messages are identical in properties. If you want to compare messages
     * without checking all the properties, use `message.id === message2.id`, which is much more efficient. This
     * method allows you to see if there are differences in content, embeds, attachments, nonce and tts properties.
     * @param {Message} message The message to compare it to
     * @param {APIMessage} rawData Raw data passed through the WebSocket about this message
     * @returns {boolean}
     */
    equals(message, rawData) {
      if (!message) return false;
      const embedUpdate = !message.author && !message.attachments;
      if (embedUpdate) return this.id === message.id && this.embeds.length === message.embeds.length;
  
      let equal =
        this.id === message.id &&
        this.author.id === message.author.id &&
        this.content === message.content &&
        this.tts === message.tts &&
        this.nonce === message.nonce &&
        this.embeds.length === message.embeds.length &&
        this.attachments.length === message.attachments.length;
  
      if (equal && rawData) {
        equal =
          this.mentions.everyone === message.mentions.everyone &&
          this.createdTimestamp === new Date(rawData.timestamp).getTime() &&
          this.editedTimestamp === new Date(rawData.edited_timestamp).getTime();
      }
  
      return equal;
    }
  
    /**
     * Whether this message is from a guild.
     * @returns {boolean}
     */
    inGuild() {
      return Boolean(this.guildId);
    }
  
    /**
     * When concatenated with a string, this automatically concatenates the message's content instead of the object.
     * @returns {string}
     * @example
     * // Logs: Message: This is a message!
     * console.log(`Message: ${message}`);
     */
    toString() {
      return this.content;
    }
  
    toJSON() {
      return super.toJSON({
        channel: 'channelId',
        author: 'authorId',
        groupActivityApplication: 'groupActivityApplicationId',
        guild: 'guildId',
        cleanContent: true,
        member: false,
        reactions: false,
      });
    }
  }
  
  module.exports = Message;
  