package org.gluu.oxtrust.auth.uma;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Inject;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;

import org.gluu.oxauth.client.uma.wrapper.UmaClient;
import org.gluu.oxauth.model.uma.UmaMetadata;
import org.gluu.oxauth.model.uma.wrapper.Token;
import org.gluu.oxtrust.exception.UmaProtectionException;
import org.gluu.oxtrust.service.EncryptionService;
import org.gluu.oxtrust.service.filter.ProtectedApi;
import org.gluu.util.Pair;
import org.gluu.util.StringHelper;
import org.gluu.util.security.StringEncrypter.EncryptionException;
import org.slf4j.Logger;

/**
 * Provide base methods to simplify work with UMA Rest services
 * 
 * @author Yuriy Movchan Date: 12/06/2016
 */
public abstract class BaseUmaProtectionService implements Serializable {

	private static final long serialVersionUID = -1147131971095468865L;

	@Inject
	private Logger log;

	@Inject
	private EncryptionService encryptionService;

	@Inject
	private UmaMetadata umaMetadata;

	@Inject
	protected UmaPermissionService umaPermissionService;

	private Token umaPat;
	private long umaPatAccessTokenExpiration = 0l; // When the "accessToken" will expire;

	private final ReentrantLock lock = new ReentrantLock();

	public Token getPatToken() throws UmaProtectionException {
		if (isValidPatToken(this.umaPat, this.umaPatAccessTokenExpiration)) {
			return this.umaPat;
		}

		lock.lock();
		try {
			if (isValidPatToken(this.umaPat, this.umaPatAccessTokenExpiration)) {
				return this.umaPat;
			}

			retrievePatToken();
		} finally {
			lock.unlock();
		}

		return this.umaPat;
	}

	protected boolean isEnabledUmaAuthentication() {
		return (umaMetadata != null) && isExistPatToken();
	}

	public boolean isExistPatToken() {
		try {
			return getPatToken() != null;
		} catch (UmaProtectionException ex) {
			log.error("Failed to check UMA PAT token status", ex);
		}

		return false;
	}

	public String getIssuer() {
		if (umaMetadata == null) {
			return "";
		}
		return umaMetadata.getIssuer();
	}

	private void retrievePatToken() throws UmaProtectionException {
		this.umaPat = null;
		if (umaMetadata == null) {
			return;
		}

		String umaClientKeyStoreFile = getClientKeyStoreFile();
		String umaClientKeyStorePassword = getClientKeyStorePassword();
		if (StringHelper.isEmpty(umaClientKeyStoreFile) || StringHelper.isEmpty(umaClientKeyStorePassword)) {
			throw new UmaProtectionException("UMA JKS keystore path or password is empty");
		}

		if (umaClientKeyStorePassword != null) {
			try {
				umaClientKeyStorePassword = encryptionService.decrypt(umaClientKeyStorePassword);
			} catch (EncryptionException ex) {
				log.error("Failed to decrypt UmaClientKeyStorePassword password", ex);
			}
		}

		try {
			this.umaPat = UmaClient.requestPat(umaMetadata.getTokenEndpoint(), umaClientKeyStoreFile,
					umaClientKeyStorePassword, getClientId(), getClientKeyId());
			if (this.umaPat == null) {
				this.umaPatAccessTokenExpiration = 0l;
			} else {
				this.umaPatAccessTokenExpiration = computeAccessTokenExpirationTime(this.umaPat.getExpiresIn());
			}
		} catch (Exception ex) {
			throw new UmaProtectionException("Failed to obtain valid UMA PAT token", ex);
		}

		if ((this.umaPat == null) || (this.umaPat.getAccessToken() == null)) {
			throw new UmaProtectionException("Failed to obtain valid UMA PAT token");
		}
	}

	protected long computeAccessTokenExpirationTime(Integer expiresIn) {
		// Compute "accessToken" expiration timestamp
		Calendar calendar = Calendar.getInstance();
		if (expiresIn != null) {
			calendar.add(Calendar.SECOND, expiresIn);
			calendar.add(Calendar.SECOND, -10); // Subtract 10 seconds to avoid expirations during executing request
		}

		return calendar.getTimeInMillis();
	}

	private boolean isValidPatToken(Token validatePatToken, long validatePatTokenExpiration) {
		final long now = System.currentTimeMillis();

		// Get new access token only if is the previous one is missing or expired
		return !((validatePatToken == null) || (validatePatToken.getAccessToken() == null)
				|| (validatePatTokenExpiration <= now));
	}

	protected Response getErrorResponse(Response.Status status, String detail) {
		return Response.status(status).entity(detail).build();
	}

	Response processUmaAuthorization(String authorization, ResourceInfo resourceInfo) throws Exception {
		List<String> scopes = getRequestedScopes(resourceInfo);
		Token patToken = null;
		try {
			patToken = getPatToken();
		} catch (UmaProtectionException ex) {
			return getErrorResponse(Response.Status.INTERNAL_SERVER_ERROR, "Failed to obtain PAT token");
		}

		Pair<Boolean, Response> rptTokenValidationResult;
		if (!scopes.isEmpty()) {
			rptTokenValidationResult = umaPermissionService.validateRptToken(patToken, authorization,
					getUmaResourceId(), scopes);
		} else {
			rptTokenValidationResult = umaPermissionService.validateRptToken(patToken, authorization,
					getUmaResourceId(), getUmaScope());
		}

		if (rptTokenValidationResult.getFirst()) {
			if (rptTokenValidationResult.getSecond() != null) {
				return rptTokenValidationResult.getSecond();
			}
		} else {
			return getErrorResponse(Response.Status.UNAUTHORIZED, "Invalid GAT/RPT token");
		}
		return null;

	}

	public List<String> getRequestedScopes(ResourceInfo resourceInfo) {
		Class<?> resourceClass = resourceInfo.getResourceClass();
		ProtectedApi typeAnnotation = resourceClass.getAnnotation(ProtectedApi.class);
		List<String> scopes = new ArrayList<String>();
		if (typeAnnotation == null) {
			addMethodScopes(resourceInfo, scopes);
		} else {
			scopes.addAll(Stream.of(typeAnnotation.scopes()).collect(Collectors.toList()));
			addMethodScopes(resourceInfo, scopes);
		}
		return scopes;
	}

	private void addMethodScopes(ResourceInfo resourceInfo, List<String> scopes) {
		Method resourceMethod = resourceInfo.getResourceMethod();
		ProtectedApi methodAnnotation = resourceMethod.getAnnotation(ProtectedApi.class);
		if (methodAnnotation != null) {
			scopes.addAll(Stream.of(methodAnnotation.scopes()).collect(Collectors.toList()));
		}
	}

	protected abstract String getClientId();

	protected abstract String getClientKeyStorePassword();

	protected abstract String getClientKeyStoreFile();

	protected abstract String getClientKeyId();

	public abstract String getUmaResourceId();

	public abstract String getUmaScope();

	public abstract boolean isEnabled();

	public abstract Response processAuthorization(HttpHeaders headers, ResourceInfo resourceInfo);

}