package gov.va.med.mpn.auth.controller;

import gov.va.med.mpn.auth.utils.AuthUtils;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.ws.rs.core.UriBuilder;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.PropertySources;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
import org.springframework.security.oauth2.client.token.AccessTokenProviderChain;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordAccessTokenProvider;
import org.springframework.security.oauth2.common.AuthenticationScheme;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RestTemplate;

import com.agilex.healthcare.mobilehealthplatform.security.AppUser;
import com.agilex.healthcare.mobilehealthplatform.security.HATokenValidation;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.WebResource;


/**
 * The Class AuthorizationController.  A Spring controller that manages the login flow
 * to the MAE AuthorizationServices and Health Adapter.  Login redirects for authorization
 * and then passes code to /oathtoken where an access key is request and then verified.  The
 * verified security principal is used along with MDWS keys to add more Spring Roles to a newly
 * created Spring Security Principal.
 * <p>
 * <A HREF="http://www.hp.com"> Hewlett-Packard Enterprise Services</A>
 * </p>
 * 
 * @author <a href="mailto:david.aar.lucas@hp.com">Aaron Lucas</a> 
 */
@Controller
@PropertySources(value = {@PropertySource("classpath:authorization.properties")})
public class AuthorizationController {
	
	/** The logger. */
	private final Logger logger = LoggerFactory.getLogger(AuthorizationController.class);
	
	/** The Constant KEY_AUTH_CODE. */
	private static final String KEY_AUTH_CODE = "code";
	
	/** The Constant KEY_GRANT_TYPE. */
	private static final String KEY_GRANT_TYPE = "grant_type";
	
	/** The Constant KEY_SCOPE. */
	private static final String KEY_SCOPE = "scope";
	
	/** The Constant KEY_REDIRECT_URI. */
	private static final String KEY_REDIRECT_URI = "redirect_uri";
	
	/** The Constant KEY_ORIGINAL_REDIRECT_URI. */
	private static final String KEY_ORIGINAL_REDIRECT_URI = "original_redirect_uri";
	
	/** The Constant KEY_CLIENT_ID. */
	private static final String KEY_CLIENT_ID = "client_id";
	
	/** The Constant KEY_RESPONSE_TYPE. */
	private static final String KEY_RESPONSE_TYPE = "response_type";
	
	/** The Constant KEY_CLIENT_SECRET. */
	private static final String KEY_CLIENT_SECRET = "client_secret";
	
	/** The Constant TOKEN_SESSION_ATTTRIBUTE. */
	public static final String TOKEN_SESSION_ATTTRIBUTE = "ACCESS_TOKEN"; 
	
	/** The trusted client rest template. */
	@Autowired
	private OAuth2RestTemplate trustedClientRestTemplate;
	
	 /** The env. */
 	@Autowired
	private Environment env;
	 
	
	
	/**
	 * Redirect mae provider login.
	 *
	 * @param error the error
	 * @param originalRedirectUri the original redirect uri
	 * @param req the req
	 * @param response the response
	 * @throws Exception the exception
	 */
	@RequestMapping(value = "/login", method = RequestMethod.GET)
	public void redirectMAEProviderLogin(@RequestParam(value = "error", required = false) boolean error,
			@RequestParam(value = KEY_REDIRECT_URI, required = false) String originalRedirectUri,
			HttpServletRequest req, HttpServletResponse response) throws Exception{
		logger.debug("In Oauth login");
		String baseUri = buildBaseUriString(req);
		logger.debug("the auth url {}", env.getRequiredProperty("ha.provider.authorization.url"));
		UriBuilder authorizeUrlBuilder = UriBuilder.fromPath(env.getRequiredProperty("ha.provider.authorization.url"));
		
		String redirectUri = baseUri + "/oauthtoken";
		
		if (StringUtils.isNotEmpty(originalRedirectUri)) {
			StringBuilder sb = new StringBuilder(redirectUri);
			sb.append("?");
			sb.append(KEY_ORIGINAL_REDIRECT_URI);
			sb.append("=");
			sb.append(originalRedirectUri);
			redirectUri = sb.toString();
		}
		
		URI uri = authorizeUrlBuilder
				.queryParam(KEY_RESPONSE_TYPE, KEY_AUTH_CODE)
				.queryParam(KEY_CLIENT_ID, env.getRequiredProperty("oauth.client.id"))
				.queryParam(KEY_REDIRECT_URI, redirectUri)
				.queryParam(KEY_SCOPE, env.getRequiredProperty("oauth.scope")).build();

		redirect(response, uri.toString());
	
	}
	
