"use strict";

/* globals RPMAddMessageListener, RPMSendQuery, RPMSendAsyncMessage */

let TorStrings;

const Orders = Object.freeze({
  Name: "name",
  NameDesc: "name-desc",
  LastUpdate: "last-update",
});

const States = Object.freeze({
  Warning: "warning",
  Details: "details",
  Edit: "edit",
  NoRulesets: "noRulesets",
});

function setUpdateDate(ruleset, element) {
  if (!ruleset.enabled) {
    element.textContent = TorStrings.rulesets.disabled;
    return;
  }
  if (!ruleset.currentTimestamp) {
    element.textContent = TorStrings.rulesets.neverUpdated;
    return;
  }

  const formatter = new Intl.DateTimeFormat(navigator.languages, {
    year: "numeric",
    month: "long",
    day: "numeric",
  });
  element.textContent = TorStrings.rulesets.lastUpdated.replace(
    "%S",
    formatter.format(new Date(ruleset.currentTimestamp * 1000))
  );
}

class WarningState {
  selectors = Object.freeze({
    wrapper: "#warning-wrapper",
    title: "#warning-title",
    description: "#warning-description",
    enableCheckbox: "#warning-enable-checkbox",
    enableLabel: "#warning-enable-label",
    button: "#warning-button",
  });

  elements = Object.freeze({
    wrapper: document.querySelector(this.selectors.wrapper),
    title: document.querySelector(this.selectors.title),
    description: document.querySelector(this.selectors.description),
    enableCheckbox: document.querySelector(this.selectors.enableCheckbox),
    enableLabel: document.querySelector(this.selectors.enableLabel),
    button: document.querySelector(this.selectors.button),
  });

  constructor() {
    const elements = this.elements;
    elements.title.textContent = TorStrings.rulesets.warningTitle;
    elements.description.textContent = TorStrings.rulesets.warningDescription;
    elements.enableLabel.textContent = TorStrings.rulesets.warningEnable;
    elements.button.textContent = TorStrings.rulesets.warningButton;
    elements.enableCheckbox.addEventListener(
      "change",
      this.onEnableChange.bind(this)
    );
    elements.button.addEventListener("click", this.onButtonClick.bind(this));
  }

  show() {
    this.elements.button.focus();
  }

  hide() {}

  onEnableChange() {
    RPMSendAsyncMessage(
      "rulesets:set-show-warning",
      this.elements.enableCheckbox.checked
    );
  }

  onButtonClick() {
    gAboutRulesets.selectFirst();
  }
}

class DetailsState {
  selectors = Object.freeze({
    title: "#ruleset-title",
    edit: "#ruleset-edit",
    jwkLabel: "#ruleset-jwk-label",
    jwkValue: "#ruleset-jwk-value",
    pathPrefixLabel: "#ruleset-path-prefix-label",
    pathPrefixValue: "#ruleset-path-prefix-value",
    scopeLabel: "#ruleset-scope-label",
    scopeValue: "#ruleset-scope-value",
    enableCheckbox: "#ruleset-enable-checkbox",
    enableLabel: "#ruleset-enable-label",
    updateButton: "#ruleset-update-button",
    updated: "#ruleset-updated",
  });

  elements = Object.freeze({
    title: document.querySelector(this.selectors.title),
    edit: document.querySelector(this.selectors.edit),
    jwkLabel: document.querySelector(this.selectors.jwkLabel),
    jwkValue: document.querySelector(this.selectors.jwkValue),
    pathPrefixLabel: document.querySelector(this.selectors.pathPrefixLabel),
    pathPrefixValue: document.querySelector(this.selectors.pathPrefixValue),
    scopeLabel: document.querySelector(this.selectors.scopeLabel),
    scopeValue: document.querySelector(this.selectors.scopeValue),
    enableCheckbox: document.querySelector(this.selectors.enableCheckbox),
    enableLabel: document.querySelector(this.selectors.enableLabel),
    updateButton: document.querySelector(this.selectors.updateButton),
    updated: document.querySelector(this.selectors.updated),
  });

