package cz.muni.ics.oidc.server.filters.impl;

import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oidc.exceptions.ConfigurationException;
import cz.muni.ics.oidc.models.Facility;
import cz.muni.ics.oidc.models.PerunAttributeValue;
import cz.muni.ics.oidc.models.PerunUser;
import cz.muni.ics.oidc.saml.ExtendedOAuth2Exception;
import cz.muni.ics.oidc.server.adapters.PerunAdapter;
import cz.muni.ics.oidc.server.configurations.FacilityAttrsConfig;
import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
import cz.muni.ics.oidc.server.filters.AuthProcFilter;
import cz.muni.ics.oidc.server.filters.AuthProcFilterCommonVars;
import cz.muni.ics.oidc.server.filters.AuthProcFilterInitContext;
import cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

import static cz.muni.ics.oidc.RedirectUtils.redirectExternal;
import static cz.muni.ics.oidc.RedirectUtils.redirectInternal;
import static cz.muni.ics.oidc.server.filters.FiltersUtils.validateUrl;
import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.PROMPT;
import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.PROMPT_NONE;
import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.REDIRECT_URI;
import static cz.muni.ics.openid.connect.request.ConnectRequestParameters.STATE;

/**
 * Authorization filter. Decides if user can access the service based on his/hers
 * membership in the groups assigned to the Perun facility resources. Facility represents
 * client in this context.
 *
 * Configuration:
 * - based on the configuration of bean "facilityAttrsConfig"
 * Configuration of filter (replace [name] part with the name defined for the filter):
 * <ul>
 *     <li><b>filter.[name].accessControlDisabledAttr</b> - resource attribute which triggers if resource assigned
 *     groups should not be used for controlling access. When not specified, all groups will be used.</li>
 * </ul>
 * @see FacilityAttrsConfig
 * @see cz.muni.ics.oidc.server.filters.AuthProcFilter (basic configuration options)
 *
 * @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
 */
@Slf4j
public class PerunAuthorizationFilter extends AuthProcFilter {

	protected static final String ACCESS_CONTROL_DISABLED_ATTR = "accessControlDisabledAttr";

	private final PerunAdapter perunAdapter;
	private final FacilityAttrsConfig facilityAttrsConfig;
	private final PerunOidcConfig config;
	private final String accessControlDisabledAttr;

	public PerunAuthorizationFilter(AuthProcFilterInitContext ctx) throws ConfigurationException {
		super(ctx);
		this.perunAdapter = ctx.getPerunAdapterBean();
		this.config = ctx.getPerunOidcConfigBean();
		this.facilityAttrsConfig = ctx.getBeanUtil().getBean(FacilityAttrsConfig.class);
		this.accessControlDisabledAttr = ctx.getProperty(ACCESS_CONTROL_DISABLED_ATTR, null);
	}

	@Override
	protected boolean process(HttpServletRequest req, HttpServletResponse res, AuthProcFilterCommonVars params) {
		Facility facility = params.getFacility();
		if (facility == null || facility.getId() == null) {
			log.debug("{} - skip filter execution: no facility provided", getFilterName());
			return true;
		}

		PerunUser user = params.getUser();
		if (user == null || user.getId() == null) {
			log.debug("{} - skip filter execution: no user provided", getFilterName());
			return true;
		}

		return this.decideAccess(req, res, facility, user, params.getClient());
	}

	@Override
	protected boolean oncePerSession() {
		return false;
	}

	private boolean decideAccess(HttpServletRequest req,
								 HttpServletResponse res,
								 Facility facility,
								 PerunUser user,
								 ClientDetailsEntity client)
	{
		Map<String, PerunAttributeValue> facilityAttributes = perunAdapter.getFacilityAttributeValues(
				facility, facilityAttrsConfig.getMembershipAttrNames());

		if (!facilityAttributes.get(facilityAttrsConfig.getCheckGroupMembershipAttr()).valueAsBoolean()) {
			log.debug("{} - skip filter execution: membership check not requested", getFilterName());
			return true;
		}

		if (perunAdapter.canUserAccessBasedOnMembership(facility, user.getId(), accessControlDisabledAttr)) {
			log.info("{} - user allowed to access the service", getFilterName());
			return true;
		} else {
			log.info("{} - user is not allowed to access the service due to membership check", getFilterName());
			if (PROMPT_NONE.equals(req.getParameter(PROMPT))) {
				throw new ExtendedOAuth2Exception("interaction_required",
						"User needs to make further steps to be able to access the service.",
						req.getParameter(REDIRECT_URI),
						req.getParameter(STATE));
			}

			redirectUserCannotAccess(req, res, facility, user, client);
			return false;
		}
	}

