Source: scripts/services/apiService.js

/**
 * The array of contacts.
 * @type {Array<Object>}
 */
let globalContacts;

/**
 * The array of todos.
 * @type {Array<Object>}
 */
let globalTodos;

/**
 * The URL of the Firebase Realtime Database.
 * @type {string}
 * @const
 */
const API_URL = firebaseConfig.apiKey;

/**
 * Fetches data from the given URL and returns it as JSON.
 *
 * @param {string} url - The URL to fetch from.
 * @returns {Promise<Object>} A promise that resolves with the data from the URL as JSON.
 * @throws {Error} If the URL is invalid or the response from the server was not OK.
 */
async function fetchData(url) {
  if (typeof url !== "string" || !url.trim().length) return Promise.reject(new Error("Invalid URL"));

  const response = await fetch(url);

  if (!response.ok) return Promise.reject(new Error(`HTTP error! status: ${response.status}`));

  return response.json();
}

/**
 * Fetches the contacts from the given user from the Firebase Realtime Database.
 *
 * @param {string} user - The user whose contacts are to be retrieved.
 * @returns {Promise<void>} - A promise that resolves when the data has been fetched and the contacts array has been set.
 */
async function getContactsFromData(user) {
  const data = await getDataFromFirebase();
  globalContacts = objectToArray(data[user].contacts);
}

/**
 * Fetches the todos from the given user from the Firebase Realtime Database.
 *
 * @param {string} user - The user whose todos are to be retrieved.
 * @returns {Promise<void>} - A promise that resolves when the data has been fetched and the todos array has been set.
 */
async function getTodosFromData(user) {
  const data = await getDataFromFirebase();
  globalTodos = objectToArray(data[user].todos);
}

/**
 * Given the 'createdAt' property of a contact and a user, returns the
 * id of the contact in the Firebase Realtime Database.
 *
 * @param {string|number} createdAt - The 'createdAt' property of the contact.
 * @param {string} user - The user whose contacts are to be searched.
 * @returns {Promise<string|undefined>} A promise that resolves with the
 * id of the contact if found, or undefined if not found.
 */
async function getContactIdByCreatedAt(user, createdAt) {
  const contacts = await fetchData(`${API_URL}/${user}/contacts.json`);
  if (!contacts) return;
  return findKeyByCreatedAt(contacts, createdAt);
}

/**
 * Retrieves the latest created contact for a specified user from Firebase.
 *
 * @param {string} user - The user whose contacts are to be retrieved.
 * @returns {Promise<Object|undefined>} A promise that resolves with the latest created contact
 * object or undefined if no contacts are found.
 */
async function getLatestCreatedContact(user) {
  const data = await getDataFromFirebase();
  if (!data) return;

  const contacts = objectToArray(data[user].contacts);

  return contacts.reduce((latestContact, currentContact) => {
    if (!latestContact || currentContact.createdAt > latestContact.createdAt) {
      return currentContact;
    }
    return latestContact;
  }, undefined);
}

/**
 * Retrieves the entire data object from the Firebase Realtime Database.
 *
 * @returns {Promise<Object|undefined>} A promise that resolves with the data
 * object from the Firebase Realtime Database, or undefined if no data is found.
 */
async function getDataFromFirebase() {
  const data = await fetchData(API_URL + ".json");
  if (!data) return;

  return data;
}

/**
 * Updates the data of a specific contact in the Firebase Realtime Database.
 *
 * @param {string} user - The user whose contact data is being updated.
 * @param {string} contactID - The ID of the contact to be updated.
 * @param {Object} data - The new data to be patched into the contact.
 * @returns {Promise<Response>} A promise that resolves with the response from the patch request.
 */
async function updateContactInDatabase(user, contactID, data) {
  const response = await fetch(`${API_URL}/${user}/contacts/${contactID}.json`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(data),
  });
  return response;
}

/**
 * Checks if a contact with the same email or phone number already exists in the contacts list.
 *
 * @param {string} email - The email to check for duplicates.
 * @param {string} phone - The phone number to check for duplicates.
 * @param {Array<Object>} contacts - The array of contact objects to check against.
 * @returns {Promise<boolean>} A promise that resolves to true if a duplicate contact is found, otherwise false.
 */
async function checkIfDuplicate(email, phone, contacts) {
  const duplicateContact = contacts.find((contact) => contact.email === email || contact.phone === phone);
  if (duplicateContact) {
    return true;
  }
  return false;
}

