'use strict';
const { randomUUID } = require('node:crypto');
const { ActivityTypes } = require('../util/Constants');
const { resolvePartialEmoji } = require('../util/Util');

// eslint-disable-next-line
const checkUrl = url => {
  try {
    return new URL(url);
  } catch {
    return false;
  }
};

class CustomStatus {
  /**
   * @typedef {Object} CustomStatusOptions
   * @property {string} [state] The state to be displayed
   * @property {EmojiIdentifierResolvable} [emoji] The emoji to be displayed
   */

  /**
   * @param {CustomStatus|CustomStatusOptions} [data={}] CustomStatus to clone or raw data
   * @param {Presence} [presence] The presence this activity is part of
   */
  constructor(data = {}, presence) {
    Object.defineProperty(this, 'presence', { value: presence });
    this.name = 'Custom Status';
    /**
     * The emoji to be displayed
     * @type {?EmojiIdentifierResolvable}
     */
    this.emoji = null;
    this.type = ActivityTypes.CUSTOM;
    /**
     * The state to be displayed
     * @type {?string}
     */
    this.state = null;
    this.setup(data);
  }
  /**
   * Sets the status from a JSON object
   * @param {CustomStatus|CustomStatusOptions} data CustomStatus to clone or raw data
   * @private
   */
  setup(data) {
    this.emoji = data.emoji ? resolvePartialEmoji(data.emoji) : null;
    this.state = data.state;
  }
  /**
   * Set the emoji of this activity
   * @param {EmojiIdentifierResolvable} emoji The emoji to be displayed
   * @returns {CustomStatus}
   */
  setEmoji(emoji) {
    this.emoji = resolvePartialEmoji(emoji);
    return this;
  }
  /**
   * Set state of this activity
   * @param {string | null} state The state to be displayed
   * @returns {CustomStatus}
   */
  setState(state) {
    if (typeof state == 'string' && state.length > 128) throw new Error('State must be less than 128 characters');
    this.state = state;
    return this;
  }

  /**
   * Returns an object that can be used to set the status
   * @returns {CustomStatus}
   */
  toJSON() {
    if (!this.emoji & !this.state) throw new Error('CustomStatus must have at least one of emoji or state');
    return {
      name: this.name,
      emoji: this.emoji,
      type: this.type,
      state: this.state,
    };
  }

  /**
   * When concatenated with a string, this automatically returns the activities' name instead of the Activity object.
   * @returns {string}
   */
  toString() {
    return this.name;
  }

  _clone() {
    return Object.assign(Object.create(this), this);
  }
}

