/*
 * JBoss, Home of Professional Open Source
 *
 * Copyright 2015 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed 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.keycloak.authorization.client.resource;

import java.util.List;
import java.util.concurrent.Callable;

import com.fasterxml.jackson.core.type.TypeReference;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.util.Http;
import org.keycloak.authorization.client.util.HttpMethod;
import org.keycloak.authorization.client.util.Throwables;
import org.keycloak.authorization.client.util.TokenCallable;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.util.JsonSerialization;

/**
 * An entry point for managing resources using the Protection API.
 *
 * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
 */
public class ProtectedResource {

    private final Http http;
    private ServerConfiguration serverConfiguration;
    private final Configuration configuration;
    private final TokenCallable pat;

    ProtectedResource(Http http, ServerConfiguration serverConfiguration, Configuration configuration, TokenCallable pat) {
        this.http = http;
        this.serverConfiguration = serverConfiguration;
        this.configuration = configuration;
        this.pat = pat;
    }

    /**
     * Creates a new resource.
     *
     * @param resource the resource data
     * @return a {@link RegistrationResponse}
     */
    public ResourceRepresentation create(final ResourceRepresentation resource) {
        Callable<ResourceRepresentation> callable = new Callable<ResourceRepresentation>() {
            @Override
            public ResourceRepresentation call() throws Exception {
                return http.<ResourceRepresentation>post(serverConfiguration.getResourceRegistrationEndpoint())
                        .authorizationBearer(pat.call())
                        .json(JsonSerialization.writeValueAsBytes(resource))
                        .response().json(ResourceRepresentation.class).execute();
            }
        };
        try {
            return callable.call();
        } catch (Exception cause) {
            return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not create resource", cause);
        }
    }

    /**
     * Updates a resource.
     *
     * @param resource the resource data
     * @return a {@link RegistrationResponse}
     */
    public void update(final ResourceRepresentation resource) {
        if (resource.getId() == null) {
            throw new IllegalArgumentException("You must provide the resource id");
        }

        Callable callable = new Callable() {
            @Override
            public Object call() throws Exception {
                http.<ResourceRepresentation>put(serverConfiguration.getResourceRegistrationEndpoint() + "/" + resource.getId())
                        .authorizationBearer(pat.call())
                        .json(JsonSerialization.writeValueAsBytes(resource)).execute();
                return null;
            }
        };
        try {
            callable.call();
        } catch (Exception cause) {
            Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not update resource", cause);
        }
    }

    /**
     * Query the server for a resource given its <code>id</code>.
     *
     * @param id the resource id
     * @return a {@link ResourceRepresentation}
     */
    public ResourceRepresentation findById(final String id) {
        Callable<ResourceRepresentation> callable = new Callable<ResourceRepresentation>() {
            @Override
            public ResourceRepresentation call() throws Exception {
                return http.<ResourceRepresentation>get(serverConfiguration.getResourceRegistrationEndpoint() + "/" + id)
                        .authorizationBearer(pat.call())
                        .response().json(ResourceRepresentation.class).execute();
            }
        };
        try {
            return callable.call();
        } catch (Exception cause) {
            return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not find resource", cause);
        }
    }

    /**
     * Query the server for a resource given its <code>name</code> where the owner is the resource server itself.
     *
     * @param name the resource name
     * @return a {@link ResourceRepresentation}
     */
    public ResourceRepresentation findByName(String name) {
        List<ResourceRepresentation> representations = find(null, name, null, configuration.getResource(), null, null, false, true, null, null);

        if (representations.isEmpty()) {
            return null;
        }

        return representations.get(0);
    }

    /**
     * Query the server for a resource given its <code>name</code> and a given <code>ownerId</code>.
     *
     * @param name the resource name
     * @param ownerId the owner id
     * @return a {@link ResourceRepresentation}
     */
    public ResourceRepresentation findByName(String name, String ownerId) {
        List<ResourceRepresentation> representations = find(null, name, null, ownerId, null, null, false, true,null, null);

        if (representations.isEmpty()) {
            return null;
        }

        return representations.get(0);
    }