	/**
	 * Handle oauth token.
	 *
	 * @param code the code
	 * @param originalRedirectUri the original redirect uri
	 * @param request the request
	 * @param response the response
	 */
	@RequestMapping(value = "/oauthtoken", method = RequestMethod.GET)
	public void handleOauthToken(@RequestParam(value = KEY_AUTH_CODE, required = false) String code,
			@RequestParam(value = KEY_ORIGINAL_REDIRECT_URI, required = false) String originalRedirectUri,
			HttpServletRequest request, HttpServletResponse response) {
	
		String contextPath = StringUtils.isNotEmpty(request.getContextPath()) ? request.getContextPath() : "";
		String redirectUri = contextPath;
	
		
		if (StringUtils.isNotEmpty(code)) {
			logger.debug("In Oauth token for code: " + code);
			SecurityContext ctx = SecurityContextHolder.getContext();
			logger.debug(ctx.getAuthentication().getPrincipal().toString());
			ctx.setAuthentication(null);
			request.getSession().setAttribute(TOKEN_SESSION_ATTTRIBUTE,getAccessToken(request, code, originalRedirectUri));
			addJSONMessageConverter(trustedClientRestTemplate);
			logger.debug("principal within context: {}",SecurityContextHolder.getContext().getAuthentication().toString());
	
		} 
		redirect(response, getRoleRedirect(redirectUri,SecurityContextHolder.getContext().getAuthentication()));
		
	}
	
	/**
	 * HA logout.
	 *
	 * @param request the request
	 * @param response the response
	 */
	@RequestMapping(value = "/halogout", method = RequestMethod.GET)
	public void haLogout(HttpServletRequest request, HttpServletResponse response) {		
		try {
			logger.debug("Invalidating session.");
			request.getSession().invalidate();
		}
		catch(IllegalStateException e) {
			logger.debug("Session already invalidated.");
		}
		
		//logout and redirect to MPN logout
		String baseUri = buildBaseUriString(request);
		String redirectUri = baseUri + "/logout";
		UriBuilder logoutUriBuilder = UriBuilder.fromPath(env.getRequiredProperty("ha.logout.url"));
		URI uri = (logoutUriBuilder.queryParam(KEY_REDIRECT_URI, redirectUri)).build();
		logger.debug("Auth logout with url: {}",uri.toString());
		redirect(response,uri.toString());		
	}
	
	/**
	 * Gets the role redirect.
	 *
	 * @param redirectUri the redirect uri
	 * @param auth the auth
	 * @return the role redirect
	 */
	private String getRoleRedirect(String redirectUri, Authentication auth)
	{
		//check to see if the roles even contain mpn if not unauthorized
		if (!AuthUtils.matchAuthority(auth, AuthUtils.REGEX_MPN_ROLE)) {
			redirectUri = redirectUri + env.getRequiredProperty("unauthorized.url");
		} else {
			//an MPN role exists. if admin redirect to home where select from Builder/Runner is option
			if (AuthUtils.matchAuthority(auth, AuthUtils.REGEX_MPNADMIN_ROLE)) {
				redirectUri = redirectUri + env.getRequiredProperty("form.builder.url");
			} else {
				//go straight to runner
				redirectUri = redirectUri + env.getRequiredProperty("form.runner.url");
			}
		}
		return redirectUri;
	}
	
	
	/**
	 * Gets the access token.
	 *
	 * @param req the req
	 * @param code the code
	 * @param originalRedirectUri the original redirect uri
	 * @return the access token
	 */
	private String getAccessToken(HttpServletRequest req, String code, String originalRedirectUri) {
		String baseUri = buildBaseUriString(req);

		String redirectUri = baseUri + "/oauthtoken";
		if (StringUtils.isNotEmpty(originalRedirectUri)) {
			StringBuilder sb = new StringBuilder(redirectUri);
			sb.append("?");
			sb.append(KEY_ORIGINAL_REDIRECT_URI);
			sb.append("=");
			sb.append(originalRedirectUri);
			redirectUri = sb.toString();
		}

		MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
		formData.add(KEY_GRANT_TYPE, "authorization_code");
		formData.add(KEY_CLIENT_ID, env.getRequiredProperty("oauth.client.id"));
		formData.add(KEY_SCOPE, env.getRequiredProperty("oauth.scope"));
		formData.add(KEY_AUTH_CODE, code);
		formData.add(KEY_CLIENT_SECRET, env.getRequiredProperty("oauth.secret"));
		formData.add(KEY_REDIRECT_URI, redirectUri);
		
		AuthorizationCodeResourceDetails resource = new AuthorizationCodeResourceDetails();

		logger.debug("the access token url {}", env.getRequiredProperty("ha.access.token.url"));
		String mhpTokenUrl = env.getRequiredProperty("ha.access.token.url");
		resource.setAccessTokenUri(mhpTokenUrl);
		resource.setClientId(env.getRequiredProperty("oauth.client.id"));
		resource.setScope(Arrays.asList(env.getRequiredProperty("oauth.scope")));
		resource.setClientSecret(env.getRequiredProperty("oauth.secret"));
		resource.setClientAuthenticationScheme(AuthenticationScheme.query);

		AccessTokenRequest request = new DefaultAccessTokenRequest();
		request.setAuthorizationCode(code);
		request.setPreservedState(new Object());
		request.setAll(formData.toSingleValueMap());
		trustedClientRestTemplate = new OAuth2RestTemplate(resource, new DefaultOAuth2ClientContext(request));
		addJSONMessageConverter(trustedClientRestTemplate);

		OAuth2AccessToken accessToken = trustedClientRestTemplate.getAccessToken();
		logger.debug("prior to validate");
		validateToken(accessToken);
		HttpSession session = req.getSession(true);
		session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
		return accessToken.getValue();
		
	}
	