class RichPresence {
  /**
   * @param {Client} [client] Discord client
   * @param {RichPresence} [data={}] RichPresence to clone or raw data
   * @param {boolean} [IPC=false] Whether to use IPC (RPC for Discord Apps)
   * @param {Presence} [presence] The presence this activity is part of
   */
  constructor(client = {}, data = {}, IPC = false, presence) {
    Object.defineProperty(this, 'client', { value: client });
    Object.defineProperty(this, 'presence', { value: presence });
    /**
     * The activity's name
     * @type {string}
     */
    this.name = null;
    /**
     * The activity status's type
     * @type {ActivityType}
     */
    this.type = ActivityTypes.PLAYING;
    /**
     * If the activity is being streamed, a link to the stream
     * @type {?string}
     */
    this.url = null;
    /**
     * The id of the application associated with this activity
     * @type {?Snowflake}
     */
    this.application_id = null;
    /**
     * State of the activity
     * @type {?string}
     */
    this.state = null;
    /**
     * Details about the activity
     * @type {?string}
     */
    this.details = null;
    /**
     * Party of the activity
     * @type {?ActivityParty}
     */
    this.party = null;
    /**
     * Timestamps for the activity
     * @type {?ActivityTimestamps}
     */
    this.timestamps = null;
    /**
     * Assets for rich presence
     * @type {?RichPresenceAssets}
     */
    this.assets = null;
    /**
     * The labels of the buttons of this rich presence
     * @type {string[]}
     */
    this.buttons = null;

    this.ipc = IPC;

    this.setup(data);
  }
  /**
   * Sets the status from a JSON object
   * @param {RichPresence} data data
   * @private
   */
  setup(data) {
    this.name = data.name;
    this.type = typeof data.type != 'number' ? ActivityTypes[data.type?.toUpperCase()] : data.type;
    this.application_id = data.application_id;
    this.url = data.url;
    this.state = data.state;
    this.details = data.details;
    this.party = data.party;
    this.timestamps = data.timestamps;
    this.created_at = data.created_at;
    this.secrets = data.secrets;
    this.assets = data.assets;
    this.buttons = data.buttons;
    this.metadata = data.metadata;
  }
  /**
   * @typedef {string} RichPresenceImage
   * Support:
   * - cdn.discordapp.com
   * - media.discordapp.net
   * - Asset ID (From https://discord.com/api/v9/oauth2/applications/:id/assets)
   * - ExternalAssets (mp:external/)
   */
  /**
   * Set the large image of this activity
   * @param {?RichPresenceImage} image The large image asset's id
   * @returns {RichPresence}
   */
  setAssetsLargeImage(image) {
    if (!(this.assets instanceof Object)) this.assets = {};
    if (typeof image != 'string') {
      image = null;
    } else if (['http:', 'https:'].includes(checkUrl(image)?.protocol)) {
      // Discord URL:
      image = image
        .replace('https://cdn.discordapp.com/', 'mp:')
        .replace('http://cdn.discordapp.com/', 'mp:')
        .replace('https://media.discordapp.net/', 'mp:')
        .replace('http://media.discordapp.net/', 'mp:');
      //
      if (!image.startsWith('mp:') && !this.ipc) {
        throw new Error('INVALID_URL');
      }
    } else if (/^[0-9]{17,19}$/.test(image)) {
      // ID Assets
    } else if (image.startsWith('mp:') || image.startsWith('youtube:') || image.startsWith('spotify:')) {
      // Image
    } else if (image.startsWith('external/')) {
      image = `mp:${image}`;
    }
    this.assets.large_image = image;
    return this;
  }
  /**
   * Set the small image of this activity
   * @param {?RichPresenceImage} image The small image asset's id
   * @returns {RichPresence}
   */
  setAssetsSmallImage(image) {
    if (!(this.assets instanceof Object)) this.assets = {};
    if (typeof image != 'string') {
      image = null;
    } else if (['http:', 'https:'].includes(checkUrl(image)?.protocol)) {
      // Discord URL:
      image = image
        .replace('https://cdn.discordapp.com/', 'mp:')
        .replace('http://cdn.discordapp.com/', 'mp:')
        .replace('https://media.discordapp.net/', 'mp:')
        .replace('http://media.discordapp.net/', 'mp:');
      //
      if (!image.startsWith('mp:') && !this.ipc) {
        throw new Error('INVALID_URL');
      }
    } else if (/^[0-9]{17,19}$/.test(image)) {
      // ID Assets
    } else if (image.startsWith('mp:') || image.startsWith('youtube:') || image.startsWith('spotify:')) {
      // Image
    } else if (image.startsWith('external/')) {
      image = `mp:${image}`;
    }
    this.assets.small_image = image;
    return this;
  }
  /**
   * Hover text for the large image
   * @param {string} text Assets text
   * @returns {RichPresence}
   */
  setAssetsLargeText(text) {
    if (typeof this.assets !== 'object') this.assets = {};
    this.assets.large_text = text;
    return this;
  }
  /**
   * Hover text for the small image
   * @param {string} text Assets text
   * @returns {RichPresence}
   */
  setAssetsSmallText(text) {
    if (typeof this.assets !== 'object') this.assets = {};
    this.assets.small_text = text;
    return this;
  }
  /**
   * Set the name of the activity
   * @param {?string} name The activity's name
   * @returns {RichPresence}
   */
  setName(name) {
    this.name = name;
    return this;
  }
  /**
   * If the activity is being streamed, a link to the stream
   * @param {?string} url URL of the stream
   * @returns {RichPresence}
   */
  setURL(url) {
    if (typeof url == 'string' && !checkUrl(url)) throw new Error('URL must be a valid URL');
    if (typeof url != 'string') url = null;
    this.url = url;
    return this;
  }
  /**
   * The activity status's type
   * @param {?ActivityTypes} type The type of activity
   * @returns {RichPresence}
   */
  setType(type) {
    this.type = ActivityTypes[type];
    if (typeof this.type == 'string') this.type = ActivityTypes[this.type];
    if (typeof this.type != 'number') throw new Error('Type must be a valid ActivityType');
    return this;
  }
  /**
   * Set the application id of this activity
   * @param {?Snowflake} id Bot's id
   * @returns {RichPresence}
   */
  setApplicationId(id) {
    this.application_id = id;
    return this;
  }
  /**
   * Set the state of the activity
   * @param {?string} state The state of the activity
   * @returns {RichPresence}
   */
  setState(state) {
    this.state = state;
    return this;
  }
  /**
   * Set the details of the activity
   * @param {?string} details The details of the activity
   * @returns {RichPresence}
   */
  setDetails(details) {
    this.details = details;
    return this;
  }
  /**
   * @typedef {Object} RichParty
   * @property {string} id The id of the party
   * @property {number} max The maximum number of members in the party
   * @property {number} current The current number of members in the party
   */
  /**
   * Set the party of this activity
   * @param {?RichParty} party The party to be displayed
   * @returns {RichPresence}
   */
  setParty(party) {
    if (typeof party == 'object') {
      if (!party.max || typeof party.max != 'number') throw new Error('Party must have max number');
      if (!party.current || typeof party.current != 'number') throw new Error('Party must have current');
      if (party.current > party.max) throw new Error('Party current must be less than max number');
      if (!party.id || typeof party.id != 'string') party.id = randomUUID();
      this.party = {
        size: [party.current, party.max],
        id: party.id,
      };
    } else {
      this.party = null;
    }
    return this;
  }
  /**
   * Sets the start timestamp of the activity
   * @param {?number} timestamp The timestamp of the start of the activity
   * @returns {RichPresence}
   */
  setStartTimestamp(timestamp) {
    if (!this.timestamps) this.timestamps = {};
    this.timestamps.start = timestamp;
    return this;
  }
  /**
   * Sets the end timestamp of the activity
   * @param {?number} timestamp The timestamp of the end of the activity
   * @returns {RichPresence}
   */
  setEndTimestamp(timestamp) {
    if (!this.timestamps) this.timestamps = {};
    this.timestamps.end = timestamp;
    return this;
  }
  /**
   * @typedef {object} RichButton
   * @property {string} name The name of the button
   * @property {string} url The url of the button
   */
  /**
   * Set the buttons of the rich presence
   * @param  {...?RichButton} button A list of buttons to set
   * @returns {RichPresence}
   */
  setButtons(...button) {
    if (button.length == 0) {
      this.buttons = null;
      delete this.metadata;
      return this;
    } else if (button.length > 2) {
      throw new Error('RichPresence can only have up to 2 buttons');
    }
    this.buttons = [];
    this.metadata = {
      button_urls: [],
    };
    button.flat(2).forEach(b => {
      if (b.name && b.url) {
        this.buttons.push(b.name);
        if (!checkUrl(b.url)) throw new Error('Button url must be a valid url');
        this.metadata.button_urls.push(b.url);
      } else {
        throw new Error('Button must have name and url');
      }
    });
    return this;
  }
  /**
   * Add a button to the rich presence
   * @param {string} name The name of the button
   * @param {string} url The url of the button
   * @returns {RichPresence}
   */
  addButton(name, url) {
    if (!name || !url) {
      throw new Error('Button must have name and url');
    }
    if (typeof name !== 'string') throw new Error('Button name must be a string');
    if (!checkUrl(url)) throw new Error('Button url must be a valid url');
    if (!this.buttons) {
      this.buttons = [];
      this.metadata = {
        button_urls: [],
      };
    }
    this.buttons.push(name);
    this.metadata.button_urls.push(url);
    return this;
  }
  /**
   * Convert the rich presence to a JSON object
   * @returns {Object}
   */
  toJSON() {
    /**
     * * Verify Timestamps
     */
    if (this.timestamps?.start || this.timestamps?.end) {
      if (this.timestamps?.start instanceof Date) {
        this.timestamps.start = Math.round(this.timestamps?.start.getTime());
      }
      if (this.timestamps.end instanceof Date) {
        this.timestamps.end = Math.round(this.timestamps.end.getTime());
      }
      if (this.timestamps.start > 2147483647000) {
        throw new RangeError('timestamps.start must fit into a unix timestamp');
      }
      if (this.timestamps.end > 2147483647000) {
        throw new RangeError('timestamps.end must fit into a unix timestamp');
      }
    }
    const obj = {
      name: this.name,
      type: this.type || 0, // PLAYING
      application_id: this.application_id,
      url: this.url,
      state: this.state,
      details: this.details,
      party: this.party,
      timestamps: this.timestamps || {},
      secrets: this.secrets,
      assets: this.assets || {},
      buttons: this.buttons,
      metadata: this.metadata,
    };
    Object.keys(obj).forEach(key => obj[key] === undefined && delete obj[key]);
    if (!this.ipc) {
      return obj;
    } else {
      delete obj.application_id;
      delete obj.name;
      delete obj.url;
      obj.type = 0;
      if (obj.buttons?.length) {
        obj.buttons = obj.buttons.map((b, i) => ({ label: b, url: obj.metadata.button_urls[i] }));
        delete obj.metadata;
      }
      return obj;
    }
  }

