/*******************************************************************************
 * Copyright 2018 The MIT Internet Trust Consortium
 *
 * 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 cz.muni.ics.openid.connect.web.endpoint;

import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTParser;
import cz.muni.ics.jwt.assertion.impl.SelfAssertionValidator;
import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oauth2.service.ClientDetailsEntityService;
import cz.muni.ics.oidc.server.configurations.PerunOidcConfig;
import cz.muni.ics.oidc.web.WebHtmlClasses;
import cz.muni.ics.oidc.web.controllers.ControllerUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.saml.SAMLLogoutFilter;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.text.ParseException;
import java.util.Map;

import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_TARGET;

/**
 * End Session Endpoint from OIDC session management.
 * <p>
 * This is a copy of the original file with modification at the end of processLogout().
 * </p>
 *
 * @author Martin Kuba <makub@ics.muni.cz>
 * @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
 */
@Controller
//TODO: implement according to spec (https://openid.net/specs/openid-connect-rpinitiated-1_0.html)
// other specs:
//TODO: https://openid.net/specs/openid-connect-frontchannel-1_0.html
//TODO: https://openid.net/specs/openid-connect-backchannel-1_0.html
//TODO: https://openid.net/specs/openid-connect-session-1_0.html
@Slf4j
public class EndSessionEndpoint {

	public static final String URL = "endsession";

	public static final String PARAM_POST_LOGOUT_REDIRECT_URI = "post_logout_redirect_uri";

	public static final String PARAM_STATE = "state";

	public static final String PARAM_CLIENT_ID = "client_id";

	public static final String PARAM_ID_TOKEN_HINT = "id_token_hint";

	private static final String SESSION_KEY_CLIENT = "client";

	private static final String SESSION_KEY_STATE = "state";

	private static final String SESSION_KEY_REDIRECT_URI = "redirect_uri";

	private static final String MODEL_CLIENT_KEY = "client";

	private static final String PREFIX_REDIRECT = "redirect:";

	private final SelfAssertionValidator validator;
	private final PerunOidcConfig perunOidcConfig;
	private final ClientDetailsEntityService clientService;
	private final WebHtmlClasses htmlClasses;

	@Autowired
	public EndSessionEndpoint(SelfAssertionValidator validator,
							  PerunOidcConfig perunOidcConfig,
							  ClientDetailsEntityService clientService,
							  WebHtmlClasses htmlClasses)
	{
		this.validator = validator;
		this.perunOidcConfig = perunOidcConfig;
		this.clientService = clientService;
		this.htmlClasses = htmlClasses;
	}

	@GetMapping(value = "/" + URL)
	public String endSession(@RequestParam(value = PARAM_ID_TOKEN_HINT, required = false) String idTokenHint,
							 @RequestParam(value = PARAM_POST_LOGOUT_REDIRECT_URI, required = false) String postLogoutRedirectUri,
							 @RequestParam(value = PARAM_STATE, required = false) String state,
							 @RequestParam(value = PARAM_CLIENT_ID, required = false) String clientId,
							 HttpServletRequest request,
							 HttpSession session,
							 Authentication auth,
							 Map<String, Object> model)
	{
		ClientDetailsEntity client = null; // pulled from ID token's audience field

		if (!Strings.isNullOrEmpty(postLogoutRedirectUri)) {
			session.setAttribute(SESSION_KEY_REDIRECT_URI, postLogoutRedirectUri);
		}
		if (!Strings.isNullOrEmpty(state)) {
			session.setAttribute(SESSION_KEY_STATE, state);
		}

		// parse the ID token hint to see if it's valid
		if (!Strings.isNullOrEmpty(idTokenHint)) {
			clientId = resolveClientIdFromTokenAndParameter(idTokenHint, clientId);
		}

		if (StringUtils.hasText(clientId)) {
			try {
				client = clientService.loadClientByClientId(clientId);
				session.setAttribute(SESSION_KEY_CLIENT, client);
			}  catch (InvalidClientException e) {
				// couldn't find the client, ignore it
				throw new InvalidRequestException(
						"Client requesting the logout cannot be found. Is someone doing something nasty?"
				);
			}
		}

		// are we logged in or not?
		if (auth == null || !request.isUserInRole("ROLE_USER")) {
			// We're not logged in, anyway. Process the final redirect bits if needed.
			return processLogout(null, null, request, session, model);
		} else {
			log.info("Display logout confirm prompt for user {} from client {}",
					auth.getName(), client != null ? client.getClientName() : "unknown"
			);
			// we are logged in, need to prompt the user before we log out
			model.put(MODEL_CLIENT_KEY, client);
			ControllerUtils.setPageOptions(model, request, htmlClasses, perunOidcConfig);
			return "logout";
		}
	}

