// Copyright (C) 2022-2026 IITC-CE - GPL-3.0 with Store Exception - see LICENSE and COPYING.STORE

import { Worker } from './worker.js';
import * as migrations from './migrations.js';
import { getUID, isSet, sanitizeFileName } from './helpers.js';
import * as backup from './backup.js';

/**
 * @classdesc This class contains methods for managing IITC and plugins.
 */
export class Manager extends Worker {
  /**
   * Changes the update channel and calls for an update.
   *
   * @async
   * @param {"release" | "beta" | "custom"} channel - Update channel for IITC and plugins.
   * @return {Promise<void>}
   */
  async setChannel(channel) {
    // Get active plugins from current channel and notify about removal
    const oldEnabledPlugins = await this.getEnabledPlugins();
    await this._sendPluginsEvent(channel, Object.keys(oldEnabledPlugins), 'remove');

    // Change channel in storage and object
    this.channel = channel;
    await this._save(channel, { channel: channel, last_check_update: null });

    // Ensure minimal data structures exist for new channel
    const newChannelData = await this.storage.get([
      `${channel}_plugins_flat`,
      `${channel}_plugins_local`,
      `${channel}_plugins_user`,
    ]);

    // Initialize missing structures if needed
    const updates = {};
    if (!newChannelData[`${channel}_plugins_flat`]) updates[`${channel}_plugins_flat`] = {};
    if (!newChannelData[`${channel}_plugins_local`]) updates[`${channel}_plugins_local`] = {};
    if (!newChannelData[`${channel}_plugins_user`]) updates[`${channel}_plugins_user`] = {};

    // Save initializations if needed
    if (Object.keys(updates).length > 0) {
      await this.storage.set(updates);
    }

    // Get active plugins from new channel and notify about addition
    const newEnabledPlugins = await this.getEnabledPlugins();
    await this._sendPluginsEvent(channel, Object.keys(newEnabledPlugins), 'add');

    await this.checkUpdates(true);
  }

  /**
   * Changes the update check interval. If the interval for the current channel changes, a forced update check is started to apply the new interval.
   *
   * @async
   * @param {number} interval - Update check interval in seconds.
   * @param {"release" | "beta" | "custom" | undefined} [channel=undefined] - Update channel for IITC and plugins.
   * If not specified, the current channel is used.
   * @return {Promise<void>}
   */
  async setUpdateCheckInterval(interval, channel) {
    if (typeof channel === 'undefined') channel = this.channel;

    const data = {};
    data[channel + '_update_check_interval'] = interval;
    await this.storage.set(data);

    if (channel === this.channel) await this.checkUpdates(true);
  }

  /**
   * Changes the URL of the repository with IITC and plugins for the custom channel.
   *
   * @async
   * @param {string} url - URL of the repository.
   * @return {Promise<void>}
   */
  async setCustomChannelUrl(url) {
    const network_host = await this.storage.get(['network_host']).then(data => data.network_host);
    network_host.custom = url;
    await this.storage.set({ network_host: network_host });
    this.network_host = network_host;
  }

  /**
   * Running the IITC and plugins manager.
   * Migrates data storage as needed, then loads or updates UserScripts from the repositories.
   *
   * @async
   * @return {Promise<void>}
   */
  async run() {
    if (!this.is_initialized) {
      await new Promise(resolve => setTimeout(resolve, 1));
      return await this.run();
    }
    const is_migrated = await migrations.migrate(this.storage);
    await this.checkUpdates(is_migrated);
  }

  /**
   * Returns an object of all enabled plugins, including IITC core, with plugin UID as the key and plugin data as the value.
   *
   * @async
   * @returns {Promise<Object>} A promise that resolves to an object containing enabled plugins and IITC core data.
   */
  async getEnabledPlugins() {
    const channel = this.channel;
    const storage = await this.storage.get([
      `${channel}_iitc_core`,
      `${channel}_iitc_core_user`,
      `${channel}_plugins_flat`,
      `${channel}_plugins_local`,
      `${channel}_plugins_user`,
    ]);

    const plugins_flat = storage[`${channel}_plugins_flat`] || {};
    const plugins_local = storage[`${channel}_plugins_local`] || {};
    const plugins_user = storage[`${channel}_plugins_user`] || {};

    const enabled_plugins = {};
    let iitc_script = await this.getIITCCore(storage);
    if (iitc_script !== null) {
      enabled_plugins[this.iitc_main_script_uid] = iitc_script;

      for (const uid in plugins_flat) {
        if (plugins_flat[uid]['status'] === 'on') {
          // If the plugin is marked as 'user', use its 'user' version; otherwise, use its 'local' version
          enabled_plugins[uid] =
            plugins_flat[uid]['user'] === true ? plugins_user[uid] || {} : plugins_local[uid] || {};
        }
      }
    }
    return enabled_plugins;
  }