  /**
   * Get Assets from a RichPresence (Util)
   * @param {Client} client Discord Client
   * @param {Snowflake} applicationId Application id
   * @param {string} image1 URL image 1 (not from Discord)
   * @param {string} image2 URL image 2 (not from Discord)
   * @returns {ExternalAssets[]}
   */
  static async getExternal(client, applicationId, image1 = '', image2 = '') {
    const checkURL = url => {
      try {
        // eslint-disable-next-line no-new
        new URL(url);
        return true;
      } catch (e) {
        return false;
      }
    };
    if (!client || !client.token || !client.api) throw new Error('Client must be set');
    // Check if applicationId is discord snowflake (17 , 18, 19 numbers)
    if (!/^[0-9]{17,19}$/.test(applicationId)) {
      throw new Error('Application id must be a Discord Snowflake');
    }
    // Check if large_image is a valid url
    if (image1 && image1.length > 0 && !checkURL(image1)) {
      throw new Error('Image 1 must be a valid url');
    }
    // Check if small_image is a valid url
    if (image2 && image2.length > 0 && !checkURL(image2)) {
      throw new Error('Image 2 must be a valid url');
    }
    const data_ = [];
    if (image1) data_.push(image1);
    if (image2) data_.push(image2);
    const res = await client.api.applications[applicationId]['external-assets'].post({
      data: {
        urls: data_,
      },
    });
    return res;
  }

