/*
 * Copyright 2013-2014 the original author or authors.
 *
 * 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.maxkey.authz.oauth2.provider.token.store;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;

import org.maxkey.authz.oauth2.common.DefaultExpiringOAuth2RefreshToken;
import org.maxkey.authz.oauth2.common.ExpiringOAuth2RefreshToken;
import org.maxkey.authz.oauth2.common.OAuth2AccessToken;
import org.maxkey.authz.oauth2.common.OAuth2RefreshToken;
import org.maxkey.authz.oauth2.provider.OAuth2Authentication;
import org.maxkey.authz.oauth2.provider.approval.Approval;
import org.maxkey.authz.oauth2.provider.approval.ApprovalStore;
import org.maxkey.authz.oauth2.provider.approval.Approval.ApprovalStatus;
import org.maxkey.authz.oauth2.provider.token.TokenStore;
import org.springframework.security.core.Authentication;

/**
 * A {@link TokenStore} implementation that just reads data from the tokens themselves. Not really a store since it
 * never persists anything, and methods like {@link #getAccessToken(OAuth2Authentication)} always return null. But
 * nevertheless a useful tool since it translates access tokens to and from authentications. Use this wherever a
 * {@link TokenStore} is needed, but remember to use the same {@link JwtAccessTokenConverter} instance (or one with the same
 * verifier) as was used when the tokens were minted.
 * 
 * @author Dave Syer
 *
 */
public class JwtTokenStore implements TokenStore {

	private JwtAccessTokenConverter jwtTokenEnhancer;

	private ApprovalStore approvalStore;

	/**
	 * Create a JwtTokenStore with this token enhancer (should be shared with the DefaultTokenServices if used).
	 * 
	 * @param jwtTokenEnhancer
	 */
	public JwtTokenStore(JwtAccessTokenConverter jwtTokenEnhancer) {
		this.jwtTokenEnhancer = jwtTokenEnhancer;
	}

	/**
	 * ApprovalStore to be used to validate and restrict refresh tokens.
	 * 
	 * @param approvalStore the approvalStore to set
	 */
	public void setApprovalStore(ApprovalStore approvalStore) {
		this.approvalStore = approvalStore;
	}

	@Override
	public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
		return readAuthentication(token.getValue());
	}

	@Override
	public OAuth2Authentication readAuthentication(String token) {
		return jwtTokenEnhancer.extractAuthentication(jwtTokenEnhancer.decode(token));
	}

	@Override
	public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
	}

	@Override
	public OAuth2AccessToken readAccessToken(String tokenValue) {
		return jwtTokenEnhancer.extractAccessToken(tokenValue, jwtTokenEnhancer.decode(tokenValue));
	}

	@Override
	public void removeAccessToken(OAuth2AccessToken token) {
		if (approvalStore != null) {
			OAuth2Authentication auth = readAuthentication(token);
			String clientId = auth.getOAuth2Request().getClientId();
			Authentication user = auth.getUserAuthentication();
			if (user != null) {
				Collection<Approval> approvals = new ArrayList<Approval>();
				for (String scope : auth.getOAuth2Request().getScope()) {
					approvals.add(new Approval(user.getName(), clientId, scope, new Date(), ApprovalStatus.APPROVED));
				}
				approvalStore.revokeApprovals(approvals);
			}
		}
	}

	@Override
	public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
	}

	@Override
	public OAuth2RefreshToken readRefreshToken(String tokenValue) {
		OAuth2AccessToken encodedRefreshToken = readAccessToken(tokenValue);
		ExpiringOAuth2RefreshToken refreshToken = new DefaultExpiringOAuth2RefreshToken(encodedRefreshToken.getValue(),
				encodedRefreshToken.getExpiration());
		if (approvalStore != null) {
			OAuth2Authentication authentication = readAuthentication(tokenValue);
			if (authentication.getUserAuthentication() != null) {
				String userId = authentication.getUserAuthentication().getName();
				String clientId = authentication.getOAuth2Request().getClientId();
				Collection<Approval> approvals = approvalStore.getApprovals(userId, clientId);
				Collection<String> approvedScopes = new HashSet<String>();
				for (Approval approval : approvals) {
					if (approval.isApproved()) {
						approvedScopes.add(approval.getScope());
					}
				}
				if (!approvedScopes.containsAll(authentication.getOAuth2Request().getScope())) {
					return null;
				}
			}
		}
		return refreshToken;
	}

	@Override
	public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
		return readAuthentication(token.getValue());
	}

	@Override
	public void removeRefreshToken(OAuth2RefreshToken token) {
		remove(token.getValue());
	}

	@Override
	public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
		removeRefreshToken(refreshToken);
	}

	@Override
	public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
		// We don't want to accidentally issue a token, and we have no way to reconstruct the refresh token
		return null;
	}

	@Override
	public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
		return Collections.emptySet();
	}

	@Override
	public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
		return Collections.emptySet();
	}

	public void setTokenEnhancer(JwtAccessTokenConverter tokenEnhancer) {
		this.jwtTokenEnhancer = tokenEnhancer;
	}

	private void remove(String token) {
		if (approvalStore != null) {
			OAuth2Authentication auth = readAuthentication(token);
			String clientId = auth.getOAuth2Request().getClientId();
			Authentication user = auth.getUserAuthentication();
			if (user != null) {
				Collection<Approval> approvals = new ArrayList<Approval>();
				for (String scope : auth.getOAuth2Request().getScope()) {
					approvals.add(new Approval(user.getName(), clientId, scope, new Date(), ApprovalStatus.APPROVED));
				}
				approvalStore.revokeApprovals(approvals);
			}
		}
	}
}