  constructor() {
    const elements = this.elements;
    elements.edit.textContent = TorStrings.rulesets.edit;
    elements.edit.addEventListener("click", this.onEdit.bind(this));
    elements.jwkLabel.textContent = TorStrings.rulesets.jwk;
    elements.pathPrefixLabel.textContent = TorStrings.rulesets.pathPrefix;
    elements.scopeLabel.textContent = TorStrings.rulesets.scope;
    elements.enableCheckbox.addEventListener(
      "change",
      this.onEnable.bind(this)
    );
    elements.enableLabel.textContent = TorStrings.rulesets.enable;
    elements.updateButton.textContent = TorStrings.rulesets.checkUpdates;
    elements.updateButton.addEventListener("click", this.onUpdate.bind(this));
  }

  show(ruleset) {
    const elements = this.elements;
    elements.title.textContent = ruleset.name;
    elements.jwkValue.textContent = JSON.stringify(ruleset.jwk);
    elements.pathPrefixValue.setAttribute("href", ruleset.pathPrefix);
    elements.pathPrefixValue.textContent = ruleset.pathPrefix;
    elements.scopeValue.textContent = ruleset.scope;
    elements.enableCheckbox.checked = ruleset.enabled;
    if (ruleset.enabled) {
      elements.updateButton.removeAttribute("disabled");
    } else {
      elements.updateButton.setAttribute("disabled", "disabled");
    }
    setUpdateDate(ruleset, elements.updated);
    this._showing = ruleset;

    gAboutRulesets.list.setItemSelected(ruleset.name);
  }

  hide() {
    this._showing = null;
  }

  onEdit() {
    gAboutRulesets.setState(States.Edit, this._showing);
  }

  async onEnable() {
    await RPMSendAsyncMessage("rulesets:enable-channel", {
      name: this._showing.name,
      enabled: this.elements.enableCheckbox.checked,
    });
  }

  async onUpdate() {
    try {
      await RPMSendQuery("rulesets:update-channel", this._showing.name);
    } catch (err) {
      console.error("Could not update the rulesets", err);
    }
  }
}

class EditState {
  selectors = Object.freeze({
    form: "#edit-ruleset-form",
    title: "#edit-title",
    nameGroup: "#edit-name-group",
    nameLabel: "#edit-name-label",
    nameInput: "#edit-name-input",
    jwkLabel: "#edit-jwk-label",
    jwkTextarea: "#edit-jwk-textarea",
    pathPrefixLabel: "#edit-path-prefix-label",
    pathPrefixInput: "#edit-path-prefix-input",
    scopeLabel: "#edit-scope-label",
    scopeInput: "#edit-scope-input",
    enableCheckbox: "#edit-enable-checkbox",
    enableLabel: "#edit-enable-label",
    save: "#edit-save",
    cancel: "#edit-cancel",
  });

  elements = Object.freeze({
    form: document.querySelector(this.selectors.form),
    title: document.querySelector(this.selectors.title),
    jwkLabel: document.querySelector(this.selectors.jwkLabel),
    jwkTextarea: document.querySelector(this.selectors.jwkTextarea),
    pathPrefixLabel: document.querySelector(this.selectors.pathPrefixLabel),
    pathPrefixInput: document.querySelector(this.selectors.pathPrefixInput),
    scopeLabel: document.querySelector(this.selectors.scopeLabel),
    scopeInput: document.querySelector(this.selectors.scopeInput),
    enableCheckbox: document.querySelector(this.selectors.enableCheckbox),
    enableLabel: document.querySelector(this.selectors.enableLabel),
    save: document.querySelector(this.selectors.save),
    cancel: document.querySelector(this.selectors.cancel),
  });

  constructor() {
    const elements = this.elements;
    elements.jwkLabel.textContent = TorStrings.rulesets.jwk;
    elements.jwkTextarea.setAttribute(
      "placeholder",
      TorStrings.rulesets.jwkPlaceholder
    );
    elements.pathPrefixLabel.textContent = TorStrings.rulesets.pathPrefix;
    elements.pathPrefixInput.setAttribute(
      "placeholder",
      TorStrings.rulesets.pathPrefixPlaceholder
    );
    elements.scopeLabel.textContent = TorStrings.rulesets.scope;
    elements.scopeInput.setAttribute(
      "placeholder",
      TorStrings.rulesets.scopePlaceholder
    );
    elements.enableLabel.textContent = TorStrings.rulesets.enable;
    elements.save.textContent = TorStrings.rulesets.save;
    elements.save.addEventListener("click", this.onSave.bind(this));
    elements.cancel.textContent = TorStrings.rulesets.cancel;
    elements.cancel.addEventListener("click", this.onCancel.bind(this));
  }

