/**
 * 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} from "../../constants.mjs";
import {isObject} from "../../types/is.mjs";
import {Datasource} from "../datasource.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.
 * 
 * ```
 * <script type="module">
 * import {RestAPI} from '@schukai/monster/source/data/datasource/restapi.mjs';
 * new RestAPI()
 * </script>
 * ```
 *
 * @example
 *
 * import {RestAPI} from '@schukai/monster/source/data/datasource/restapi.mjs';
 *
 * const ds = new RestAPI({
 *   url: 'https://httpbin.org/get'
 * },{
 *   url: 'https://httpbin.org/post'
 * });
 *
 * ds.set({flag:true})
 * ds.write().then(()=>console.log('done'));
 * ds.read().then(()=>console.log('done'));
 *
 * @license AGPLv3
 * @since 1.22.0
 * @copyright schukai GmbH
 * @memberOf Monster.Data.Datasource
 * @summary The LocalStorage class encapsulates the access to data objects.
 */
class RestAPI extends Datasource {

    /**
     *
     * @param {Object} [readDefinition] An options object containing any custom settings that you want to apply to the read request.
     * @param {Object} [writeDefinition] An options object containing any custom settings that you want to apply to the write request.
     * @throws {TypeError} value is not a string
     */
    constructor(readDefinition, writeDefinition) {
        super();

        const options = {}

        if (isObject(readDefinition)) options.read = readDefinition;
        if (isObject(writeDefinition)) options.write = writeDefinition;

        this.setOptions(options);

    }

    /**
     * @property {string} url=undefined Defines the resource that you wish to fetch.
     * @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 {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 {Object} write.report
     * @property {String} write.report.path Path to validations
     * @property {Monster.Data.Datasource~exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing.
     * @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: []
                },
                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 response;

        let init = self.getOption('read.init');
        if (!isObject(init)) init = {};

        return fetch(self.getOption('read.url'), init).then(resp => {
            response = resp;

            const acceptedStatus = self.getOption('read.acceptedStatus', [200]);

            if (acceptedStatus.indexOf(resp.status) === -1) {
                throw Error('the data cannot be read (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 + ').');
            }

            let transformation = self.getOption('read.mapping.transformer');
            if (transformation !== undefined) {
                const pipe = new Pipe(transformation);
                obj = pipe.run(obj);
            }

            self.set(obj);
            return response;
        })
    }

    /**
     * @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'
            }
        }

        let obj = self.get();
        let transformation = self.getOption('write.mapping.transformer');
        if (transformation !== undefined) {
            const pipe = new Pipe(transformation);
            obj = pipe.run(obj);
        }

        let sheathingObject = self.getOption('write.sheathing.object');
        let sheathingPath = self.getOption('write.sheathing.path');
        let reportPath = self.getOption('write.report.path');

        if (sheathingObject && sheathingPath) {
            const sub = obj;
            obj = sheathingObject;
            (new Pathfinder(obj)).setVia(sheathingPath, sub);
        }

        init['body'] = JSON.stringify(obj);

        return fetch(self.getOption('write.url'), init).then(response => {

            const acceptedStatus = self.getOption('write.acceptedStatus', [200, 2001]);

            if (acceptedStatus.indexOf(response.status) === -1) {

                return response.text().then((body) => {

                    let obj, validation;
                    try {
                        obj = JSON.parse(body);
                        validation = new Pathfinder(obj).getVia(reportPath)

                    } catch (e) {

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

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

                    throw new WriteError('the data cannot be written (response ' + response.status + ')', response, validation)

                })


            }

            return response;
        });
    }


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

}


/**
 * This callback can be passed to a datasource and is used to adapt data structures.
 *
 * @callback Monster.Data.Datasource~exampleCallback
 * @param {*} value Value
 * @param {string} key  Key
 * @memberOf Monster.Data
 * @see Monster.Data.Datasource
 */