/*
 * 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;

import org.keycloak.TokenVerifier;
import org.keycloak.TokenVerifier.Predicate;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.*;
import org.keycloak.models.*;
import org.keycloak.services.Urls;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
import java.util.UUID;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;

/**
 * Representation of a token that represents a time-limited reset credentials action.
 * <p>
 * This implementation handles signature.
 *
 * @author hmlnarik
 */
public class ResetCredentialsActionToken extends DefaultActionToken {

    private static final Logger LOG = Logger.getLogger(ResetCredentialsActionToken.class);

    private static final String RESET_CREDENTIALS_ACTION = "reset-credentials";
    public static final String NOTE_CLIENT_SESSION_ID = "clientSessionId";
    private static final String JSON_FIELD_CLIENT_SESSION_ID = "csid";
    private static final String JSON_FIELD_LAST_CHANGE_PASSWORD_TIMESTAMP = "lcpt";

    @JsonIgnore
    private ClientSessionModel clientSession;

    @JsonProperty(value = JSON_FIELD_LAST_CHANGE_PASSWORD_TIMESTAMP)
    private Long lastChangedPasswordTimestamp;

    public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, Long lastChangedPasswordTimestamp, String clientSessionId) {
        super(userId, RESET_CREDENTIALS_ACTION, absoluteExpirationInSecs, actionVerificationNonce);
        setNote(NOTE_CLIENT_SESSION_ID, clientSessionId);
        this.lastChangedPasswordTimestamp = lastChangedPasswordTimestamp;
    }

    public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, Long lastChangedPasswordTimestamp, ClientSessionModel clientSession) {
        this(userId, absoluteExpirationInSecs, actionVerificationNonce, lastChangedPasswordTimestamp, clientSession == null ? null : clientSession.getId());
        this.clientSession = clientSession;
    }

    private ResetCredentialsActionToken() {
        super(null, null, -1, null);
    }

    public ClientSessionModel getClientSession() {
        return this.clientSession;
    }

    public void setClientSession(ClientSessionModel clientSession) {
        this.clientSession = clientSession;
        setClientSessionId(clientSession == null ? null : clientSession.getId());
    }

    @JsonProperty(value = JSON_FIELD_CLIENT_SESSION_ID)
    public String getClientSessionId() {
        return getNote(NOTE_CLIENT_SESSION_ID);
    }

    public void setClientSessionId(String clientSessionId) {
        setNote(NOTE_CLIENT_SESSION_ID, clientSessionId);
    }

    public Long getLastChangedPasswordTimestamp() {
        return lastChangedPasswordTimestamp;
    }

    public void setLastChangedPasswordTimestamp(Long lastChangedPasswordTimestamp) {
        this.lastChangedPasswordTimestamp = lastChangedPasswordTimestamp;
    }

    @Override
    @JsonIgnore
    public Map<String, String> getNotes() {
        Map<String, String> res = super.getNotes();
        if (this.clientSession != null) {
            res.put(NOTE_CLIENT_SESSION_ID, getNote(NOTE_CLIENT_SESSION_ID));
        }
        return res;
    }

    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());
    }

    /**
     * Returns a {@code DefaultActionToken} instance decoded from the given string. If decoding fails, returns {@code null}
     *
     * @param session
     * @param actionTokenString
     * @return
     */
    public static ResetCredentialsActionToken deserialize(KeycloakSession session, RealmModel realm, UriInfo uri, String token,
      Predicate<? super ResetCredentialsActionToken>... checks) throws VerificationException {
        return TokenVerifier.create(token, ResetCredentialsActionToken.class)
          .secretKey(session.keys().getActiveHmacKey(realm).getSecretKey())
          .realmUrl(getIssuer(realm, uri))
          .tokenType(RESET_CREDENTIALS_ACTION)

          .checkActive(false)   // TODO: If this line is omitted, the following tests in ResetPasswordTest fail: resetPasswordExpiredCodeShort, resetPasswordExpiredCode

          .check(ACTION_TOKEN_BASIC_CHECKS)
          .check(checks)
          .verify()
          .getToken()
        ;
    }
}