  /**
   * Invokes the injection of IITC core script and plugins to the page.
   * IITC core is injected first to ensure it initializes before any plugins. This is crucial because
   * the initialization of IITC takes some time, and during this time, plugins can be added to `window.bootPlugins`
   * without being started immediately. Injecting IITC first also prevents plugins from throwing errors
   * when attempting to access IITC, leaflet, or other dependencies during their initialization.
   *
   * @async
   * @returns {Promise<void>}
   */
  async inject() {
    const plugins = await this.getEnabledPlugins();

    // Ensure IITC core is injected first
    if (plugins[this.iitc_main_script_uid]) {
      this.inject_user_script(plugins[this.iitc_main_script_uid].code);
      this.inject_plugin(plugins[this.iitc_main_script_uid]);
      delete plugins[this.iitc_main_script_uid]; // Remove IITC core from the list to avoid re-injecting
    }

    // Now inject the rest of the plugins
    for (const uid in plugins) {
      const plugin = plugins[uid];
      if (plugin && plugin.code) {
        this.inject_user_script(plugin.code);
        this.inject_plugin(plugin);
      }
    }
  }

  /**
   * Runs periodic checks and installs updates for IITC, internal and external plugins.
   *
   * @async
   * @param {boolean} [force=false] - Forced to run the update right now.
   * @return {Promise<void>}
   */
  async checkUpdates(force) {
    await Promise.all([this._checkInternalUpdates(force), this._checkExternalUpdates(force)]);
  }

  /**
   * Controls the plugin. Allows you to enable, disable and remove the plugin.
   *
   * @async
   * @param {string} uid - Unique identifier of the plugin.
   * @param {"on" | "off" | "delete"} action - Type of action with the plugin.
   * @return {Promise<void>}
   */
  async managePlugin(uid, action) {
    const channel = this.channel;
    let local = await this.storage.get([
      `${channel}_plugins_flat`,
      `${channel}_plugins_local`,
      `${channel}_plugins_user`,
    ]);

    let plugins_flat = local[`${channel}_plugins_flat`];
    let plugins_local = local[`${channel}_plugins_local`];
    let plugins_user = local[`${channel}_plugins_user`];

    if (!isSet(plugins_local)) plugins_local = {};
    if (!isSet(plugins_user)) plugins_user = {};

    const currentTime = Math.floor(Date.now() / 1000);

    if (action === 'on') {
      const isUserPlugin = plugins_flat[uid]['user'];
      if ((isUserPlugin === false && plugins_local[uid] !== undefined) || isUserPlugin === true) {
        plugins_flat[uid]['status'] = 'on';
        plugins_flat[uid]['statusChangedAt'] = currentTime;

        if (isUserPlugin) {
          plugins_user[uid]['status'] = 'on';
          plugins_user[uid]['statusChangedAt'] = currentTime;
        } else {
          plugins_local[uid]['status'] = 'on';
          plugins_local[uid]['statusChangedAt'] = currentTime;
        }

        this.inject_user_script(
          isUserPlugin === true ? plugins_user[uid]['code'] : plugins_local[uid]['code']
        );
        this.inject_plugin(isUserPlugin === true ? plugins_user[uid] : plugins_local[uid]);

        await this._save(channel, {
          plugins_flat: plugins_flat,
          plugins_local: plugins_local,
          plugins_user: plugins_user,
        });
        await this._sendPluginsEvent(channel, [uid], 'add');
      } else {
        let filename = plugins_flat[uid]['filename'];
        let result = await this._getUrl(`${this.network_host[channel]}/plugins/${filename}`);
        if (result.data) {
          plugins_flat[uid]['status'] = 'on';
          plugins_flat[uid]['statusChangedAt'] = currentTime;
          plugins_flat[uid]['code'] = result.data;
          plugins_local[uid] = { ...plugins_flat[uid] };

          this.inject_user_script(plugins_local[uid]['code']);
          this.inject_plugin(plugins_local[uid]);

          await this._save(channel, {
            plugins_flat: plugins_flat,
            plugins_local: plugins_local,
          });
          await this._sendPluginsEvent(channel, [uid], 'add');
        }
      }
    }
    if (action === 'off') {
      plugins_flat[uid]['status'] = 'off';
      plugins_flat[uid]['statusChangedAt'] = currentTime;

      if (plugins_flat[uid]['user']) {
        plugins_user[uid]['status'] = 'off';
        plugins_user[uid]['statusChangedAt'] = currentTime;
      } else {
        plugins_local[uid]['status'] = 'off';
        plugins_local[uid]['statusChangedAt'] = currentTime;
      }

      await this._save(channel, {
        plugins_flat: plugins_flat,
        plugins_local: plugins_local,
        plugins_user: plugins_user,
      });
      await this._sendPluginsEvent(channel, [uid], 'remove');
    }
    if (action === 'delete') {
      if (uid === this.iitc_main_script_uid) {
        await this._save(channel, {
          iitc_core_user: {},
        });
        await this._sendPluginsEvent(channel, [uid], 'update');
      } else {
        const isEnabled = plugins_flat[uid]['status'] === 'on';
        if (plugins_flat[uid]['override']) {
          if (plugins_local[uid] !== undefined) {
            plugins_flat[uid] = { ...plugins_local[uid] };
          }
          plugins_flat[uid]['user'] = false;
          plugins_flat[uid]['override'] = false;
          plugins_flat[uid]['status'] = 'off';
          delete plugins_flat[uid]['addedAt'];
        } else {
          delete plugins_flat[uid];
        }
        delete plugins_user[uid];

        await this._save(channel, {
          plugins_flat: plugins_flat,
          plugins_local: plugins_local,
          plugins_user: plugins_user,
        });
        if (isEnabled) {
          await this._sendPluginsEvent(channel, [uid], 'remove');
        }
      }
    }
  }

