/*
 * Copyright (C) 2000 - 2026 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception.  You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "https://www.silverpeas.org/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.silverpeas.mobile.shared.services.rest;

import com.google.gwt.http.client.URL;
import com.googlecode.gwt.crypto.bouncycastle.util.encoders.Base64;
import elemental2.core.Global;
import elemental2.core.JsArray;
import elemental2.dom.AbortController;
import elemental2.dom.DomGlobal;
import elemental2.dom.Headers;
import elemental2.dom.RequestInit;
import jsinterop.base.JsPropertyMap;
import org.silverpeas.mobile.client.common.DEBUG;
import org.silverpeas.mobile.client.common.network.SpMobileRequestBuilder;
import org.silverpeas.mobile.client.common.network.rest.RestCallback;
import org.silverpeas.mobile.client.common.network.rest.RestMethod;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

import static elemental2.dom.DomGlobal.fetch;

public class AbstractService {

    private static String token = null;
    public String encode(String param) {
        if (param != null) {
            return URL.encodeQueryString(param);
        }
        return param;
    }

    private RequestInit initRequest(String method, String contentType) {
        AbortController controller = new AbortController();
        RequestInit init = RequestInit.create();
        init.setMethod(method);

        Headers headers = new Headers();
        headers.append("Content-Type", contentType);
        headers.append("Authorization", "Basic " + token);
        init.setHeaders(headers);

        init.setSignal(controller.signal);

        DomGlobal.setTimeout(__ -> {
            controller.abort();
        }, SpMobileRequestBuilder.TIMEOUT);

        return init;
    }

    public String escapeJson(String s) {
        if (s == null) return "";

        return s.replace("\\", "\\\\")
                .replace("\"", "\\\"")
                .replace("\n", "\\n")
                .replace("\r", "\\r")
                .replace("\t", "\\t");
    }

    protected <T> void request(
            String method,
            String url,
            Object body,
            Function<Object, T> mapper,
            RestCallback<T> callback, String contentType) {

        if (isJsonResponse(contentType)) {
            requestJson(method, url, body, mapper, callback);
        } else {
            requestText(method, url, body, (RestCallback<String>) callback);
        }
    }

    protected <T> void requestJson(String method, String url, Object body, Function<Object, T> mapper,
            RestCallback<T> callback) {
        DEBUG.log(this, "requestJson " + method + " " + url);
        RequestInit init = initRequest(method, "application/json");

        if (body != null) {
            init.setBody(body.toString());
        }

        RestMethod restMethod = new RestMethod(init);

        fetch(url, init)
                .then(response -> {
                    restMethod.setStatusCode(response.status);
                    restMethod.setHeaders(response.headers);

                    if (!response.ok) {
                        throw new RuntimeException("HTTP " + response.status);
                    }

                    if (response.status == 204) {
                        return null;
                    }
                    return response.text();
                })
                .then(text -> {

                    if (text == null || text.trim().isEmpty()) {
                        callback.onSuccess(restMethod, null);
                        return null;
                    }

                    try {
                        Object json = Global.JSON.parse(text);
                        T result = mapper.apply(json);
                        callback.onSuccess(restMethod, result);
                    } catch (Exception e) {
                        callback.onFailure(restMethod, e);
                    }

                    return null;
                })
                .catch_(err -> {
                                   callback.onFailure(restMethod,
                            new RuntimeException(String.valueOf(err)));
                    return null;
                });
    }

    protected void requestText(String method, String url, Object body, RestCallback<String> callback) {
        DEBUG.log(this, "requestText " + method + " " + url);
        RequestInit init = initRequest(method, "text/plain");

        if (body != null) {
            init.setBody(body.toString());
        }

        RestMethod restMethod = new RestMethod(init);

        fetch(url, init)
                .then(response -> {
                    restMethod.setStatusCode(response.status);
                    restMethod.setHeaders(response.headers);

                    if (!response.ok) {
                        throw new RuntimeException("HTTP " + response.status);
                    }

                    if (response.status == 204) {
                        callback.onSuccess(restMethod, null);
                        return null;
                    }

                    return response.text();
                })
                .then(text -> {
                    callback.onSuccess(restMethod, text);
                            //(text == null || text.isEmpty()) ? null : text);
                    return null;
                })
                .catch_(err -> {
                    callback.onFailure(restMethod,
                            new RuntimeException(String.valueOf(err)));
                    return null;
                });
    }

    private boolean isJsonResponse(String contentType) {
        return contentType != null && contentType.contains("application/json");
    }

    protected <T> void request(
            String method,
            String url,
            Object body,
            Function<Object, T> mapper,
            RestCallback<T> callback) {

        request(method, url, body, mapper,callback, "application/json");

    }

    protected <T> void get(
            String url,
            Function<Object, T> mapper,
            RestCallback<T> callback) {
        request("GET", url, null, mapper, callback);
    }

    protected <T> void getText(
            String url,
            Function<Object, T> mapper,
            RestCallback<T> callback) {
        request("GET", url, null, mapper, callback, "text/plain");
    }

    protected <T> void post(
            String url,
            String body,
            Function<Object, T> mapper,
            RestCallback<T> callback) {

        request("POST", url, body, mapper, callback);
    }

    protected <T> void put(
            String url,
            String body,
            Function<Object, T> mapper,
            RestCallback<T> callback) {

        request("PUT", url, body, mapper, callback);
    }

    protected <T> void delete(
            String url,
            Object body,
            Function<Object, T> mapper,
            RestCallback<T> callback) {

        request("DELETE", url, body, mapper, callback);
    }

    protected String toJsonArray(List<String> values) {

        return values.stream()
                .map(v -> "\"" + escapeJson(v) + "\"")
                .collect(java.util.stream.Collectors.joining(",", "[", "]"));
    }

    protected <T> List<T> mapArray(
            Object result,
            Function<JsPropertyMap<Object>, T> mapper) {

        JsArray<Object> array = (JsArray<Object>) result;

        List<T> values = new ArrayList<>();

        for (int i = 0; i < array.length; i++) {
            values.add(
                    mapper.apply(
                            (JsPropertyMap<Object>) array.getAt(i)
                    )
            );
        }

        return values;
    }

    protected Boolean asBoolean(Object result) {
        return (Boolean) result;
    }

    public void initContext(final String login, final String password,
                            final String domainId) {
        String credentials = login + "@domain" + domainId + ":" + password;
        byte[] credentialsEncoded = Base64.encode(credentials.getBytes());
        this.token = convertByteArrayToString(credentialsEncoded);
    }

    private static String convertByteArrayToString(byte[] byteArray) {
        String s = "";
        for (int i = 0; i < byteArray.length; i++) {
            s += (char) (byteArray[i]);
        }
        return s;
    }
}
