/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.causeway.viewer.restfulobjects.client;

import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.function.UnaryOperator;

import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Invocation.Builder;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;

import org.apache.causeway.applib.value.semantics.ValueDecomposition;
import org.apache.causeway.applib.value.semantics.ValueSemanticsProvider;
import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.commons.functional.Try;
import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.internal.context._Context;
import org.apache.causeway.viewer.restfulobjects.client.auth.AuthFilter;
import org.apache.causeway.viewer.restfulobjects.client.auth.AuthorizationHeaderFactory;
import org.apache.causeway.viewer.restfulobjects.client.log.ClientConversationLogger;

import org.jspecify.annotations.NonNull;
import lombok.extern.log4j.Log4j2;

/**
 * Setup the Restful Client with Basic-Auth:
 *
 * <p>
 *     For example:
 * </p>
 *
 * <blockquote><pre>
var clientConfig = RestfulClientConfig.builder();
    .restfulBaseUrl("http://localhost:8080/helloworld/restful/")
        .authenticationMode(AuthenticationMode.BASIC)
        .basicAuthUser("sven")
        .basicAuthPassword("pass")
// setup request/response debug logging
.useRequestDebugLogging(true) // default = false
.build();

RestfulClient client = RestfulClient.ofConfig(clientConfig);
 * </pre></blockquote>
 *
 * Make a Request and then digest the Response:
 * <blockquote><pre>{@code

Builder request = client.request("services/myService/actions/lookupMyObjectById/invoke")
    .accept(RestfulClientMediaType.SIMPLE_JSON.mediaTypeFor(MyObject.class, EnumSet.of(SuppressionType.RO)));

Entity<String> args = client.arguments()
        .addActionParameter("id", "12345")
        .build();

Response response = request.post(args);

Try<MyObject> digest = client.digest(response, MyObject.class);
}

if(digest.isSuccess()) {
    System.out.println("result: "+ digest.getValue().orElseThrow().get$$instanceId());
} else {
    digest.getFailure().get().printStackTrace();
}
 * </pre></blockquote>
 *
 * @since 2.0 {@index}
 */
@Log4j2
public class RestfulClient implements AutoCloseable {

    private final RestfulClientConfig clientConfig;
    private final AuthorizationHeaderFactory authorizationHeaderFactory;
    private final Client client;

    /**
     * @param clientConfig - used to derive authorization (either BASIC or OAUTH)
     */
    public static RestfulClient ofConfig(
            final RestfulClientConfig clientConfig) {
        return new RestfulClient(clientConfig, UnaryOperator.identity(), AuthorizationHeaderFactory.factoryFor(clientConfig));
    }

    /**
     * @param clientConfig - NOT used for auth; instead the authorizationHeaderFactory is used.
     * @param authorizationHeaderFactory
     */
    public static RestfulClient ofConfig(
            final RestfulClientConfig clientConfig,
            final AuthorizationHeaderFactory authorizationHeaderFactory) {
        return new RestfulClient(clientConfig, UnaryOperator.identity(), authorizationHeaderFactory);
    }

    public static RestfulClient ofConfig(
            final RestfulClientConfig clientConfig,
            final UnaryOperator<ClientBuilder> configRefiner) {
        return new RestfulClient(clientConfig, configRefiner, AuthorizationHeaderFactory.factoryFor(clientConfig));
    }

    public static RestfulClient ofConfig(
            final RestfulClientConfig clientConfig,
            final UnaryOperator<ClientBuilder> configRefiner,
            final AuthorizationHeaderFactory authorizationHeaderFactory) {
        return new RestfulClient(clientConfig, configRefiner, authorizationHeaderFactory);
    }

    private RestfulClient(
            final @NonNull RestfulClientConfig clientConfig,
            final @NonNull UnaryOperator<ClientBuilder> configRefiner,
            final AuthorizationHeaderFactory authorizationHeaderFactory) {

        this.clientConfig = clientConfig;
        this.authorizationHeaderFactory = authorizationHeaderFactory;

        final ClientBuilder clientBuilder = configRefiner.apply(ClientBuilder.newBuilder());
        this.client = clientBuilder.build();

        registerDefaultJsonProvider();
        registerAuthFilter(this.authorizationHeaderFactory);
        registerConversationFilters();
    }

    public RestfulClientConfig getConfig() {
        return clientConfig;
    }

    public Client getJaxRsClient() {
        return client;
    }

    @Override
    public void close() {
        if (client == null) {
            return;
        }
        try {
            client.close();
        } catch (Throwable ex) {
            // just ignore
        }
    }

    // -- REQUEST BUILDER

    public Builder request(final String path) {
        return client
                .target(relativePathToUri(path))
                .request();
    }

    // -- ARGUMENT BUILDER

    public ActionParameterListBuilder arguments() {
        return new ActionParameterListBuilder(this);
    }

    // -- RESPONSE PROCESSING

    public <T> Try<T> digest(final Response response, final Class<T> entityType) {
        final var digest = ResponseDigest.wrap(response, entityType);
        if(digest.isSuccess()) {
            return Try.success(digest.getEntity().orElse(null));
        }
        return Try.failure(digest.getFailureCause());
    }

    public <T> Try<Can<T>> digestList(final Response response, final Class<T> entityType, final GenericType<List<T>> genericType) {
        final var listDigest = ResponseDigest.wrapList(response, entityType, genericType);
        if(listDigest.isSuccess()) {
            return Try.success(listDigest.getEntities());
        }
        return Try.failure(listDigest.getFailureCause());
    }

    /**
     * For transport of {@link ValueDecomposition} over REST.
     * @see ActionParameterListBuilder#addActionParameter(String, ValueDecomposition)
     */
    public <T> Try<T> digestValue(final Response response, final ValueSemanticsProvider<T> valSemantics) {
        return digest(response, ValueDecomposition.class)
                .mapEmptyToFailure()
                .mapSuccessAsNullable(valDecomposition->valSemantics.compose(valDecomposition));
    }

    // -- UTILITY

    /**
     * Returns an {@link URI} constructed from this client's base path plus given relative {@code path}.
     * @param path relative to this client's base
     */
    public URI uri(final String path) {
        return relativePathToUri(path).build();
    }

    // -- HELPER FILTER

    private void registerDefaultJsonProvider() {
        try {
            Class<?> MOXyJsonProvider = _Context.loadClass("org.eclipse.persistence.jaxb.rs.MOXyJsonProvider");
            client.register(MOXyJsonProvider);
        } catch (Exception e) {
            log.warn("This implementation of RestfulClient does require the class 'MOXyJsonProvider'"
                    + " on the class-path."
                    + " Are you missing a maven dependency?");
        }
    }

    private void registerAuthFilter(final AuthorizationHeaderFactory authorizationHeaderFactory) {
        client.register(AuthFilter.of(authorizationHeaderFactory));
    }

    private void registerConversationFilters() {
        if(clientConfig.isUseRequestDebugLogging()){
            client.register(new ClientConversationLogger());
        }
        clientConfig.getClientConversationFilters().stream()
        .filter(Objects::nonNull)
        .forEach(client::register);
    }

    // -- HELPER OTHER

    private UriBuilder relativePathToUri(String path) {
        final String baseUri = _Strings.suffix(clientConfig.getRestfulBaseUrl(), "/");
        while(path.startsWith("/")) {
            path = path.substring(1);
        }
        return UriBuilder.fromUri(baseUri + path);
    }

}