  /**
   * Allows adding third-party UserScript plugins to IITC.
   * Returns the dictionary of installed or updated plugins.
   *
   * @async
   * @param {Object[]} scripts - Array of UserScripts.
   * @param {plugin} scripts[].meta - Parsed "meta" object of UserScript.
   * @param {string} scripts[].code - UserScript code.
   * @return {Promise<Object.<string, plugin>>}
   */
  async addUserScripts(scripts) {
    const channel = this.channel;
    let local = await this.storage.get([
      `${channel}_iitc_core_user`,
      `${channel}_categories`,
      `${channel}_plugins_flat`,
      `${channel}_plugins_local`,
      `${channel}_plugins_user`,
    ]);

    let iitc_core_user = local[`${channel}_iitc_core_user`];
    let categories = local[`${channel}_categories`];
    let plugins_flat = local[`${channel}_plugins_flat`];
    let plugins_local = local[`${channel}_plugins_local`];
    let plugins_user = local[`${channel}_plugins_user`];

    if (!isSet(categories)) categories = {};
    if (!isSet(plugins_flat)) plugins_flat = {};
    if (!isSet(plugins_local)) plugins_local = {};
    if (!isSet(plugins_user)) plugins_user = {};

    const added_uids = [];
    const updated_uids = [];
    const installed_scripts = {};
    const currentTime = Math.floor(Date.now() / 1000);

    scripts.forEach(script => {
      let meta = script['meta'];
      const code = script['code'];
      const plugin_uid = getUID(meta);

      if (plugin_uid === null) throw new Error('The plugin has an incorrect ==UserScript== header');

      if (plugin_uid === this.iitc_main_script_uid) {
        iitc_core_user = Object.assign(meta, {
          uid: plugin_uid,
          code: code,
        });
        updated_uids.push(plugin_uid);
        installed_scripts[plugin_uid] = iitc_core_user;
      } else {
        const is_user_plugins = plugins_user[plugin_uid] !== undefined;
        plugins_user[plugin_uid] = Object.assign(meta, {
          uid: plugin_uid,
          status: 'on',
          filename: meta['filename']
            ? meta['filename']
            : sanitizeFileName(`${meta['name']}.user.js`),
          code: code,
          addedAt: currentTime,
          statusChangedAt: currentTime,
        });

        if (plugin_uid in plugins_flat && !is_user_plugins) {
          if (plugin_uid in plugins_local && plugins_flat[plugin_uid]['status'] !== 'off') {
            plugins_local[plugin_uid]['status'] = 'off';
          }

          plugins_flat[plugin_uid]['status'] = 'on';
          plugins_flat[plugin_uid]['code'] = code;
          plugins_flat[plugin_uid]['override'] = true;
          plugins_flat[plugin_uid]['addedAt'] = currentTime;
          plugins_flat[plugin_uid]['statusChangedAt'] = currentTime;
          updated_uids.push(plugin_uid);
        } else {
          let category = plugins_user[plugin_uid]['category'];
          if (category === undefined) {
            category = 'Misc';
            plugins_user[plugin_uid]['category'] = category;
          }
          if (!(category in categories)) {
            categories[category] = {
              name: category,
              description: '',
            };
          }
          plugins_flat[plugin_uid] = { ...plugins_user[plugin_uid] };
          added_uids.push(plugin_uid);
        }
        plugins_flat[plugin_uid]['user'] = true;
        installed_scripts[plugin_uid] = plugins_flat[plugin_uid];
      }
    });

    await this._save(channel, {
      iitc_core_user: iitc_core_user,
      categories: categories,
      plugins_flat: plugins_flat,
      plugins_local: plugins_local,
      plugins_user: plugins_user,
    });

    if (added_uids.length) await this._sendPluginsEvent(channel, added_uids, 'add');
    if (updated_uids.length) await this._sendPluginsEvent(channel, updated_uids, 'update');

    return installed_scripts;
  }

