/*
 * Copyright 2016 The AppAuth for Android Authors. All Rights Reserved.
 *
 * 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 net.openid.appauth;

import static net.openid.appauth.AdditionalParamsProcessor.builtInParams;
import static net.openid.appauth.AdditionalParamsProcessor.checkAdditionalParams;
import static net.openid.appauth.Preconditions.checkCollectionNotEmpty;
import static net.openid.appauth.Preconditions.checkNotEmpty;
import static net.openid.appauth.Preconditions.checkNotNull;

import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class RegistrationRequest {
    /**
     * OpenID Connect 'application_type'.
     */
    public static final String APPLICATION_TYPE_NATIVE = "native";

    static final String PARAM_REDIRECT_URIS = "redirect_uris";
    static final String PARAM_RESPONSE_TYPES = "response_types";
    static final String PARAM_GRANT_TYPES = "grant_types";
    static final String PARAM_APPLICATION_TYPE = "application_type";
    static final String PARAM_SUBJECT_TYPE = "subject_type";
    static final String PARAM_TOKEN_ENDPOINT_AUTHENTICATION_METHOD = "token_endpoint_auth_method";

    private static final Set<String> BUILT_IN_PARAMS = builtInParams(
            PARAM_REDIRECT_URIS,
            PARAM_RESPONSE_TYPES,
            PARAM_GRANT_TYPES,
            PARAM_APPLICATION_TYPE,
            PARAM_SUBJECT_TYPE,
            PARAM_TOKEN_ENDPOINT_AUTHENTICATION_METHOD
    );

    static final String KEY_ADDITIONAL_PARAMETERS = "additionalParameters";
    static final String KEY_CONFIGURATION = "configuration";

    /**
     * Instructs the authorization server to generate a pairwise subject identifier.
     *
     * @see "OpenID Connect Core 1.0, Section 8
     * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.8>"
     */
    public static final String SUBJECT_TYPE_PAIRWISE = "pairwise";

    /**
     * Instructs the authorization server to generate a public subject identifier.
     *
     * @see "OpenID Connect Core 1.0, Section 8
     * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.8>"
     */
    public static final String SUBJECT_TYPE_PUBLIC = "public";

    /**
     * The service's {@link AuthorizationServiceConfiguration configuration}.
     * This configuration specifies how to connect to a particular OAuth provider.
     * Configurations may be
     * {@link AuthorizationServiceConfiguration#AuthorizationServiceConfiguration(Uri,
     * Uri, Uri) created manually}, or
     * {@link AuthorizationServiceConfiguration#fetchFromUrl(Uri,
     * AuthorizationServiceConfiguration.RetrieveConfigurationCallback)
     * via an OpenID Connect Discovery Document}.
     */
    @NonNull
    public final AuthorizationServiceConfiguration configuration;

    /**
     * The client's redirect URI's.
     *
     * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.1.2
     * <https://tools.ietf.org/html/rfc6749#section-3.1.2>"
     */
    @NonNull
    public final List<Uri> redirectUris;

    /**
     * The application type to register, will always be 'native'.
     */
    @NonNull
    public final String applicationType;

    /**
     * The response types to use.
     *
     * @see "OpenID Connect Core 1.0, Section 3
     * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3>"
     */
    @Nullable
    public final List<String> responseTypes;

    /**
     * The grant types to use.
     *
     * @see "OpenID Connect Dynamic Client Registration 1.0, Section 2
     * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.2>"
     */
    @Nullable
    public final List<String> grantTypes;

    /**
     * The subject type to use.
     *
     * @see "OpenID Connect Core 1.0, Section 8 <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.8>"
     */
    @Nullable
    public final String subjectType;

    /**
     * The client authentication method to use at the token endpoint.
     *
     * @see "OpenID Connect Core 1.0, Section 9 <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.9>"
     */
    @Nullable
    public final String tokenEndpointAuthenticationMethod;

    /**
     * Additional parameters to be passed as part of the request.
     */
    @NonNull
    public final Map<String, String> additionalParameters;


    /**
     * Creates instances of {@link RegistrationRequest}.
     */
    public static final class Builder {
        @NonNull
        private AuthorizationServiceConfiguration mConfiguration;
        @NonNull
        private List<Uri> mRedirectUris = new ArrayList<>();

        @Nullable
        private List<String> mResponseTypes;

        @Nullable
        private List<String> mGrantTypes;

        @Nullable
        private String mSubjectType;

        @Nullable
        private String mTokenEndpointAuthenticationMethod;

        @NonNull
        private Map<String, String> mAdditionalParameters = Collections.emptyMap();


        /**
         * Creates a registration request builder with the specified mandatory properties.
         */
        public Builder(
                @NonNull AuthorizationServiceConfiguration configuration,
                @NonNull List<Uri> redirectUri) {
            setConfiguration(configuration);
            setRedirectUriValues(redirectUri);
        }

        /**
         * Specifies the authorization service configuration for the request, which must not
         * be null or empty.
         */
        @NonNull
        public Builder setConfiguration(@NonNull AuthorizationServiceConfiguration configuration) {
            mConfiguration = checkNotNull(configuration);
            return this;
        }

        /**
         * Specifies the redirect URI's.
         *
         * @see <a href="https://tools.ietf.org/html/rfc6749#section-3.1.2"> "The OAuth 2.0
         * Authorization Framework" (RFC 6749), Section 3.1.2</a>
         */
        @NonNull
        public Builder setRedirectUriValues(@NonNull Uri... redirectUriValues) {
            return setRedirectUriValues(Arrays.asList(redirectUriValues));
        }

        /**
         * Specifies the redirect URI's.
         *
         * @see "The OAuth 2.0 Authorization Framework (RFC 6749), Section 3.1.2
         * <https://tools.ietf.org/html/rfc6749#section-3.1.2>"
         */
        @NonNull
        public Builder setRedirectUriValues(@NonNull List<Uri> redirectUriValues) {
            checkCollectionNotEmpty(redirectUriValues, "redirectUriValues cannot be null");
            mRedirectUris = redirectUriValues;
            return this;
        }

        /**
         * Specifies the response types.
         *
         * @see "OpenID Connect Core 1.0, Section 3
         * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3>"
         */
        @NonNull
        public Builder setResponseTypeValues(@Nullable String... responseTypeValues) {
            return setResponseTypeValues(Arrays.asList(responseTypeValues));
        }

        /**
         * Specifies the response types.
         *
         * @see "OpenID Connect Core 1.0, Section X
         * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.X>"
         */
        @NonNull
        public Builder setResponseTypeValues(@Nullable List<String> responseTypeValues) {
            mResponseTypes = responseTypeValues;
            return this;
        }

        /**
         * Specifies the grant types.
         *
         * @see "OpenID Connect Dynamic Client Registration 1.0, Section 2
         * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.2>"
         */
        @NonNull
        public Builder setGrantTypeValues(@Nullable String... grantTypeValues) {
            return setGrantTypeValues(Arrays.asList(grantTypeValues));
        }

        /**
         * Specifies the grant types.
         *
         * @see "OpenID Connect Dynamic Client Registration 1.0, Section 2
         * <https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.2>"
         */
        @NonNull
        public Builder setGrantTypeValues(@Nullable List<String> grantTypeValues) {
            mGrantTypes = grantTypeValues;
            return this;
        }

        /**
         * Specifies the subject types.
         *
         * @see "OpenID Connect Core 1.0, Section 8
         * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.8>"
         */
        @NonNull
        public Builder setSubjectType(@Nullable String subjectType) {
            mSubjectType = subjectType;
            return this;
        }

        /**
         * Specifies the client authentication method to use at the token endpoint.
         *
         * @see "OpenID Connect Core 1.0, Section 9
         * <https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.9>"
         */
        @NonNull
        public Builder setTokenEndpointAuthenticationMethod(
                @Nullable String tokenEndpointAuthenticationMethod) {
            this.mTokenEndpointAuthenticationMethod = tokenEndpointAuthenticationMethod;
            return this;
        }

        /**
         * Specifies additional parameters. Replaces any previously provided set of parameters.
         * Parameter keys and values cannot be null or empty.
         */
        @NonNull
        public Builder setAdditionalParameters(@Nullable Map<String, String> additionalParameters) {
            mAdditionalParameters = checkAdditionalParams(additionalParameters, BUILT_IN_PARAMS);
            return this;
        }

        /**
         * Constructs the registration request. At a minimum, the redirect URI must have been
         * set before calling this method.
         */
        @NonNull
        public RegistrationRequest build() {
            return new RegistrationRequest(
                    mConfiguration,
                    Collections.unmodifiableList(mRedirectUris),
                    mResponseTypes == null
                            ? mResponseTypes : Collections.unmodifiableList(mResponseTypes),
                    mGrantTypes == null ? mGrantTypes : Collections.unmodifiableList(mGrantTypes),
                    mSubjectType,
                    mTokenEndpointAuthenticationMethod,
                    Collections.unmodifiableMap(mAdditionalParameters));
        }
    }

    private RegistrationRequest(
            @NonNull AuthorizationServiceConfiguration configuration,
            @NonNull List<Uri> redirectUris,
            @Nullable List<String> responseTypes,
            @Nullable List<String> grantTypes,
            @Nullable String subjectType,
            @Nullable String tokenEndpointAuthenticationMethod,
            @NonNull Map<String, String> additionalParameters) {
        this.configuration = configuration;
        this.redirectUris = redirectUris;
        this.responseTypes = responseTypes;
        this.grantTypes = grantTypes;
        this.subjectType = subjectType;
        this.tokenEndpointAuthenticationMethod = tokenEndpointAuthenticationMethod;
        this.additionalParameters = additionalParameters;
        this.applicationType = APPLICATION_TYPE_NATIVE;
    }

    /**
     * Converts the registration request to JSON for transmission to an authorization service.
     * For local persistence and transmission, use {@link #jsonSerialize()}.
     */
    @NonNull
    public String toJsonString() {
        JSONObject json = jsonSerializeParams();
        for (Map.Entry<String, String> param : additionalParameters.entrySet()) {
            JsonUtil.put(json, param.getKey(), param.getValue());
        }
        return json.toString();
    }

    /**
     * Produces a JSON representation of the registration request for persistent storage or
     * local transmission (e.g. between activities).
     */
    @NonNull
    public JSONObject jsonSerialize() {
        JSONObject json = jsonSerializeParams();
        JsonUtil.put(json, KEY_CONFIGURATION, configuration.toJson());
        JsonUtil.put(json, KEY_ADDITIONAL_PARAMETERS,
                JsonUtil.mapToJsonObject(additionalParameters));
        return json;
    }

    /**
     * Produces a JSON string representation of the registration request for persistent storage or
     * local transmission (e.g. between activities). This method is just a convenience wrapper
     * for {@link #jsonSerialize()}, converting the JSON object to its string form.
     */
    @NonNull
    public String jsonSerializeString() {
        return jsonSerialize().toString();
    }

    private JSONObject jsonSerializeParams() {
        JSONObject json = new JSONObject();
        JsonUtil.put(json, PARAM_REDIRECT_URIS, JsonUtil.toJsonArray(redirectUris));
        JsonUtil.put(json, PARAM_APPLICATION_TYPE, applicationType);

        if (responseTypes != null) {
            JsonUtil.put(json, PARAM_RESPONSE_TYPES, JsonUtil.toJsonArray(responseTypes));
        }
        if (grantTypes != null) {
            JsonUtil.put(json, PARAM_GRANT_TYPES, JsonUtil.toJsonArray(grantTypes));
        }
        JsonUtil.putIfNotNull(json, PARAM_SUBJECT_TYPE, subjectType);
        JsonUtil.putIfNotNull(json, PARAM_TOKEN_ENDPOINT_AUTHENTICATION_METHOD,
                tokenEndpointAuthenticationMethod);
        return json;
    }

    /**
     * Reads a registration request from a JSON string representation produced by
     * {@link #jsonSerialize()}.
     * @throws JSONException if the provided JSON does not match the expected structure.
     */
    public static RegistrationRequest jsonDeserialize(@NonNull JSONObject json)
            throws JSONException {
        checkNotNull(json, "json must not be null");
        List<Uri> redirectUris = JsonUtil.getUriList(json, PARAM_REDIRECT_URIS);

        Builder builder = new RegistrationRequest.Builder(
                AuthorizationServiceConfiguration.fromJson(json.getJSONObject(KEY_CONFIGURATION)),
                redirectUris)
                .setSubjectType(JsonUtil.getStringIfDefined(json, PARAM_SUBJECT_TYPE))
                .setResponseTypeValues(JsonUtil.getStringListIfDefined(json, PARAM_RESPONSE_TYPES))
                .setGrantTypeValues(JsonUtil.getStringListIfDefined(json, PARAM_GRANT_TYPES))
                .setAdditionalParameters(JsonUtil.getStringMap(json, KEY_ADDITIONAL_PARAMETERS));

        return builder.build();
    }

    /**
     * Reads a registration request from a JSON string representation produced by
     * {@link #jsonSerializeString()}. This method is just a convenience wrapper for
     * {@link #jsonDeserialize(JSONObject)}, converting the JSON string to its JSON object form.
     * @throws JSONException if the provided JSON does not match the expected structure.
     */
    public static RegistrationRequest jsonDeserialize(@NonNull String jsonStr)
            throws JSONException {
        checkNotEmpty(jsonStr, "jsonStr must not be empty or null");
        return jsonDeserialize(new JSONObject(jsonStr));
    }
}