"use strict";

import { getDatastreamIdFromBuildingNumber } from "./getDatastreamId.mjs";

/**
 * Create URL to fetch the details of single Datastream
 * @param {String} baseUrl Base URL of the STA server
 * @param {Number} datastreamID Integer representing the Datastream ID
 * @returns {String} URL string for fetching a single Datastream
 */
const createDatastreamUrl = function (baseUrl, datastreamID) {
  if (!datastreamID) return;
  return `${baseUrl}/Datastreams(${datastreamID})`;
};

/**
 * Create URL to fetch Observations
 * @param {String} baseUrl Base URL of the STA server
 * @param {Number} datastreamID Integer representing the Datastream ID
 * @returns {String} URL string for fetching Observations
 */
const createObservationsUrl = function (baseUrl, datastreamID) {
  if (!datastreamID) return;
  return `${baseUrl}/Datastreams(${datastreamID})/Observations`;
};

/**
 * Perform a GET request using the Axios library
 * @param {String} urlObservations A URL that fetches Observations from an STA instance
 * @param {Object} urlParamObj The URL parameters to be sent together with the GET request
 * @returns {Promise} A promise that contains the first page of results when fulfilled
 */
const performGetRequestUsingAxios = function (urlObservations, urlParamObj) {
  return axios.get(urlObservations, {
    params: urlParamObj,
  });
};

/**
 * Retrieve the metadata for a single datastream
 * @async
 * @param {String} urlDatastream A URL that fetches a Datastream from an STA instance
 * @returns {Promise} A promise that contains a metadata object for a Datastream when fulfilled
 */
const getMetadataFromSingleDatastream = async function (urlDatastream) {
  try {
    // Extract properties of interest
    const {
      data: { description, name, unitOfMeasurement },
    } = await performGetRequestUsingAxios(urlDatastream);

    return { description, name, unitOfMeasurement };
  } catch (err) {
    console.error(err);
  }
};

/**
 * Retrieve metadata from multiple datastreams
 * @async
 * @param {Array} datastreamsUrlArr An array that contains N Datastream URL strings
 * @returns {Promise} A promise that contains an array of N Datastream metadata objects when fulfilled
 */
const getMetadataFromMultipleDatastreams = async function (datastreamsUrlArr) {
  try {
    // Array to store our final result
    const datastreamMetadataArr = [];

    // Use for/of loop - we need to maintain the order of execution of the async operations
    for (const datastreamUrl of datastreamsUrlArr) {
      // Metadata from a single Datastream
      const datastreamMetadata = await getMetadataFromSingleDatastream(
        datastreamUrl
      );
      datastreamMetadataArr.push(datastreamMetadata);
    }

    return datastreamMetadataArr;
  } catch (err) {
    console.error(err);
  }
};

/**
 * Traverses all the pages that make up the response from a SensorThingsAPI instance. The link to the next page, if present, is denoted by the presence of a "@iot.nextLink" property in the response object. This function concatenates all the values so that the complete results are returned in one array.
 * @async
 * @param {Promise} httpGetRequestPromise Promise object resulting from an Axios GET request
 * @returns {Promise} A promise that contains an object containing results from all the pages when fulfilled
 */
const combineResultsFromAllPages = async function (httpGetRequestPromise) {
  try {
    if (!httpGetRequestPromise) return;

    const lastSuccess = await httpGetRequestPromise;

    // The "success" objects contain a "data" object which in turn has a "value" property
    // If the "data" object in turn has a "@iot.nextLink" property, then a next page exists
    if (lastSuccess.data["@iot.nextLink"]) {
      const nextLinkSuccess = await combineResultsFromAllPages(
        axios.get(lastSuccess.data["@iot.nextLink"])
      );
      // The "data" object in turn has a "value" property
      // The "value" property's value is an array
      nextLinkSuccess.data.value = lastSuccess.data.value.concat(
        nextLinkSuccess.data.value
      );
      return nextLinkSuccess;
    } else {
      return lastSuccess;
    }
  } catch (err) {
    console.error(err);
  }
};