	/**
	 * Validate token.
	 *
	 * @param accessToken the access token
	 */
	private void validateToken(OAuth2AccessToken accessToken)
	{
		logger.debug("the validate token url {}", env.getRequiredProperty("ha.validate.token.url"));
		HATokenValidation tokenValidator =  new HATokenValidation(env.getRequiredProperty("ha.validate.token.url"));
		OAuth2Authentication authentication = tokenValidator.loadAuthentication(accessToken.getValue());
		//add mdws keys as roles - MPN roles will be stored as security keys in VistA
		SecurityContextHolder.getContext().setAuthentication(AuthUtils.updateAppUserRoles(((AppUser)authentication.getPrincipal())));
	}
	
	
	/**
	 * Adds the json message converter.
	 *
	 * @param template the template
	 */
	private void addJSONMessageConverter(OAuth2RestTemplate template) {
		List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
		MappingJackson2HttpMessageConverter jsonMessageConverter = new  MappingJackson2HttpMessageConverter();
		jsonMessageConverter.setObjectMapper(new ObjectMapper());

		messageConverters.add(jsonMessageConverter);

		AuthorizationCodeAccessTokenProvider authorizationCodeAccessTokenProvider = new AuthorizationCodeAccessTokenProvider();
		authorizationCodeAccessTokenProvider.setMessageConverters(messageConverters);

		AccessTokenProviderChain accessTokenProvider = new AccessTokenProviderChain(Arrays.<AccessTokenProvider> asList(
				authorizationCodeAccessTokenProvider, new ImplicitAccessTokenProvider(),
				new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider()));

		template.setAccessTokenProvider(accessTokenProvider);
	}
	
	/**
	 * Builds the base uri string.
	 *
	 * @param req the req
	 * @return the string
	 */
	private String buildBaseUriString(HttpServletRequest req) {
		StringBuilder sb = new StringBuilder();
		sb.append(req.getScheme());
		sb.append("://");
		sb.append(req.getServerName());
		sb.append(":");
		sb.append(req.getServerPort());
		sb.append(req.getContextPath());

		return sb.toString();
	}
	
	/**
	 * Redirect.
	 *
	 * @param response the response
	 * @param redirectUri the redirect uri
	 */
	private void redirect(HttpServletResponse response, String redirectUri){
		try {
			response.sendRedirect(redirectUri);
		} catch (IOException e) {
			throw new RuntimeException("Exception while redirecting to " + redirectUri, e);
		}
	}

	/**
	 * Sets the trusted client rest template.
	 *
	 * @param trustedClientRestTemplate the new trusted client rest template
	 */
	public void setTrustedClientRestTemplate(
			OAuth2RestTemplate trustedClientRestTemplate) {
		this.trustedClientRestTemplate = trustedClientRestTemplate;
	}
	
	

}