    /**
     * Query the server for any resource with the matching arguments.
     *
     * @param id the resource id
     * @param name the resource name
     * @param uri the resource uri
     * @param owner the resource owner
     * @param type the resource type
     * @param scope the resource scope
     * @param matchingUri the resource uri. Use this parameter to lookup a resource that best match the given uri
     * @param firstResult the position of the first resource to retrieve
     * @param maxResult the maximum number of resources to retrieve
     * @return an array of strings with the resource ids
     */
    public String[] find(final String id, final String name, final String uri, final String owner, final String type, final String scope, final boolean matchingUri, final Integer firstResult, final Integer maxResult) {
        Callable<String[]> callable = new Callable<String[]>() {
            @Override
            public String[] call() throws Exception {
                return (String[]) createFindRequest(id, name, uri, owner, type, scope, matchingUri, false, firstResult, maxResult).response().json(String[].class).execute();
            }
        };
        try {
            return callable.call();
        } catch (Exception cause) {
            return Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not find resource", cause);
        }
    }

    /**
     * Query the server for any resource with the matching arguments.
     *
     * @param id the resource id
     * @param name the resource name
     * @param uri the resource uri
     * @param owner the resource owner
     * @param type the resource type
     * @param scope the resource scope
     * @param matchingUri the resource uri. Use this parameter to lookup a resource that best match the given uri
     * @param deep if the result should be a list of resource representations with details about the resource. If false, only ids are returned
     * @param firstResult the position of the first resource to retrieve
     * @param maxResult the maximum number of resources to retrieve
     * @return a list of resource representations or an array of strings representing resource ids, depending on the generic type
     */
    public <R> R find(final String id, final String name, final String uri, final String owner, final String type, final String scope, final boolean matchingUri, final boolean deep, final Integer firstResult, final Integer maxResult) {
        if (deep) {
            Callable<List<ResourceRepresentation>> callable = new Callable<List<ResourceRepresentation>>() {
                @Override
                public List<ResourceRepresentation> call() {
                    return (List<ResourceRepresentation>) createFindRequest(id, name, uri, owner, type, scope, matchingUri, deep, firstResult, maxResult).response().json(new TypeReference<List<ResourceRepresentation>>() {
                    }).execute();
                }
            };
            try {
                return (R) callable.call();
            } catch (Exception cause) {
                return (R) Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "Could not find resource", cause);
            }
        }

        return (R) find(id, name, uri, owner, type, scope, matchingUri, firstResult, maxResult);
    }

    /**
     * Query the server for all resources.
     *
     * @return @return an array of strings with the resource ids
     */
    public String[] findAll() {
        try {
            return find(null,null , null, null, null, null, false, null, null);
        } catch (Exception cause) {
            throw Throwables.handleWrapException("Could not find resource", cause);
        }
    }

    /**
     * Deletes a resource with the given <code>id</code>.
     *
     * @param id the resource id
     */
    public void delete(final String id) {
        Callable callable = new Callable() {
            @Override
            public Object call() throws Exception {
                http.delete(serverConfiguration.getResourceRegistrationEndpoint() + "/" + id)
                        .authorizationBearer(pat.call())
                        .execute();
                return null;
            }
        };
        try {
            callable.call();
        } catch (Exception cause) {
            Throwables.retryAndWrapExceptionIfNecessary(callable, pat, "", cause);
        }
    }

    /**
     * Query the server for all resources with the given uri.
     *
     * @param uri the resource uri
     */
    public List<ResourceRepresentation> findByUri(String uri) {
        return find(null, null, uri, null, null, null, false, true, null, null);
    }

    /**
     * Returns a list of resources that best matches the given {@code uri}. This method queries the server for resources whose
     * {@link ResourceRepresentation#uri} best matches the given {@code uri}.
     *
     * @param uri the resource uri to match
     * @return a list of resources
     */
    public List<ResourceRepresentation> findByMatchingUri(String uri) {
        return find(null, null, uri, null, null, null, true, true,null, null);
    }

    private HttpMethod createFindRequest(String id, String name, String uri, String owner, String type, String scope, boolean matchingUri, boolean deep, Integer firstResult, Integer maxResult) {
        return http.get(serverConfiguration.getResourceRegistrationEndpoint())
                .authorizationBearer(pat.call())
                .param("_id", id)
                .param("name", name)
                .param("uri", uri)
                .param("owner", owner)
                .param("type", type)
                .param("scope", scope)
                .param("matchingUri", Boolean.valueOf(matchingUri).toString())
                .param("deep", Boolean.toString(deep))
                .param("first", firstResult != null ? firstResult.toString() : null)
                .param("max", maxResult != null ? maxResult.toString() : Integer.toString(-1));
    }
}
