/**
 * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
 * Node module: @schukai/monster
 * This file is licensed under the AGPLv3 License.
 * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
 */

import { internalSymbol, instanceSymbol } from "../../../constants.mjs";
import { isObject, isFunction } from "../../../types/is.mjs";
import { Server } from "../server.mjs";
import { WriteError } from "./restapi/writeerror.mjs";
import { DataFetchError } from "./restapi/data-fetch-error.mjs";

export { RestAPI };

/**
 * @type {symbol}
 * @memberOf Monster
 * @license AGPLv3
 * @since 3.12.0
 */
const rawDataSymbol = Symbol.for(
	"@schukai/monster/data/datasource/server/restapi/rawdata",
);

/**
 * The RestAPI is a class that enables a REST API server.
 *
 * @externalExample ../../../../example/data/datasource/server/restapi.mjs
 * @license AGPLv3
 * @since 1.22.0
 * @copyright schukai GmbH
 * @memberOf Monster.Data.Datasource.Server
 * @summary The RestAPI is a class that binds a REST API server.
 */
class RestAPI extends Server {
	/**
	 *
	 * @param {Object} [options] options contains definitions for the datasource.
	 */
	constructor(options) {
		super();

		if (isObject(options)) {
			this.setOptions(options);
		}
	}

	/**
	 * This method is called by the `instanceof` operator.
	 * @returns {symbol}
	 * @since 2.1.0
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/data/datasource/server/restapi");
	}

	/**
	 * @property {Object} write={} Options
	 * @property {Object} write.init={} An options object containing any custom settings that you want to apply to the request. The parameters are identical to those of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request|Request constructor}
	 * @property {string} write.init.method=POST
	 * @property {Object} write.init.headers Object containing any custom headers that you want to apply to the request.
	 * @property {string} write.responseCallback Callback function to be executed after the request has been completed.
	 * @property {string} write.acceptedStatus=[200,201]
	 * @property {string} write.url URL
	 * @property {Object} write.mapping the mapping is applied before writing.
	 * @property {String} write.mapping.transformer Transformer to select the appropriate entries
	 * @property {Monster.Data.Datasource~exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing.
	 * @property {Object} write.report
	 * @property {String} write.report.path Path to validations
	 * @property {Object} write.sheathing
	 * @property {Object} write.sheathing.object Object to be wrapped
	 * @property {string} write.sheathing.path Path to the data
	 * @property {Object} read={} Options
	 * @property {Object} read.init={} An options object containing any custom settings that you want to apply to the request. The parameters are identical to those of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request|Request constructor}
	 * @property {string} read.init.method=GET
	 * @property {string} read.acceptedStatus=[200]
	 * @property {string} read.url URL
	 * @property {Object} read.mapping the mapping is applied after reading.
	 * @property {String} read.mapping.transformer Transformer to select the appropriate entries
	 * @property {Monster.Data.Datasource~exampleCallback[]} read.mapping.callback with the help of the callback, the structures can be adjusted after reading.
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			write: {
				init: {
					method: "POST",
				},
				responseCallback: undefined,
				acceptedStatus: [200, 201],
				url: undefined,
				mapping: {
					transformer: undefined,
					callbacks: [],
				},
				sheathing: {
					object: undefined,
					path: undefined,
				},
				report: {
					path: undefined,
				},
			},
			read: {
				init: {
					method: "GET",
				},
				responseCallback: undefined,
				acceptedStatus: [200],
				url: undefined,
				mapping: {
					transformer: undefined,
					callbacks: [],
				},
			},
		});
	}

	/**
	 * @return {Promise}
	 * @throws {Error} the options does not contain a valid json definition
	 * @throws {TypeError} value is not a object
	 * @throws {Error} the data cannot be read
	 */
	read() {
		const self = this;

		let init = self.getOption("read.init");
		if (!isObject(init)) init = {};
		if (!init["method"]) init["method"] = "GET";

		let callback = self.getOption("read.responseCallback");
		if (!callback)
			callback = (obj) => {
				self.set(self.transformServerPayload.call(self, obj));
			};

		return fetchData.call(this, init, "read", callback);
	}

	/**
	 * @return {Promise}
	 * @throws {WriteError} the data cannot be written
	 */
	write() {
		const self = this;

		let init = self.getOption("write.init");
		if (!isObject(init)) init = {};
		if (typeof init["headers"] !== "object") {
			init["headers"] = {
				"Content-Type": "application/json",
			};
		}
		if (!init["method"]) init["method"] = "POST";

		let obj = self.prepareServerPayload(self.get());
		init["body"] = JSON.stringify(obj);

		let callback = self.getOption("write.responseCallback");
		return fetchData.call(this, init, "write", callback);
	}

	/**
	 * @return {RestAPI}
	 */
	getClone() {
		const self = this;
		return new RestAPI(
			self[internalSymbol].getRealSubject()["options"].read,
			self[internalSymbol].getRealSubject()["options"].write,
		);
	}
}

/**
 * @private
 * @param init
 * @param key
 * @param callback
 * @returns {Promise<string>}
 */
function fetchData(init, key, callback) {
	const self = this;
	let response;

	return fetch(self.getOption(`${key}.url`), init)
		.then((resp) => {
			response = resp;

			const acceptedStatus = self.getOption(`${key}.acceptedStatus`, [200]);

			if (acceptedStatus.indexOf(resp.status) === -1) {
				throw new DataFetchError(
					`the response does not contain a accepted status (actual: ${resp.status}).`,
					response,
				);
			}

			return resp.text();
		})
		.then((body) => {
			let obj;

			try {
				obj = JSON.parse(body);

				response[rawDataSymbol] = obj;
			} catch (e) {
				if (body.length > 100) {
					body = `${body.substring(0, 97)}...`;
				}

				throw new DataFetchError(
					`the response does not contain a valid json (actual: ${body}).`,
					response,
				);
			}

			if (callback && isFunction(callback)) {
				callback(obj);
			}

			return response;
		});
}