  /**
   * When concatenated with a string, this automatically returns the activities' name instead of the Activity object.
   * @returns {string}
   */
  toString() {
    return this.name;
  }

  _clone() {
    return Object.assign(Object.create(this), this);
  }
}

/**
 * @extends {RichPresence}
 */
class SpotifyRPC extends RichPresence {
  /**
   * Create a new RichPresence (Spotify style)
   * @param {Client} client Discord Client
   * @param {SpotifyRPC} options Options for the Spotify RPC
   * @param {Presence} presence Presence
   */
  constructor(client, options = {}, presence) {
    if (!client) throw new Error('Client must be set');
    super(client, options, false, presence);
    this.setup(options);
  }
  /**
   * Sets the status from a JSON object
   * @param {SpotifyRPC} options data
   * @private
   */
  setup(options) {
    this.name = options.name || 'Spotify';

    this.type = ActivityTypes.LISTENING;

    this.party = {
      id: `spotify:${this.client.user.id}`,
    };
    /**
     * The Spotify song's id
     * @type {?string}
     */
    this.sync_id = options.sync_id;
    /**
     * The activity's id
     * @type {string}
     */
    this.id = 'spotify:1';
    /**
     * Flags that describe the activity
     * @type {ActivityFlags}
     */
    this.flags = 48; // Sync + Play (ActivityFlags)

    /**
     * @typedef {Object} SpotifyMetadata
     * @property {string} album_id Album id
     * @property {Array<string>} artist_ids Artist ids
     */

    /**
     * Spotify metadata
     * @type {SpotifyMetadata}
     */
    this.metadata = {
      album_id: options.metadata?.album_id || null,
      artist_ids: options.metadata?.artist_ids || [],
      context_uri: null,
    };
  }