  show(ruleset) {
    const elements = this.elements;
    elements.form.reset();
    elements.title.textContent = ruleset.name;
    elements.jwkTextarea.value = JSON.stringify(ruleset.jwk);
    elements.pathPrefixInput.value = ruleset.pathPrefix;
    elements.scopeInput.value = ruleset.scope;
    elements.enableCheckbox.checked = ruleset.enabled;
    this._editing = ruleset;
  }

  hide() {
    this.elements.form.reset();
    this._editing = null;
  }

  async onSave(e) {
    e.preventDefault();
    const elements = this.elements;

    let valid = true;
    const name = this._editing.name;

    let jwk;
    try {
      jwk = JSON.parse(elements.jwkTextarea.value);
      await crypto.subtle.importKey(
        "jwk",
        jwk,
        {
          name: "RSA-PSS",
          saltLength: 32,
          hash: { name: "SHA-256" },
        },
        true,
        ["verify"]
      );
      elements.jwkTextarea.setCustomValidity("");
    } catch (err) {
      console.error("Invalid JSON or invalid JWK", err);
      elements.jwkTextarea.setCustomValidity(TorStrings.rulesets.jwkInvalid);
      valid = false;
    }

    const pathPrefix = elements.pathPrefixInput.value.trim();
    try {
      const url = new URL(pathPrefix);
      if (url.protocol !== "http:" && url.protocol !== "https:") {
        elements.pathPrefixInput.setCustomValidity(
          TorStrings.rulesets.pathPrefixInvalid
        );
        valid = false;
      } else {
        elements.pathPrefixInput.setCustomValidity("");
      }
    } catch (err) {
      console.error("The path prefix is not a valid URL", err);
      elements.pathPrefixInput.setCustomValidity(
        TorStrings.rulesets.pathPrefixInvalid
      );
      valid = false;
    }

    let scope;
    try {
      scope = new RegExp(elements.scopeInput.value.trim());
      elements.scopeInput.setCustomValidity("");
    } catch (err) {
      elements.scopeInput.setCustomValidity(TorStrings.rulesets.scopeInvalid);
      valid = false;
    }

    if (!valid) {
      return;
    }

    const enabled = elements.enableCheckbox.checked;

    const rulesetData = { name, jwk, pathPrefix, scope, enabled };
    const ruleset = await RPMSendQuery("rulesets:set-channel", rulesetData);
    gAboutRulesets.setState(States.Details, ruleset);
    if (enabled) {
      try {
        await RPMSendQuery("rulesets:update-channel", name);
      } catch (err) {
        console.warn("Could not update the ruleset after adding it", err);
      }
    }
  }

  onCancel(e) {
    e.preventDefault();
    if (this._editing === null) {
      gAboutRulesets.selectFirst();
    } else {
      gAboutRulesets.setState(States.Details, this._editing);
    }
  }
}

class NoRulesetsState {
  show() {}
  hide() {}
}

class RulesetList {
  selectors = Object.freeze({
    heading: "#ruleset-heading",
    list: "#ruleset-list",
    emptyContainer: "#ruleset-list-empty",
    emptyTitle: "#ruleset-list-empty-title",
    emptyDescription: "#ruleset-list-empty-description",
    itemTemplate: "#ruleset-template",
    itemName: ".name",
    itemDescr: ".description",
  });

  elements = Object.freeze({
    heading: document.querySelector(this.selectors.heading),
    list: document.querySelector(this.selectors.list),
    emptyContainer: document.querySelector(this.selectors.emptyContainer),
    emptyTitle: document.querySelector(this.selectors.emptyTitle),
    emptyDescription: document.querySelector(this.selectors.emptyDescription),
    itemTemplate: document.querySelector(this.selectors.itemTemplate),
  });

  nameAttribute = "data-name";

  rulesets = [];

