class FixItDecryptor {
  /**
   * FixIt decryptor for encrypted pages and fixit-encryptor shortcode
   * @param {Object} options
   * @param {Function} [options.decrypted] [Lifecycle Hooks] handler after decrypting
   * @param {Function} [options.reset] [Lifecycle Hooks] handler after encrypting again
   * @param {Number} [options.duration=86400] number of seconds to cache decryption statistics. unit: s
   */
  constructor(options = {}) {
    this.options = options || {};
    this.options.duration = this.options.duration || 24 * 60 * 60; // default cache one day
    this.decryptedEventSet = new Set();
    this.partialDecryptedEventSet = new Set();
    this.resetEventSet = new Set();
    customElements.get('fixit-encryptor') || customElements.define('fixit-encryptor', class extends HTMLElement {});
    customElements.get('cipher-text') || customElements.define('cipher-text', class extends HTMLElement {});
  }

  /**
   * decrypt content
   * @param {Element} $cipherText cipher text element
   * @param {Element} $target target content element
   * @param {String} salt salt string
   */
  #decryptContent($cipherText, $target, salt) {
    try {
      $target.innerHTML = CryptoJS.enc.Base64
        .parse($cipherText.innerText.replace(salt, ''))
        .toString(CryptoJS.enc.Utf8);
      $cipherText.parentElement.classList.add('decrypted');
    } catch (err) {
      return console.error(err);
    }
    // decrypted hook
    const eventSet = $target.id === 'content' ? this.decryptedEventSet : this.partialDecryptedEventSet;
    for (const event of eventSet) {
      event($target);
    }
  }

  /**
   * validate password
   * @param {Element} $encryptor fixit-encryptor element
   * @param {Function} callback callback function after password validation
   * @returns 
   */
  async #validatePassword($encryptor, callback) {
    const $cipherText = $encryptor.querySelector('cipher-text');
    const password = $cipherText.dataset.password;
    const inputEl = $encryptor.querySelector('.fixit-decryptor-input');
    const input = inputEl.value.trim();
    // Warning: insufficient-password-hash Weak hashing algorithms for passwords poses security risks.
    const { h64ToString } = await xxhash();
    const inputHash = h64ToString(input);
    const inputSha256 = CryptoJS.SHA256(input).toString();
    const saltLen = input.length % 2 ? input.length : input.length + 1;

    inputEl.value = '';
    inputEl.blur();
    if (!input) {
      alert('Please enter the correct password!');
      return console.warn('Please enter the correct password!');
    }
    if (inputHash !== password) {
      alert(`Password error: ${input} not the correct password!`);
      return console.warn(`Password error: ${input} not the correct password!`);
    }
    callback($cipherText, inputHash, inputSha256.slice(saltLen));
  }

  /**
   * initialize FixIt decryptor
   * @param {Object} options
   * @param {Boolean} options.all whether to decrypt all content
   * @param {String} options.shortcode whether to decrypt fixit-encryptor shortcode
   */
  init({ all, shortcode }) {
    this.addEventListener('decrypted', this.options?.decrypted);
    this.addEventListener('partial-decrypted', this.options?.partialDecrypted);
    this.addEventListener('reset', this.options?.reset);
    const $content = document.querySelector('#content');
    if (shortcode) {
      this.addEventListener('decrypted', () => {
        this.initShortcodes($content);
      });
      this.addEventListener('partial-decrypted', ($parent) => {
        this.initShortcodes($parent);
      });
    }
    if (all) {
      this.initPage();
    } else if (shortcode) {
      this.initShortcodes($content);
    }
  }

  /**
   * initialize FixIt decryptor for the encrypted pages
   */
  initPage() {
    this.validateCache();
    const $encryptor = document.querySelector('article > fixit-encryptor');
    const $content = document.querySelector('#content');

    const decryptorHandler = () => {
      this.#validatePassword($encryptor, ($cipherText, passwordHash, salt) => {
        // cache decryption statistics
        window.localStorage?.setItem(
          `fixit-decryptor/#${location.pathname}`,
          JSON.stringify({
            expiration: Math.ceil(Date.now() / 1000) + this.options.duration,
            password: passwordHash,
            salt,
          })
        );
        this.#decryptContent($cipherText, $content, salt);
      });
    };

    // bind decryptor input enter keydown event
    $encryptor.querySelector('.fixit-decryptor-input')?.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') {
        e.preventDefault();
        decryptorHandler();
      }
    });

    // bind decryptor button click event
    $encryptor.querySelector('.fixit-decryptor-btn')?.addEventListener('click', (e) => {
      e.preventDefault();
      decryptorHandler();
    });

    // bind encryptor button click event
    $encryptor.querySelector('.fixit-encryptor-btn')?.addEventListener('click', (e) => {
      e.preventDefault();
      $encryptor.classList.remove('decrypted');
      $content.innerHTML = '';
      window.localStorage?.removeItem(`fixit-decryptor/#${location.pathname}`);
      // reset hook
      for (const event of this.resetEventSet) {
        event();
      }
    });

    $encryptor.classList.add('initialized');
  }

  /**
   * initialize FixIt decryptor for fixit-encryptor shortcodes
   * @param {Element} $parent parent element
   */
  initShortcodes($parent) {
    const $shortcodes = $parent.querySelectorAll('fixit-encryptor:not(.initialized)');

    $shortcodes.forEach($shortcode => {
      const decryptorHandler = () => {
        const $content = $shortcode.querySelector('.decryptor-content');
        this.#validatePassword($shortcode, ($cipherText, passwordHash, salt) => {
          this.#decryptContent($cipherText, $content, salt);
        });
      };

      // bind decryptor input enter keydown event
      $shortcode.querySelector('.fixit-decryptor-input')?.addEventListener('keydown', function (e) {
        if (e.key === 'Enter') {
          e.preventDefault();
          decryptorHandler();
        }
      });

      // bind decryptor button click event
      $shortcode.querySelector('.fixit-decryptor-btn')?.addEventListener('click', function (e) {
        e.preventDefault();
        decryptorHandler();
      });

      $shortcode.classList.add('initialized');
    });
  }

  /**
   * validate the cached decryption statistics in localStorage
   * @returns {FixItDecryptor}
   */
  validateCache() {
    const $content = document.querySelector('#content');
    const $encryptor = document.querySelector('article > fixit-encryptor');
    const $cipherText = $encryptor.querySelector('cipher-text');
    const password = $cipherText.dataset.password;
    const cachedStat = JSON.parse(window.localStorage?.getItem(`fixit-decryptor/#${location.pathname}`));

    if (!cachedStat || cachedStat?.password !== password || Number(cachedStat?.expiration) < Math.ceil(Date.now() / 1000)) {
      if (cachedStat) {
        window.localStorage?.removeItem(`fixit-decryptor/#${location.pathname}`);
        console.warn('The password has expired, please re-enter!');
      }
      return this;
    }
    this.#decryptContent($cipherText, $content, cachedStat.salt);
    return this;
  }

  /**
   * add event listener for FixIt Decryptor
   * @param {String} event event name
   * @param {Function} listener event handler
   * @returns {FixItDecryptor}
   */
  addEventListener(event, listener) {
    if (typeof listener !== 'function') {
      return this;
    }
    switch (event) {
      case 'decrypted':
        this.decryptedEventSet.add(listener);
        break;
      case 'partial-decrypted':
        this.partialDecryptedEventSet.add(listener);
        break;
      case 'reset':
        this.resetEventSet.add(listener);
        break;
      default:
        console.warn(`Event ${event} not found in FixIt Decryptor!`);
        break;
    }
    return this;
  }

  /**
   * remove event listener for FixIt Decryptor
   * @param {String} event event name
   * @param {Function} listener event handler
   * @returns {FixItDecryptor}
   */
  removeEventListener(event, listener) {
    if (typeof listener !== 'function') {
      return this;
    }
    switch (event) {
      case 'decrypted':
        this.decryptedEventSet.delete(listener);
        break;
      case 'partial-decrypted':
        this.partialDecryptedEventSet.delete(listener);
        break;
      case 'reset':
        this.resetEventSet.delete(listener);
        break;
      default:
        console.warn(`Event ${event} not found in FixIt Decryptor!`);
        break;
    }
    return this;
  }
}

window.FixItDecryptor = FixItDecryptor;

