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

import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oidc.RedirectUtils;
import cz.muni.ics.oidc.exceptions.ConfigurationException;
import cz.muni.ics.oidc.models.Facility;
import cz.muni.ics.oidc.models.Group;
import cz.muni.ics.oidc.models.PerunAttributeValue;
import cz.muni.ics.oidc.models.PerunUser;
import cz.muni.ics.oidc.models.Vo;
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.server.filters.FiltersUtils;
import cz.muni.ics.oidc.web.controllers.PerunUnauthorizedController;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

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

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;


/**
 * Filter that decides if the user is valid or not. It checks for membership in specified groups and VOs. In addition,
 * if service identifier is present and can be obtained, it also checks membership in specified groups and VOs based on
 * the environment the service is in.
 *
 * Configuration (replace [name] part with the name defined for the filter):
 * @see cz.muni.ics.oidc.server.filters.AuthProcFilter (basic configuration options)
 * <ul>
 *     <li><b>filter.[name].allEnvGroups</b> - Comma separated list of GROUP IDs the user must be always member of</li>
 *     <li><b>filter.[name].allEnvGroups</b> - Comma separated list of VO IDs the user must be always member of</li>
 *     <li><b>filter.[name].testEnvGroups</b> - Comma separated list of GROUP IDs the user must be member of if service
 *  *         is in the test environment</li>
 *     <li><b>filter.[name].testEnvVos</b> - Comma separated list of VO IDs the user must be member of if service
 *  *         is in the test environment</li>
 *     <li><b>filter.[name].prodEnvGroups</b> - Comma separated list of GROUP IDs the user must be member of if service
 *  *         is in the production environment</li>
 *     <li><b>filter.[name].prodEnvVos</b> - Comma separated list of VO IDs the user must be member of if service
 *         is in the production environment</li>
 * </ul>
 *
 * @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
 */
@SuppressWarnings("SqlResolve")
@Slf4j
public class ValidUserFilter extends AuthProcFilter {

	/* CONFIGURATION OPTIONS */
	private static final String ALL_ENV_GROUPS = "allEnvGroups";
	private static final String ALL_ENV_VOS = "allEnvVos";
	private static final String TEST_ENV_GROUPS = "testEnvGroups";
	private static final String TEST_ENV_VOS = "testEnvVos";
	private static final String PROD_ENV_GROUPS = "prodEnvGroups";
	private static final String PROD_ENV_VOS = "prodEnvVos";
	private final Set<Long> allEnvGroups;
	private final Set<Long> allEnvVos;
	private final Set<Long> testEnvGroups;
	private final Set<Long> testEnvVos;
	private final Set<Long> prodEnvGroups;
	private final Set<Long> prodEnvVos;
	/* END OF CONFIGURATION OPTIONS */

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

	public ValidUserFilter(AuthProcFilterInitContext ctx) throws ConfigurationException {
		super(ctx);
		this.perunAdapter = ctx.getPerunAdapterBean();
		this.config = ctx.getPerunOidcConfigBean();
		this.facilityAttrsConfig = ctx.getBeanUtil().getBean(FacilityAttrsConfig.class);

		this.allEnvGroups = getIdsFromParam(ctx, ALL_ENV_GROUPS);
		this.allEnvVos = getIdsFromParam(ctx, ALL_ENV_VOS);
		this.testEnvGroups = getIdsFromParam(ctx, TEST_ENV_GROUPS);
		this.testEnvVos = getIdsFromParam(ctx, TEST_ENV_VOS);
		this.prodEnvGroups = getIdsFromParam(ctx, PROD_ENV_GROUPS);
		this.prodEnvVos = getIdsFromParam(ctx, PROD_ENV_VOS);

		if (allSetsEmpty()) {
			throw new ConfigurationException("All sets are configured to be empty");
		}
	}

	private boolean allSetsEmpty() {
		return allEnvVos.isEmpty() && allEnvGroups.isEmpty()
				&& prodEnvVos.isEmpty() && prodEnvGroups.isEmpty()
				&& testEnvVos.isEmpty() && testEnvGroups.isEmpty();
	}

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

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

		PerunAttributeValue isTestSpAttrValue = perunAdapter.getFacilityAttributeValue(
				facility.getId(), facilityAttrsConfig.getTestSpAttr());
		boolean testService = false;
		if (isTestSpAttrValue != null) {
			testService = isTestSpAttrValue.valueAsBoolean();
		}

		Set<Long> vos = new HashSet<>(allEnvVos);
		Set<Long> groups = new HashSet<>(allEnvGroups);
		if (testService) {
			vos.addAll(testEnvVos);
			groups.addAll(testEnvGroups);
		} else {
			vos.addAll(prodEnvVos);
			groups.addAll(prodEnvGroups);
		}

		Set<String> invalidUnits = getVosAndGroupsNamesWhereUserInvalid(user, vos, groups);
		if (!invalidUnits.isEmpty()) {
			respondToPrompt(req);
			redirectCannotAccess(req, res, invalidUnits, params.getClient());
			return false;
		}

		log.info("{} - user satisfies the membership criteria", getFilterName());
		return true;
	}

	private void redirectCannotAccess(HttpServletRequest req,
									  HttpServletResponse res,
									  Set<String> invalidUnits,
									  ClientDetailsEntity client)
	{
		Map<Vo, Set<Group>> registrationUnits = perunAdapter.getAdapterRpc().getRegistrationUnits(invalidUnits);

		Map<String, Object> sessParams = new HashMap<>();
		sessParams.put(PerunUnauthorizedController.SESS_ATTR_REGISTRATION_UNITS, registrationUnits);
		sessParams.put(PerunUnauthorizedController.SESS_ATTR_CLIENT, client);
		RedirectUtils.redirectInternal(req, res, config, PerunUnauthorizedController.UNAUTHORIZED_NOT_IN_ENV_VOS_GROUPS_MAPPING, sessParams);
	}

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

	private void respondToPrompt(HttpServletRequest req) {
		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)
			);
		}
	}

	private Set<Long> getIdsFromParam(AuthProcFilterInitContext params, String propKey) {
		Set<Long> result = new HashSet<>();

		String prop = params.getProperty(propKey, "");
		if (StringUtils.hasText(prop)) {
			String[] parts = prop.split(",");
			for (String idStr: parts) {
				result.add(Long.parseLong(idStr));
			}
		}

		return result;
	}

	private Set<String> getVosAndGroupsNamesWhereUserInvalid(PerunUser user, Set<Long> vos, Set<Long> groups) {
		Set<String> invalidUnits = perunAdapter.getVosAndGroupsNamesWhereUserInvalid(user.getId(), vos, groups);
		if (!invalidUnits.isEmpty()) {
			log.info("{} - user is not member in required set of vos and groups", getFilterName());
			log.debug("{} - user: '{}', vos: '{}', groups: '{}', invalid units: '{}",
					getFilterName(), user.getId(), vos, groups, invalidUnits);
		}
		return invalidUnits;
	}

}