	@PostMapping(value = "/" + URL)
	public String processLogout(@RequestParam(value = "approve", required = false) String approved,
								@RequestParam(value = "deny", required = false) String deny,
								HttpServletRequest request,
								HttpSession session,
								Map<String, Object> model)
	{
		String redirectUri = (String) session.getAttribute(SESSION_KEY_REDIRECT_URI);
		String state = (String) session.getAttribute(SESSION_KEY_STATE);
		ClientDetailsEntity client = (ClientDetailsEntity) session.getAttribute(SESSION_KEY_CLIENT);
		String redirectURL = null;

		// if we have a client AND the client has post-logout redirect URIs
		// registered AND the URI given is in that list, then...
		if (isUriValid(redirectUri, client)) {
			UriComponentsBuilder uri = UriComponentsBuilder.fromHttpUrl(redirectUri);
			if (StringUtils.hasText(state)) {
				uri = uri.queryParam(PARAM_STATE, state);
			}
			UriComponents uriComponents = uri.build();
			redirectURL = uriComponents.toString();
		}

		if (redirectURL != null) {
			String target = getRedirectUrl(redirectUri, state);
			if (StringUtils.hasText(approved)) {
				target = getLogoutUrl(target);
				log.trace("Endsession - redirecting to SAML logout, then to {}", target);
				return PREFIX_REDIRECT + target;
			} else {
				log.trace("Endsession - redirecting to {}", target);
				return PREFIX_REDIRECT + redirectURL;
			}
		} else {
			if (StringUtils.hasText(approved)) {
				log.trace("Endsession - redirecting to SAML logout only");
				return PREFIX_REDIRECT + getLogoutUrl(null);
			} else {
				ControllerUtils.setPageOptions(model, request, htmlClasses, perunOidcConfig);
				log.trace("Endsession - user denied the logout and we have no redirect, display logout denied page");
				return "logout_denied";
			}
		}
	}

	private String resolveClientIdFromTokenAndParameter(String idTokenHint, String clientId) {
		JWTClaimsSet idTokenClaims = null;
		try {
			JWT idToken = JWTParser.parse(idTokenHint);
			if (validator.isValid(idToken)) {
				idTokenClaims = idToken.getJWTClaimsSet();
			}
		} catch (ParseException e) {
			// it's not a valid ID token, ignore it
			log.debug("Invalid id token hint", e);
		}
		if (idTokenClaims != null) {
			String clientIdFromToken = Iterables.getOnlyElement(idTokenClaims.getAudience());

			if (StringUtils.hasText(clientId)) {
				if (StringUtils.hasText(clientIdFromToken) && !clientIdFromToken.equals(clientId)) {
					throw new InvalidRequestException(
							"Client ID and client for which the ID token has been issued do not match. Is someone doing something nasty?"
					);
				}
			} else {
				clientId = clientIdFromToken;
			}
		}

		return clientId;
	}

	private boolean isUriValid(String redirectUri, ClientDetailsEntity client) {
		return StringUtils.hasText(redirectUri)
			&& client != null
			&& client.getPostLogoutRedirectUris() != null
			&& client.getPostLogoutRedirectUris().contains(redirectUri);
	}

	private String getLogoutUrl(String target) {
		UriComponentsBuilder builder = UriComponentsBuilder.fromPath(SAMLLogoutFilter.FILTER_URL);
		if (StringUtils.hasText(target)) {
			builder.queryParam(PARAM_TARGET, target);
		}
		return builder.build().toString();
	}

	private String getRedirectUrl(String postLogoutRedirectUri, String state) {
		UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(postLogoutRedirectUri);
		if (StringUtils.hasText(state)) {
			builder.queryParam(PARAM_STATE, state);
		}
		return builder.build().toString();
	}

}