  /**
   * Returns information about requested plugin by UID.
   *
   * @async
   * @param {string} uid - Plugin UID.
   * @return {Promise<plugin|null>}
   */
  async getPluginInfo(uid) {
    let all_plugins = await this.storage
      .get([this.channel + '_plugins_flat'])
      .then(data => data[this.channel + '_plugins_flat']);
    if (all_plugins === undefined) return null;
    return all_plugins[uid];
  }

  /**
   * Returns IITC core script.
   *
   * @async
   * @param {Object|undefined} [storage=undefined] - Storage object with keys `channel_iitc_core` and `channel_iitc_core_user`.
   * If not specified, the data is queried from the storage.
   * @param {"release" | "beta" | "custom" | undefined} [channel=undefined] - Current channel.
   * If not specified, the current channel is used.
   * @return {Promise<plugin|null>}
   */
  async getIITCCore(storage, channel) {
    if (typeof channel === 'undefined') channel = this.channel;

    if (storage === undefined || !isSet(storage[`${channel}_iitc_core`])) {
      storage = await this.storage.get([`${channel}_iitc_core`, `${channel}_iitc_core_user`]);
    }

    const iitc_core = storage[`${channel}_iitc_core`];
    const iitc_core_user = storage[`${channel}_iitc_core_user`];

    let iitc_script = null;
    if (isSet(iitc_core_user) && isSet(iitc_core_user['code'])) {
      iitc_script = iitc_core_user;
      iitc_script['override'] = true;
    } else if (isSet(iitc_core) && isSet(iitc_core['code'])) {
      iitc_script = iitc_core;
    }
    return iitc_script;
  }

  /**
   * Asynchronously retrieves backup data based on the specified parameters.
   *
   * @async
   * @param {BackupParams} params - The parameters for the backup data retrieval.
   * @return {Promise<object>} A promise that resolves to the backup data.
   */
  async getBackupData(params) {
    // Process the input parameters using the 'paramsProcessing' function from the 'backup' module.
    params = backup.paramsProcessing(params);

    // Initialize the backup_data object with its properties.
    const backup_data = {
      external_plugins: {},
      data: {
        iitc_settings: {},
        plugins_data: {},
        app: 'IITC Button',
      },
    };

    // Retrieve all_storage using the 'get' method of 'storage' module.
    const all_storage = await this.storage.get(null);

    if (params.settings) backup_data.data.iitc_settings = backup.exportIitcSettings(all_storage);
    if (params.data) backup_data.data.plugins_data = backup.exportPluginsSettings(all_storage);
    if (params.external) backup_data.external_plugins = backup.exportExternalPlugins(all_storage);

    // Return the backup_data object.
    return backup_data;
  }

  /**
   * Asynchronously sets backup data based on the specified parameters.
   *
   * This function takes the provided parameters and backup data object and sets the data
   * accordingly. The input parameters are processed using the 'paramsProcessing' function
   * from the 'backup' module. Depending on the parameters, the function imports IITC settings,
   * plugin data, and external plugins into the 'this' object using appropriate functions from
   * the 'backup' module.
   *
   * @async
   * @param {BackupParams} params - The parameters for setting the backup data.
   * @param {object} backup_data - The backup data object containing the data to be set.
   * @return {Promise<void>} A promise that resolves when the backup data is set.
   */
  async setBackupData(params, backup_data) {
    // Process the input parameters using the 'paramsProcessing' function from the 'backup' module.
    params = backup.paramsProcessing(params);

    if (params.settings) await backup.importIitcSettings(this, backup_data.data.iitc_settings);
    if (params.data) await backup.importPluginsSettings(this, backup_data.data.plugins_data);
    if (params.external) await backup.importExternalPlugins(this, backup_data.external_plugins);
  }
}