/**
 * Copyright schukai GmbH and contributors 2022. 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} from "../../../types/is.mjs";
import {Server} from "../server.mjs";
import {Pathfinder} from "../../pathfinder.mjs";
import {Pipe} from "../../pipe.mjs";
import {WriteError} from "./restapi/writeerror.mjs";

export {RestAPI}

/**
 * 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.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',
                },
                acceptedStatus: [200, 201],
                url: undefined,
                mapping: {
                    transformer: undefined,
                    callbacks: []
                },
                sheathing: {
                    object: undefined,
                    path: undefined,
                },
                report: {
                    path: undefined
                }
            },
            read: {
                init: {
                    method: 'GET'
                },
                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';

        return fetchData.call(this, 'read', (obj) => {
            self.set(self.transformServerPayload.call(self, obj));
        });

    }

    /**
     * @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);

        return fetchData.call(this, init, 'write');
    }


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

}


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 Error('the data cannot be ' + key + ' (response ' + resp.status + ')')
        }

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

        let obj;

        try {
            obj = JSON.parse(body);

        } catch (e) {

            if (body.length > 100) {
                body = body.substring(0, 97) + '...';
            }

            throw new Error('the response does not contain a valid json (actual: ' + body + ').');
        }

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

    });


}