let state = {};
let schema = {};

const buildLabel = (elementType, text, hintText) => {
  const labelElement = document.createElement("label");
  labelElement.innerHTML = text;
  labelElement.style.fontWeight = elementType === "group" ? "bold" : "lighter";

  if (hintText) {
    const hintElement = document.createElement("span");
    hintElement.classList.add("tooltip");
    hintElement.innerHTML = " ⓘ";

    const hintTextElement = document.createElement("span");
    hintTextElement.classList.add("tooltip-text");
    hintTextElement.innerHTML = hintText;

    hintElement.appendChild(hintTextElement);

    labelElement.appendChild(hintElement);
  }

  return labelElement;
};

const buildNumberInput = (schema, parentState, key) => {
  const input = document.createElement("input");
  input.classList.add("base-input");
  input.type = "number";
  input.value = parentState[key];

  const min = schema.min ?? 0;
  input.min = min;

  input.addEventListener("change", () => {
    const normalizedValue = parseFloat(input.value, 10);
    parentState[key] = Math.max(normalizedValue, min);
  });

  return input;
};

const buildBooleanInput = (parentState, key) => {
  const input = document.createElement("input");
  input.classList.add("base-input");
  input.type = "checkbox";
  input.checked = parentState[key] ?? false;

  input.addEventListener("change", () => {
    parentState[key] = input.checked;
  });

  return input;
};

const buildStringInput = (parentState, key) => {
  const input = document.createElement("input");
  input.classList.add("base-input");
  input.type = "text";
  input.value = parentState[key] ?? "";

  input.addEventListener("change", () => {
    parentState[key] = input.value;
  });

  return input;
};

const defaultValueForType = (type) => {
  switch (type) {
    case "number":
      return 0;
    case "boolean":
      return false;
    case "string":
      return "";
    case "array":
      return [];
    case "object":
      return {};
  }

  return null;
};

const arrayFormElementDecorator = (childElement, parentState, index) => {
  const decoratedElement = document.createElement("div");
  decoratedElement.classList.add("array-form-element-decorator");

  const removeButton = document.createElement("button");
  removeButton.innerHTML = "X";
  removeButton.classList.add("array-input", "array-input-delete", "button");
  removeButton.addEventListener("click", () => {
    parentState.splice(index, 1);
    rerender();
  });

  decoratedElement.appendChild(childElement);
  decoratedElement.appendChild(removeButton);

  return decoratedElement;
};

const buildArrayInput = (schema, parentState) => {
  const itemType = schema.items.type;
  const inputControlsDiv = document.createElement("div");
  inputControlsDiv.classList.add("array-input-controls");

  const addButton = document.createElement("button");
  addButton.innerHTML = "Add One";
  addButton.classList.add("array-input", "button");
  addButton.addEventListener("click", () => {
    parentState.push(defaultValueForType(itemType));
    rerender();
  });

  const removeButton = document.createElement("button");
  removeButton.innerHTML = "Delete All";
  removeButton.classList.add("array-input", "array-input-delete", "button");
  removeButton.addEventListener("click", () => {
    parentState.splice(0, parentState.length);
    rerender();
  });

  inputControlsDiv.appendChild(addButton);
  inputControlsDiv.appendChild(removeButton);

  return inputControlsDiv;
};

const buildUnknownInput = () => {
  const disclaimer = document.createElement("div");
  disclaimer.innerHTML = `<i class="unknown-input">This configuration is not yet supported</i>`;

  return disclaimer;
};

const render = (state, schema) => {
  const build = (
    schema,
    state,
    parentState,
    currentKey = "",
    path = "configuration",
  ) => {
    const parent = document.createElement("div");
    parent.classList.add("form-element");

    const { type, label, hint, fields, items } = schema;

    if (label) {
      parent.appendChild(buildLabel(type, label, hint));
    }

    parent.id = path;

    if (type === "object") {
      const entries = Object.entries(fields);
      entries.forEach(([key, value]) => {
        state[key] ??= defaultValueForType(value.type);

        const childElement = build(
          value,
          state[key],
          state,
          key,
          `${path}.${key}`,
        );
        parent.appendChild(childElement);
      });
    } else if (type === "array") {
      const arrayInputControls = buildArrayInput(schema, state);
      parent.appendChild(arrayInputControls);

      if (state && state.length > 0) {
        state.forEach((element, index) => {
          const childElement = build(
            items,
            element,
            state,
            index,
            `${path}[${index}]`,
          );

          const decoratedChildElement = arrayFormElementDecorator(
            childElement,
            state,
            index,
          );
          parent.appendChild(decoratedChildElement);
        });
      }
    } else if (type === "number") {
      parent.appendChild(buildNumberInput(schema, parentState, currentKey));
      parent.classList.add("input-label");
    } else if (type === "string") {
      parent.appendChild(buildStringInput(parentState, currentKey));
      parent.classList.add("input-label");
    } else if (type === "boolean") {
      parent.appendChild(buildBooleanInput(parentState, currentKey));
      parent.classList.add("input-label");
    } else {
      parent.appendChild(buildUnknownInput());
    }

    return parent;
  };

  return build(schema, state, state);
};

function rerender() {
  const root = document.querySelector("#root");
  root.innerHTML = "";
  root?.append(render(state, schema));
}

window.onload = async () => {
  const [schemaResponse, dataResponse] = await Promise.all([
    fetch("/configuration/schema"),
    fetch("/configuration"),
  ]);

  const [schemaResponseJson, dataResponseJson] = await Promise.all([
    schemaResponse.json(),
    dataResponse.json(),
  ]);

  if (schemaResponse.status !== 200 || dataResponse.status !== 200) {
    const root = document.querySelector("#root");
    let html = "";
    if (schemaResponse.status !== 200) {
      html += `<i class="unknown-input">Error fetching configuration schema: ${schemaResponseJson.message}</i>`;
    }
    if (dataResponse.status !== 200) {
      html += `<i class="unknown-input">Error fetching configuration data: ${dataResponseJson.message}</i>`;
    }
    root.innerHTML = html;
    return;
  }

  const { data: formSchema } = schemaResponseJson;
  const { data: initialData } = dataResponseJson;

  state = initialData;
  schema = formSchema;

  rerender();

  const saveButton = document.querySelector("#save");

  saveButton?.addEventListener("click", async () => {
    if (saveButton.disabled) {
      return;
    }

    saveButton.innerHTML = "Saving...";
    saveButton.disabled = true;
    const response = await fetch("/configuration", {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        configuration: state,
      }),
    });
    if (response.status === 200) {
      saveButton.innerHTML = "Saved!";
      saveButton.classList.add("good");
    } else {
      saveButton.innerHTML = "Failed!";
      saveButton.classList.add("bad");
    }
    setTimeout(() => {
      saveButton.innerHTML = "Save Changes";
      saveButton.classList.remove("good");
      saveButton.classList.remove("bad");
      saveButton.disabled = false;
    }, 3000);
  });

  const exportButton = document.querySelector("#export");

  exportButton.addEventListener("click", async () => {
    download(
      "backend-configuration.json",
      JSON.stringify({ configuration: state }),
    );
  });
};

function download(filename, text) {
  let element = document.createElement("a");
  element.setAttribute(
    "href",
    "data:text/plain;charset=utf-8," + encodeURIComponent(text),
  );
  element.setAttribute("download", filename);

  element.style.display = "none";
  document.body.appendChild(element);
  element.click();
  document.body.removeChild(element);
}
