Source: src/js/models/sysmeta/SysMeta.js

define([], () => {
  // TODO: update eslint config to ecmaVersion 2022 to support static class
  // fields, then move these constants to the class body

  const SIMPLE_TEXT_FIELDS = [
    "identifier",
    "formatId",
    "submitter",
    "rightsHolder",
    "obsoletes",
    "obsoletedBy",
    "originMemberNode",
    "authoritativeMemberNode",
    "fileName",
  ];

  const SIMPLE_NUMBER_FIELDS = ["serialVersion", "size"];
  const SIMPLE_BOOLEAN_FIELDS = ["archived"];
  const DATE_FIELDS = ["dateUploaded", "dateSysMetadataModified"];
  const DEFAULTS = {
    identifier: null,
    formatId: null,
    size: null,
    checksum: null,
    checksumAlgorithm: null,
    submitter: null,
    rightsHolder: null,
    dateUploaded: null,
    dateSysMetadataModified: null,
    originMemberNode: null,
    authoritativeMemberNode: null,
    accessPolicy: [],
    replicationAllowed: false,
    numberReplicas: 0,
    preferredNodes: [],
    blockedNodes: [],
    obsoletes: null,
    obsoletedBy: null,
    archived: false,
    serialVersion: null,
  };

  // TODO: Add more fields like accessPolicy, replicationPolicy, etc.
  // TODO: Add node order constant for serialization

  /**
   * Class representing System Metadata for a DataONE object. This class
   * currently only provides a basic implementation for fetching and parsing
   * system metadata from a DataONE service. It excludes parsing complex
   * elements like accessPolicy and replicationPolicy. In the future, all fields
   * will be implemented, and the class will support serialization to XML and
   * updating system metadata on the server.
   * @property {object} data - The object that contains all the system metadata
   * fields, like identifier, formatId, size, checksum, etc.
   * @property {Array} errors - An array to hold any errors that occur during
   * the fetch operation.
   * @property {boolean} parsed - Indicates whether the system metadata has been
   * parsed from XML.
   * @class SystemMetadata
   * @since 2.34.0
   */
  class SystemMetadata {
    /**
     * Creates an instance of SystemMetadata.
     * @class
     * @param {object} data SystemMetadata fields and values, if known.
     */
    constructor(data = {}) {
      // Deep copy defaults so that nested objects/arrays are not shared
      const defaults = JSON.parse(JSON.stringify(DEFAULTS));
      this.data = { ...defaults, ...data };
    }

    /**
     * Attempts to parse an xml error object returned from DataONE, e.g.:
     * <?xml version="1.0" encoding="UTF-8"?><error detailCode="1040" errorCode="401" name="NotAuthorized">
     * <description>READ not allowed on urn:uuid:c6556d90-4f58-4439-a309-a517a4fe3dc3 for subject[s]: public; </description>
     * </error>
     * @param {string} text - The XML string to parse.
     * @returns {Error|null} Returns a SysMetaError with the error message and status
     * if the XML contains an error element, or null if no error is found.
     */
    static parseError(text) {
      if (!text) return null;

      const parser = new DOMParser();
      const xmlDoc = parser.parseFromString(text, "application/xml");
      const errorEl = xmlDoc.querySelector("error");

      if (!errorEl) return null;

      const message = errorEl.querySelector("description")
        ? errorEl.querySelector("description").textContent.trim()
        : "Unknown error";
      const status = errorEl.getAttribute("errorCode") || "unknown";

      // return new SysMetaError(message, status);
      const error = new Error(message);
      error.name = "SysMetaError";
      error.status = status;
      return error;
    }

    /**
     * Parses the XML string into a system metadata object.
     * @param {string} xmlString - The XML string to parse.
     * @returns {object} The parsed system metadata object.
     */
    parse(xmlString) {
      this.parsed = false;
      this.parseError = false;
      this.fetchedXmlString = xmlString;
      const parser = new DOMParser();
      const xmlDoc = parser.parseFromString(xmlString, "application/xml");
      // Detect XML parser errors (e.g., when the server returns HTML)
      if (xmlDoc.querySelector("parsererror")) {
        this.parseError = true;
        throw new Error("Invalid SystemMetadata XML");
      }
      const defaults = JSON.parse(JSON.stringify(DEFAULTS));
      const sysMeta = { ...defaults };

      const getText = (tag) => {
        const el = xmlDoc.querySelector(tag);
        return el ? el.textContent.trim() : null;
      };

      // Simple fields
      SIMPLE_TEXT_FIELDS.forEach((field) => {
        sysMeta[field] = getText(field);
      });

      SIMPLE_NUMBER_FIELDS.forEach((field) => {
        const value = getText(field);
        if ((value || value === "0") && !isNaN(value)) {
          sysMeta[field] = parseInt(value, 10);
        }
      });

      SIMPLE_BOOLEAN_FIELDS.forEach((field) => {
        const value = getText(field);
        if (value !== null) sysMeta[field] = value.toLowerCase() === "true";
      });

      DATE_FIELDS.forEach((field) => {
        const value = getText(field);
        if (value !== null) sysMeta[field] = new Date(value);
      });

      const checksumEl = xmlDoc.querySelector("checksum");
      if (checksumEl) {
        sysMeta.checksum = checksumEl.textContent.trim();
        sysMeta.checksumAlgorithm = checksumEl.getAttribute("algorithm");
      }

      this.data = sysMeta;

      // TODO: accessPolicy, replicationPolicy, etc.
      this.parsed = true;
      return sysMeta;
    }

    /**
     * Return a JSON-serializable representation of the SystemMetadata.
     * @returns {object} The JSON representation of the SystemMetadata, as a new
     * and unreferenced object.
     * @param {boolean} [includeErrors] Whether to include any parsing errors in
     * the output. If true, the `errors` array will be included, and if there
     * was a parsing error, an additional Error object will be added to the
     * array. If false, no errors will be included.
     * @param {Array} [otherFields] An array of other SystemMetadata fields to
     * include in the output, if they exist on the instance.
     */
    toJSON(includeErrors = true, otherFields = []) {
      const json = JSON.parse(JSON.stringify(this.data));
      if (includeErrors) {
        json.errors = this.errors ? [...this.errors] : [];
        if (this.parseError) {
          json.errors.push(new Error("Failed to parse SystemMetadata XML"));
        }
      }
      if (Array.isArray(otherFields) && otherFields.length > 0) {
        otherFields.forEach((field) => {
          if (this[field] !== undefined) {
            json[field] = this[field];
          }
        });
      }
      return json;
    }
  }

  /**
   * Create a SystemMetadata instance from an XML string.
   * @param {string} xmlString XML string to parse.
   * @returns {SystemMetadata} Parsed SystemMetadata instance.
   */
  SystemMetadata.fromXml = function fromXml(xmlString) {
    const sysMeta = new SystemMetadata();
    sysMeta.parse(xmlString);
    return sysMeta;
  };

  return SystemMetadata;
});