import { BnumPromise } from '../../BnumPromise.js';
import { BnumLog } from '../../classes/bnum_log.js';
import { EMPTY_STRING } from '../../constants/constants.js';
import { MelObject } from '../../mel_object.js';
export { Connector };

/**
 * @template {Object<string, string | number | boolean>} T
 * @template Y
 * @callback PostProcessCallback
 * @param {{datas: Y | null, has_error: boolean, error: any | null}} data
 * @param {Connector<T, Y>} caller
 * @return {{datas: Y | any | null, has_error: boolean, error: any | null}}
 */

/**
 * Représente un connecteur avec le back-end
 * @template {Object<string, string | number | boolean>} T Objet à envoyer au serveur
 * @template Y Réponse attendu
 */
class Connector {
  /**
   * Constructeur de la classe
   * @param {string} task Nom de la tâche qui permet de récupérer les données
   * @param {string} action Nom de l'action qui permet de récupérer les données
   * @param {Object} param2
   * @param {Symbol} param2.type Type de la requête (Connector.enums.type)
   * @param {any | {} | null} param2.params Paramètres de la requête
   * @param {null | PostProcessCallback<T, Y>} param2.postProcess Action à faire une fois les données récupérés
   * @param {?T} param2.needed Paramètres à mettre dans `connect` et à compléter pour que ça fonctionne
   */
  constructor(
    task,
    action,
    {
      type = Connector.enums.type.get,
      params = null,
      postProcess = null,
      needed = {},
    },
  ) {
    //Init pour inteliscense
    this.task = EMPTY_STRING;
    this.action = EMPTY_STRING;
    this.type = Connector.enums.type.get;
    this.params = null;
    this.on_success = null;
    /**
     * @type {T}
     */
    this.needed = null;
    //Getter des variables privés
    Object.defineProperties(this, {
      task: {
        get: function () {
          return task;
        },
        configurable: false,
      },
      action: {
        get: function () {
          return action;
        },
        configurable: false,
      },
      type: {
        get: function () {
          return type;
        },
        configurable: false,
      },
      params: {
        get: function () {
          return params;
        },
        configurable: false,
      },
      on_success: {
        get: function () {
          return postProcess;
        },
        configurable: false,
      },
      needed: {
        get: function () {
          return JSON.parse(JSON.stringify(needed));
        },
        configurable: false,
      },
    });
  }

  /**
   * Connecte le front avec le back
   *
   * Récupère ou envoi des données au serveur
   * @param {Object} param0
   * @param {?T} param0.params Paramètres additionnels
   * @param {*} param0.default_return Valeur de retour par défaut
   * @returns {Promise<{datas: Y | null, has_error: boolean, error: any | null}>} Retourne les données récupérés ou null si il y a une erreur
   */
  async connect({ params = null, default_return = null }) {
    let return_datas = null;
    let error_datas = {
      has_error: false,
      error: null,
    };

    BnumLog.info('connect', `Connecting to ${this.task}/${this.action}`);

    if (
      Connector.constants.in_progress_task === this.task &&
      Connector.constants.in_progress === this.action
    ) {
      error_datas.has_error = true;
      error_datas.error = 'Connector is in progress';
      BnumLog.warning(
        'connect',
        `${this.task}/${this.action}`,
        'Connector is in progress !',
      );
    } else {
      let url_parameters = this.params ?? {};
      if (params !== null) {
        const keys = Object.keys(params);
        for (let index = 0, key; index < keys.length; ++index) {
          key = keys[index];
          url_parameters[key] = params[key];
        }
      }

      /**
       * @type {?BnumPromise<void>}
       * @package
       */
      let promise;
      switch (this.type) {
        case Connector.enums.type.get:
          promise = BnumPromise.Ajax.Get(
            MelObject.Empty().url(this.task, {
              action: this.action,
              params: url_parameters,
            }),
            {
              success: (datas) => {
                try {
                  if (typeof datas === 'string') datas = JSON.parse(datas);
                } catch (error) {
                  BnumLog.debug('connect', 'datas', datas, error);
                }

                return_datas = datas;

                BnumLog.info('connect', 'Connected !');
              },
              failed: (...args) => {
                error_datas.has_error = true;
                error_datas.error = args;
                BnumLog.error(
                  'connect',
                  `${this.task}/${this.action}`,
                  'Connexion failed !',
                  ...args,
                );
              },
            },
          );
          break;
        case Connector.enums.type.post:
          promise = BnumPromise.Ajax.Post(
            MelObject.Empty().url(this.task, { action: this.action }),
            {
              data: url_parameters,
              success: (datas) => {
                try {
                  if (typeof datas === 'string') datas = JSON.parse(datas);
                } catch (error) {
                  BnumLog.debug('connect', 'datas', datas, error);
                }

                return_datas = datas;
                BnumLog.info('connect', 'Connected !');
              },
              failed: (...args) => {
                error_datas.has_error = true;
                error_datas.error = args;
                BnumLog.error(
                  'connect',
                  `${this.task}/${this.action}`,
                  'Connexion failed !',
                  ...args,
                );
              },
            },
          );

          break;
        default:
          throw new Error('Unknown connector type');
      }

      await promise;
    }

    return_datas = {
      datas: return_datas ?? default_return,
      has_error: error_datas.has_error,
      error: error_datas.error,
      params,
    };

    if (!error_datas.has_error) {
      if (
        !!this.on_success &&
        this.on_success.constructor.name === 'AsyncFunction'
      )
        return_datas = await this.on_success(return_datas, this);
      else return_datas = this.on_success?.(return_datas, this) ?? return_datas;
    }

    BnumLog.info(
      'connect',
      `${this.task}/${this.action}`,
      'Connection ended !',
    );

    return return_datas;
  }

  /**
   * Connecte le front avec le back
   *
   * Récupère ou envoi des données au serveur, ignore les erreurs.
   * @param {Object} param0
   * @param {?T} param0.params Paramètres additionnels
   * @param {*} param0.default_return Valeur de retour par défaut
   * @returns {Promise<{datas: Y | null, has_error: boolean, error: any | null}>} Retourne les données récupérés
   */
  async force_connect({ params = null, default_return = null }) {
    return (
      (await this.connect({ params, default_return })).datas ?? default_return
    );
  }

  /**
   * Représente un connecteur qui n'éxiste pas encore
   * @returns {Connector}
   */
  static in_work() {
    return new Connector(
      Connector.constants.in_progress_task,
      Connector.constants.in_progress,
      {},
    );
  }

  static Create(
    task,
    action,
    {
      type = Connector.enums.type.get,
      params = null,
      postProcess = null,
      needed = {},
    },
  ) {
    return new Connector(task, action, { type, params, postProcess, needed });
  }
}

Connector.enums = {};
/**
 * @enum {Symbol}
 */
Connector.enums.type = {
  get: Symbol('get'),
  post: Symbol('post'),
};
Connector.constants = {
  in_progress: 'in_progress',
  in_progress_task: 'mel_metapage',
};