  /**
   * Set the large image of this activity
   * @param {?string} image Spotify song's image ID
   * @returns {SpotifyRPC}
   */
  setAssetsLargeImage(image) {
    if (image.startsWith('spotify:')) image = image.replace('spotify:', '');
    super.setAssetsLargeImage(`spotify:${image}`);
    return this;
  }

  /**
   * Set the small image of this activity
   * @param {?string} image Spotify song's image ID
   * @returns {RichPresence}
   */
  setAssetsSmallImage(image) {
    if (image.startsWith('spotify:')) image = image.replace('spotify:', '');
    super.setAssetsSmallImage(`spotify:${image}`);
    return this;
  }

  /**
   * Set Spotify song id to sync with
   * @param {string} id Song id
   * @returns {SpotifyRPC}
   */
  setSongId(id) {
    this.sync_id = id;
    return this;
  }

  /**
   * Add the artist id
   * @param {string} id Artist id
   * @returns {SpotifyRPC}
   */
  addArtistId(id) {
    if (!this.metadata.artist_ids) this.metadata.artist_ids = [];
    this.metadata.artist_ids.push(id);
    return this;
  }

  /**
   * Set the artist ids
   * @param {string | Array<string>} ids Artist ids
   * @returns {SpotifyRPC}
   */
  setArtistIds(...ids) {
    if (!ids?.length) {
      this.metadata.artist_ids = [];
      return this;
    }
    if (!this.metadata.artist_ids) this.metadata.artist_ids = [];
    ids.flat(2).forEach(id => this.metadata.artist_ids.push(id));
    return this;
  }

  /**
   * Set the album id
   * @param {string} id Album id
   * @returns {SpotifyRPC}
   */
  setAlbumId(id) {
    this.metadata.album_id = id;
    this.metadata.context_uri = `spotify:album:${id}`;
    return this;
  }

  /**
   * Convert the rich presence to a JSON object
   * @returns {SpotifyRPC}
   */
  toJSON() {
    if (!this.sync_id) throw new Error('Song id is required');
    const obj = {
      name: this.name,
      type: this.type,
      application_id: this.application_id,
      url: this.url,
      state: this.state,
      details: this.details,
      party: this.party,
      timestamps: this.timestamps || {},
      assets: this.assets || {},
      sync_id: this.sync_id,
      flags: this.flags,
      metadata: this.metadata,
    };
    Object.keys(obj).forEach(key => obj[key] === undefined && delete obj[key]);
    return obj;
  }
}

/**
 * @typedef {Object} ExternalAssets
 * @property {?string} url Orginal url of the image
 * @property {?string} external_asset_path Proxy url of the image (Using to RPC)
 */

module.exports = {
  CustomStatus,
  RichPresence,
  SpotifyRPC,
};