/**
 * Puts new contact data into the Firebase Realtime Database.
 *
 * @param {Object} newContact - The new contact data to be added.
 * @param {string} user - The user to add the contact for.
 * @returns {Promise<Response|Error>} A promise that resolves with the response from the Firebase Database if successful, or rejects with an error if there is a HTTP error.
 */
async function createContactInDatabase(user, newContact) {
  const data = await getDataFromFirebase();
  if (!data) return;
  const contacts = objectToArray(data[user].contacts);
  if (await checkIfDuplicate(newContact.email, newContact.phone, contacts)) {
    return { status: 400, ok: true, statusText: "Duplicate contact found." };
  }
  const initials = getInitialsFromContact(newContact);
  const response = await fetch(`${API_URL}/${user}/contacts/${initials}${newContact.createdAt}.json`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(newContact),
  });
  return response;
}

/**
 * Deletes the contact with the given contactID from the Firebase Realtime Database.
 *
 * @param {string} user - The user whose contact is to be deleted.
 * @param {string} contactID - The ID of the contact to be deleted.
 * @returns {Promise<Response>} A promise that resolves with the response from the Firebase Database if successful, or rejects with an error if there is a HTTP error.
 */
async function deleteContactFromDatabase(user, contactID) {
  const response = await fetch(`${API_URL}/${user}/contacts/${contactID}.json`, {
    method: "DELETE",
  });
  return response;
}

/**
 * Patches the todos object in the Firebase Realtime Database.
 *
 * @param {Object} todosObject - The object containing the todos to be updated.
 * @param {string} user - The user whose todos are to be updated.
 * @returns {Promise<Response|Error>} A promise that resolves with the response from the Firebase Database if successful, or rejects with an error if there is a HTTP error.
 */
async function updateTodosInFirebase(user, todosObject) {
  const response = await fetch(`${API_URL}/${user}/todos/.json`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(todosObject),
  });
  return response;
}

/**
 * Deletes the todo with the given todoID from the Firebase Realtime Database.
 *
 * @param {string} user - The user whose todo is to be deleted.
 * @param {string} todoID - The ID of the todo to be deleted.
 * @returns {Promise<Response|Error>} A promise that resolves with the response from the Firebase Database if successful, or rejects with an error if there is a HTTP error.
 */
async function deleteTodosInFirebase(user, todoID) {
  const response = await fetch(`${API_URL}/${user}/todos/${todoID}.json`, {
    method: "DELETE",
    headers: {
      "Content-Type": "application/json",
    },
  });
  if (response.ok) return response;
  return response;
}

/**
 * Creates a new user in the Firebase Realtime Database.
 *
 * @param {Object} newUser - The new user data to be added.
 * @returns {Promise<Response|Error>} A promise that resolves with the response from the Firebase Database if successful, or rejects with an error if there is a HTTP error.
 */
async function createUserInFirebaseDatabase(newUser) {
  if (!newUser.email) return { status: 401, ok: true, statusText: "Email address should not be empty." };

  const data = await getDataFromFirebase();
  if (!data) return { status: 404, ok: true, statusText: "No data found in Firebase Database." };

  const users = data.users || {};
  const userId = getInitialsFromContact(newUser).toUpperCase() + Date.now();
  const userExists = Object.values(users).some((user) => user.email === newUser.email);

  if (userExists) return { status: 400, ok: true, statusText: "User with this email already exists." };
  const response = await fetch(`${API_URL}/users/${userId}/.json`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(newUser),
  });

  return response;
}

/**
 * Retrieves a user from the Firebase Realtime Database using the provided credentials.
 *
 * @param {Object} credentials - The user's credentials.
 * @param {string} credentials.email - The email of the user to be retrieved.
 * @param {string} credentials.password - The password of the user to be verified.
 * @returns {Promise<Object>} A promise that resolves with an object containing the status of the operation,
 * an ok flag indicating success or failure, and either the user's data or an error message.
 */
async function getUserFromFirebaseDatabase({ email, password }) {
  const data = await getDataFromFirebase();

  if (!data) return { status: 404, ok: false, statusText: "No data found in Firebase Database." };

  const users = data.users || {};
  const user = Object.values(users).find((user) => user.email === email);

  if (!user) return { status: 400, ok: false, statusText: "No user with this email found." };
  if (email === "demo@join.com")
    return { status: 200, ok: true, user: { name: user.name, isDemo: true, isLoggedIn: !user.isLoggedIn } };
  if (user.password !== password) return { status: 401, ok: true, statusText: "Incorrect password." };

  return { status: 200, ok: true, user: { name: user.name, isLoggedIn: !user.isLoggedIn } };
}