/*
 * Copyright 2017 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 * 
 * 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.authentication.actiontoken;

import org.keycloak.TokenVerifier.Predicate;
import org.keycloak.common.VerificationException;

import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.*;
import org.keycloak.services.Urls;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.*;
import javax.ws.rs.core.UriInfo;

/**
 * Part of action token that is intended to be used e.g. in link sent in password-reset email.
 * The token encapsulates user, expected action and its time of expiry.
 *
 * @author hmlnarik
 */
public class DefaultActionToken extends DefaultActionTokenKey implements ActionTokenValueModel {

    public static final String JSON_FIELD_AUTHENTICATION_SESSION_ID = "asid";

    public static final Predicate<DefaultActionTokenKey> ACTION_TOKEN_BASIC_CHECKS = t -> {
        if (t.getActionVerificationNonce() == null) {
            throw new VerificationException("Nonce not present.");
        }

        return true;
    };

    /**
     * Single-use random value used for verification whether the relevant action is allowed.
     */
    public DefaultActionToken() {
        super(null, null, 0, null);
    }

    /**
     *
     * @param userId User ID
     * @param actionId Action ID
     * @param absoluteExpirationInSecs Absolute expiration time in seconds in timezone of Keycloak.
     * @param actionVerificationNonce
     */
    protected DefaultActionToken(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce) {
        super(userId, actionId, absoluteExpirationInSecs, actionVerificationNonce);
    }

    /**
     *
     * @param userId User ID
     * @param actionId Action ID
     * @param absoluteExpirationInSecs Absolute expiration time in seconds in timezone of Keycloak.
     * @param actionVerificationNonce
     */
    protected DefaultActionToken(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce, String compoundAuthenticationSessionId) {
        super(userId, actionId, absoluteExpirationInSecs, actionVerificationNonce);
        setCompoundAuthenticationSessionId(compoundAuthenticationSessionId);
    }

    @JsonProperty(value = JSON_FIELD_AUTHENTICATION_SESSION_ID)
    public String getCompoundAuthenticationSessionId() {
        return (String) getOtherClaims().get(JSON_FIELD_AUTHENTICATION_SESSION_ID);
    }

    @JsonProperty(value = JSON_FIELD_AUTHENTICATION_SESSION_ID)
    public final void setCompoundAuthenticationSessionId(String authenticationSessionId) {
        setOtherClaims(JSON_FIELD_AUTHENTICATION_SESSION_ID, authenticationSessionId);
    }

    @JsonIgnore
    @Override
    public Map<String, String> getNotes() {
        Map<String, String> res = new HashMap<>();
        if (getCompoundAuthenticationSessionId() != null) {
            res.put(JSON_FIELD_AUTHENTICATION_SESSION_ID, getCompoundAuthenticationSessionId());
        }
        return res;
    }

    @Override
    public String getNote(String name) {
        Object res = getOtherClaims().get(name);
        return res instanceof String ? (String) res : null;
    }

    /**
     * Sets value of the given note
     * @return original value (or {@code null} when no value was present)
     */
    public final String setNote(String name, String value) {
        Object res = value == null
          ? getOtherClaims().remove(name)
          : getOtherClaims().put(name, value);
        return res instanceof String ? (String) res : null;
    }

    /**
     * Removes given note, and returns original value (or {@code null} when no value was present)
     * @return see description
     */
    public final String removeNote(String name) {
        Object res = getOtherClaims().remove(name);
        return res instanceof String ? (String) res : null;
    }

    /**
     * Updates the following fields and serializes this token into a signed JWT. The list of updated fields follows:
     * <ul>
     * <li>{@code id}: random nonce</li>
     * <li>{@code issuedAt}: Current time</li>
     * <li>{@code issuer}: URI of the given realm</li>
     * <li>{@code audience}: URI of the given realm (same as issuer)</li>
     * </ul>
     *
     * @param session
     * @param realm
     * @param uri
     * @return
     */
    public String serialize(KeycloakSession session, RealmModel realm, UriInfo uri) {
        String issuerUri = getIssuer(realm, uri);
        KeyManager.ActiveHmacKey keys = session.keys().getActiveHmacKey(realm);

        this
          .issuedAt(Time.currentTime())
          .id(getActionVerificationNonce().toString())
          .issuer(issuerUri)
          .audience(issuerUri);

        return new JWSBuilder()
          .kid(keys.getKid())
          .jsonContent(this)
          .hmac512(keys.getSecretKey());
    }

    private static String getIssuer(RealmModel realm, UriInfo uri) {
        return Urls.realmIssuer(uri.getBaseUri(), realm.getName());
    }

}