/**
 * Traverses all the pages that make up the response from a SensorThingsAPI instance and extracts the combined Observations
 * @async
 * @param {Promise} httpGetRequestPromise Promise object resulting from an Axios GET request
 * @returns {Promise} A promise that contains an array of Observations when fulfilled
 */
const extractCombinedObservationsFromAllPages = async function (
  httpGetRequestPromise
) {
  try {
    const successResponse = await combineResultsFromAllPages(
      httpGetRequestPromise
    );

    // Extract value array from the success response object
    const {
      data,
      data: { value: valueArr },
    } = successResponse;

    // Array that will hold the combined observations
    const combinedObservations = [];

    valueArr.forEach((val) => {
      // Each page of results will have a dataArray that holds the observations
      const { dataArray } = val;
      combinedObservations.push(...dataArray);
    });

    return new Promise((resolve, reject) => {
      resolve(combinedObservations);
    });
  } catch (err) {
    console.error(err);
  }
};

/**
 * Retrieve all the Observations from an array of Observations promises
 * @async
 * @param {Promise} observationPromiseArray An array that contains N observation promises
 * @returns {Promise} A promise that contains an array of Observations from multiple Datastreams when fulfilled
 */
const getObservationsFromMultipleDatastreams = async function (
  observationPromiseArray
) {
  try {
    // Array to store our final result
    const observationsAllDatastreamsArr = [];

    // Use for/of loop - we need to maintain the order of execution of the async operations
    for (const observationPromise of observationPromiseArray) {
      // Observations from a single Datastream
      const observations = await observationPromise;
      observationsAllDatastreamsArr.push(observations);
    }

    return observationsAllDatastreamsArr;
  } catch (err) {
    console.error(err);
  }
};

/**
 * Retrieve the metadata from a single Datastream or multiple Datastreams and the Observations corresponding to the Datastream(s)
 * @async
 * @param {String} baseUrl Base URL of the STA server
 * @param {Object} urlParamObj The URL parameters to be sent together with the GET request
 * @param {Array} bldgSensorSamplingRateNestedArr A N*1 array (where N >= 1) containing a nested array of buildings, sensors & sampling rates as strings, i.e. [["101", "rl", "15min"]] or [["101", "rl", "15min"], ["102", "vl", "60min"]] or [["101", "rl", "15min"], ["102", "vl", "60min"], ["225", "vl", "60min"]], etc
 * @returns {Promise} A promise that contains a 1*2 array (the first element is an array that contans N Observations arrays; and the second element is an array of N Datastream metadata objects) when fulfilled
 */
const getMetadataPlusObservationsFromSingleOrMultipleDatastreams =
  async function (baseUrl, urlParamObj, bldgSensorSamplingRateNestedArr) {
    try {
      if (!bldgSensorSamplingRateNestedArr) return;

      // Datastreams IDs
      const datastreamsIdsArr = bldgSensorSamplingRateNestedArr.map(
        (bldgSensorSamplingRateArr) =>
          getDatastreamIdFromBuildingNumber(...bldgSensorSamplingRateArr)
      );

      // Observations URLs
      const observationsUrlArr = datastreamsIdsArr.map((datastreamId) =>
        createObservationsUrl(baseUrl, datastreamId)
      );

      // Datastreams URLs
      const datastreamsUrlArr = datastreamsIdsArr.map((datastreamId) =>
        createDatastreamUrl(baseUrl, datastreamId)
      );

      // Promise objects - Observations
      const observationsPromisesArr = observationsUrlArr.map((obsUrl) =>
        extractCombinedObservationsFromAllPages(
          performGetRequestUsingAxios(obsUrl, urlParamObj)
        )
      );

      // Observations array
      const observationsArr = await getObservationsFromMultipleDatastreams(
        observationsPromisesArr
      );

      // Metadata array
      const metadataArr = await getMetadataFromMultipleDatastreams(
        datastreamsUrlArr
      );

      return [observationsArr, metadataArr];
    } catch (err) {
      console.error(err);
    }
  };

export { getMetadataPlusObservationsFromSingleOrMultipleDatastreams };