	private void redirectUserCannotAccess(HttpServletRequest req,
										  HttpServletResponse res,
										  Facility facility,
										  PerunUser user,
										  ClientDetailsEntity client)
	{
		Map<String, PerunAttributeValue> facilityAttributes = perunAdapter.getFacilityAttributeValues(
			facility, facilityAttrsConfig.getMembershipAttrNames()
		);

		if (allowsRegistration(facilityAttributes)) {
			log.debug("{} - service '{}' allows registration", getFilterName(), client.getClientName());
			PerunAttributeValue customRegUrlAttr = facilityAttributes.get(facilityAttrsConfig.getRegistrationURLAttr());
			if (customRegUrlAttr != null && customRegUrlAttr.valueAsString() != null) {
				String customRegUrl = facilityAttributes.get(facilityAttrsConfig.getRegistrationURLAttr())
						.valueAsString();
				log.debug("{} - service '{}' has a custom registration URL", getFilterName(), customRegUrl);
				customRegUrl = validateUrl(customRegUrl);
				if (customRegUrl != null) {
					log.debug(
						"{} - service '{}' registration URL '{}' passed validation. Redirecting user there.",
						getFilterName(), client.getClientName(), customRegUrl
					);
					redirectExternal(res, customRegUrl);
					return;
				} else {
					log.debug(
						"{} - service '{}' registration URL did not pass validation. Redirecting to unauthorized.",
						getFilterName(), client.getClientName()
					);
				}
			} else {
				boolean groupsForRegistrationExist = perunAdapter.getAdapterRpc().groupWhereCanRegisterExists(facility);
				boolean dynamicRegistrationAllowed =
						facilityAttributes.get(facilityAttrsConfig.getDynamicRegistrationAttr()).valueAsBoolean();
				if (groupsForRegistrationExist && dynamicRegistrationAllowed) {
					log.debug(
						"{} - service '{}' allows dynamic registration and some registration units exist. Redirecting user to the form to select VO (and GROUP)",
						getFilterName(), client.getClientName()
					);
					redirectChoosePerunEntities(req, res, user, client, facility);
					return;
				} else {
					log.debug(
							"{} - service '{}' has no registration option for user - units exist: {}, dynamic reg. allowed: {}",
							getFilterName(), client.getClientName(), groupsForRegistrationExist, dynamicRegistrationAllowed
					);
				}
			}
		}

		// cannot register, redirect to unauthorized
		log.debug("User cannot register to obtain access (facility did not enable this), redirecting user '{}' to unauthorized page", user);
		redirectUnauthorized(req, res, user, client);
	}

	private boolean allowsRegistration(Map<String, PerunAttributeValue> facilityAttributes) {
		return facilityAttributes.getOrDefault(facilityAttrsConfig.getAllowRegistrationAttr(), null) != null
			&& facilityAttributes.get(facilityAttrsConfig.getAllowRegistrationAttr()).valueAsBoolean();
	}

	private void redirectChoosePerunEntities(HttpServletRequest req,
											 HttpServletResponse res,
											 PerunUser user,
											 ClientDetailsEntity client,
											 Facility facility)
	{
		log.trace(
			"{} - Redirecting user '{}' to the internal form to choose VO (and possibly GROUP)",
			getFilterName(), user
		);
		Map<String, Object> sessAttributes = new HashMap<>();
		sessAttributes.put(PerunUnauthorizedController.SESS_ATTR_USER, user);
		sessAttributes.put(PerunUnauthorizedController.SESS_ATTR_CLIENT, client);
		sessAttributes.put(PerunUnauthorizedController.SESS_ATTR_FACILITY, facility);
		redirectInternal(req, res, config, PerunUnauthorizedController.UNAUTHORIZED_REGISTER_NOTIFY_ACTION_REQUIRED_MAPPING, sessAttributes);
	}

	private void redirectUnauthorized(HttpServletRequest req,
									HttpServletResponse res,
									PerunUser user,
									ClientDetailsEntity client)
	{
		log.debug("{} - Redirecting user '{}' to the internal page informing about unauthorized access",
				getFilterName(), user);
		Map<String, Object> sessAttributes = new HashMap<>();
		sessAttributes.put(PerunUnauthorizedController.SESS_ATTR_CLIENT, client);
		redirectInternal(req, res, config, PerunUnauthorizedController.UNAUTHORIZED_AUTHORIZATION_MAPPING, sessAttributes);
	}

}