  constructor() {
    const elements = this.elements;

    // Header
    elements.heading.textContent = TorStrings.rulesets.rulesets;
    // Empty
    elements.emptyTitle.textContent = TorStrings.rulesets.noRulesets;
    elements.emptyDescription.textContent = TorStrings.rulesets.noRulesetsDescr;

    RPMAddMessageListener(
      "rulesets:channels-change",
      this.onRulesetsChanged.bind(this)
    );
  }

  getSelectedRuleset() {
    const name = this.elements.list
      .querySelector(".selected")
      ?.getAttribute(this.nameAttribute);
    for (const ruleset of this.rulesets) {
      if (ruleset.name == name) {
        return ruleset;
      }
    }
    return null;
  }

  isEmpty() {
    return !this.rulesets.length;
  }

  async update() {
    this.rulesets = await RPMSendQuery("rulesets:get-channels");
    await this._populateRulesets();
  }

  setItemSelected(name) {
    name = name.replace(/["\\]/g, "\\$&");
    const item = this.elements.list.querySelector(
      `.item[${this.nameAttribute}="${name}"]`
    );
    this._selectItem(item);
  }

  async _populateRulesets() {
    if (this.isEmpty()) {
      this.elements.emptyContainer.classList.remove("hidden");
    } else {
      this.elements.emptyContainer.classList.add("hidden");
    }

    const list = this.elements.list;
    const selName = list
      .querySelector(".item.selected")
      ?.getAttribute(this.nameAttribute);
    const items = list.querySelectorAll(".item");
    for (const item of items) {
      item.remove();
    }

    for (const ruleset of this.rulesets) {
      const item = this._addItem(ruleset);
      if (ruleset.name === selName) {
        this._selectItem(item);
      }
    }
  }

  _addItem(ruleset) {
    const item = this.elements.itemTemplate.cloneNode(true);
    item.removeAttribute("id");
    item.classList.add("item");
    item.querySelector(this.selectors.itemName).textContent = ruleset.name;
    const descr = item.querySelector(this.selectors.itemDescr);
    if (ruleset.enabled) {
      setUpdateDate(ruleset, descr);
    } else {
      descr.textContent = TorStrings.rulesets.disabled;
      item.classList.add("disabled");
    }
    item.setAttribute(this.nameAttribute, ruleset.name);
    item.addEventListener("click", () => {
      this.onRulesetClick(ruleset);
    });
    this.elements.list.append(item);
    return item;
  }

  _selectItem(item) {
    this.elements.list.querySelector(".selected")?.classList.remove("selected");
    item?.classList.add("selected");
  }

  onRulesetClick(ruleset) {
    gAboutRulesets.setState(States.Details, ruleset);
  }

  onRulesetsChanged(data) {
    this.rulesets = data.data;
    this._populateRulesets();
    const selected = this.getSelectedRuleset();
    if (selected !== null) {
      gAboutRulesets.setState(States.Details, selected);
    }
  }
}

class AboutRulesets {
  _state = null;

  async init() {
    const args = await RPMSendQuery("rulesets:get-init-args");
    TorStrings = args.TorStrings;
    const showWarning = args.showWarning;

    this.list = new RulesetList();
    this._states = {};
    this._states[States.Warning] = new WarningState();
    this._states[States.Details] = new DetailsState();
    this._states[States.Edit] = new EditState();
    this._states[States.NoRulesets] = new NoRulesetsState();

    await this.refreshRulesets();

    if (showWarning) {
      this.setState(States.Warning);
    } else {
      this.selectFirst();
    }
  }

  setState(state, ...args) {
    document.querySelector("body").className = `state-${state}`;
    this._state?.hide();
    this._state = this._states[state];
    this._state.show(...args);
  }

  async refreshRulesets() {
    await this.list.update();
    if (this._state === this._states[States.Details]) {
      const ruleset = this.list.getSelectedRuleset();
      if (ruleset !== null) {
        this.setState(States.Details, ruleset);
      } else {
        this.selectFirst();
      }
    } else if (this.list.isEmpty()) {
      this.setState(States.NoRulesets);
    }
  }

  selectFirst() {
    if (this.list.isEmpty()) {
      this.setState(States.NoRulesets);
    } else {
      this.setState("details", this.list.rulesets[0]);
    }
  }
}

const gAboutRulesets = new AboutRulesets();
gAboutRulesets.init();
