package com.agilex.healthcare.schedulingmanager.clientapi;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.StringTokenizer;

import javax.ws.rs.core.UriBuilder;

import org.apache.log4j.Logger;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.common.AuthenticationScheme;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestTemplate;

import com.agilex.healthcare.schedulingmanager.clientapi.AuthenticationInfo;
import com.agilex.healthcare.utility.NullChecker;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlHiddenInput;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlPasswordInput;
import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
import com.gargoylesoftware.htmlunit.html.HtmlTextInput;

public class OauthClient {

	private static final Logger LOGGER = Logger.getLogger(OauthClient.class);
	private static final String CLIENT_SECRET = "client_secret";
	private static final String AUTH_CODE = "code";
	private static final String GRANT_TYPE = "grant_type";
	private static final String SCOPE = "scope";
	private static final String REDIRECT_URI = "redirect_uri";
	private static final String CLIENT_ID = "client_id";
	private static final String STATE = "state";
	private static final String RESPONSE_TYPE = "response_type";
	private String redirectionUri = "http://anywhere";

	public String authenticate(AuthenticationInfo authenticationInfo, WebClient userAgent) throws Exception {
		AuthorizationResult authorizationResult = authorize(authenticationInfo, userAgent);
		OAuth2AccessToken accessToken = getAccessToken(authenticationInfo, authorizationResult);
		return accessToken.getValue();
	}
 
	public AuthorizationResult authorize(AuthenticationInfo authenticationInfo, WebClient userAgent) throws IOException, MalformedURLException, URISyntaxException {
		String stateId = "stateId";

		if (userAgent == null)
			userAgent = new WebClient(BrowserVersion.CHROME);

		userAgent.getOptions().setThrowExceptionOnScriptError(false);
		
		userAgent.getOptions().setRedirectEnabled(false);

		String mhpAuthorizeUrl = authenticationInfo.getAuthorizeUrl();
		UriBuilder authorizeUrlBuilder = UriBuilder.fromPath(mhpAuthorizeUrl);

		URI uri = authorizeUrlBuilder.queryParam(RESPONSE_TYPE, AUTH_CODE).queryParam(STATE, stateId).queryParam(CLIENT_ID, authenticationInfo.getClientId()).queryParam(REDIRECT_URI, redirectionUri).queryParam(SCOPE, "read").build();

		String authenticationRedirectUri = null;
		HtmlPage confirmationPage = null;
		try {
			confirmationPage = userAgent.getPage(uri.toString());
		} catch (FailingHttpStatusCodeException e) {
			authenticationRedirectUri = e.getResponse().getResponseHeaderValue("Location");
			if (e.getResponse().getStatusCode() == 404 && authenticationRedirectUri == null)
				throw new IllegalStateException("404 Error (is the service running?): " + e.getMessage());
		}
		
		boolean denied = false;

		if (NullChecker.isNotNullish(authenticationRedirectUri)) {
			HtmlPage loginPage = userAgent.getPage(authenticationRedirectUri);
			// should be directed to the login screen...
			HtmlForm loginForm = loginPage.getFormByName("logonForm");
			((HtmlPasswordInput) loginForm.getInputByName("j_username")).setValueAttribute(authenticationInfo.getUsername());
			((HtmlPasswordInput) loginForm.getInputByName("j_password")).setValueAttribute(authenticationInfo.getPassword());
			
			HtmlHiddenInput facilityName = null;
			HtmlHiddenInput facilityCode = null;
			String errorMsg = null;
			try {
				facilityName = (HtmlHiddenInput) loginPage.getHtmlElementById("facilityName");
				facilityCode = (HtmlHiddenInput) loginPage.getHtmlElementById("facilityCode");
				
				if (facilityName != null){
					facilityName.setValueAttribute(authenticationInfo.getFacilityName());
				}
				if (facilityCode != null){
					facilityCode.setValueAttribute(authenticationInfo.getFacilityCode());
				}
			} catch (Exception e) {
				// Intentionally ignored as vistaLocation is not available on Veteran Login page
				LOGGER.error("loginForm element error : " + e.getMessage());
				errorMsg = e.getMessage();
			}

			try {
				HtmlElement button = (HtmlElement)loginPage.createElement("button");
				button.setAttribute("type", "submit");
				
				loginForm.appendChild(button);
				button.click();
				
				throw new IllegalStateException("should have been redirected to the authorization endpoint.  " + errorMsg);
			} catch (FailingHttpStatusCodeException e) {
				String authorizationRedirectUri = e.getResponse().getResponseHeaderValue("Location");
			
				if (authorizationRedirectUri != null && authorizationRedirectUri.contains("denied"))
					denied = true;
				
				try {
					confirmationPage = userAgent.getPage(authorizationRedirectUri);
					
					// dev mode (confirmation page)
					HtmlForm okForm = confirmationPage.getFormByName("confirmationForm");
					((HtmlSubmitInput) okForm.getInputByName("authorize")).click();
				} catch (FailingHttpStatusCodeException fe) {
					// other modes (no confirmation page)
					authenticationRedirectUri = fe.getResponse().getResponseHeaderValue("Location");
					if (fe.getResponse().getStatusCode() == 404 && authenticationRedirectUri == null)
						throw new IllegalStateException("404 Error (possible cause was access was denied for that user): " + fe.getMessage());
				} catch (ElementNotFoundException exc) {
					if (denied == true)
						throw new IllegalStateException("Access was denied for that user.");
					else
						throw exc;
				}
			}
		}
		
		if (authenticationRedirectUri == null) {
			if (denied == true)
				throw new IllegalStateException("No Redirect URI was returned after the User was denied access");
			else
				throw new IllegalStateException("No Redirect URI was returned after login completed");
		}
		URI redirection = new URI(authenticationRedirectUri); //authenticationRedirectUriBuilder.build();

		String code = null;
		String state = null;
		for (StringTokenizer queryTokens = new StringTokenizer(redirection.getQuery(), "&="); queryTokens.hasMoreTokens();) {
			String token = queryTokens.nextToken();
			if (AUTH_CODE.equals(token)) {
				if (code != null) {
					fail("shouldn't have returned more than one code.");
				}

				code = queryTokens.nextToken();
			} else if (STATE.equals(token)) {
				state = queryTokens.nextToken();
			}
		}

		assertEquals(stateId, state);
		assertNotNull(code);

		AuthorizationResult authorizationResult = new AuthorizationResult(code, state);
		return authorizationResult;
	}

	public OAuth2AccessToken getAccessToken(AuthenticationInfo authenticationInfo, AuthorizationResult authorizationResult) {
		MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
		formData.add(GRANT_TYPE, "authorization_code");
		formData.add(CLIENT_ID, authenticationInfo.getClientId());
		formData.add(SCOPE, "read");
		formData.add(REDIRECT_URI, redirectionUri);
		formData.add(AUTH_CODE, authorizationResult.getCode());
		formData.add(CLIENT_SECRET, authenticationInfo.getClientSecret());
		
		AuthorizationCodeResourceDetails resource = new AuthorizationCodeResourceDetails();

		
		resource.setAccessTokenUri(authenticationInfo.getTokenUrl());
		resource.setClientId(authenticationInfo.getClientId());
		resource.setScope(Arrays.asList("read"));
		resource.setClientSecret(authenticationInfo.getClientSecret());
		resource.setClientAuthenticationScheme(AuthenticationScheme.query);

		AccessTokenRequest request = new DefaultAccessTokenRequest();
		request.setAuthorizationCode(authorizationResult.getCode());
		request.setPreservedState(new Object());
		request.setAll(formData.toSingleValueMap());
		
		OAuth2RestTemplate template = new OAuth2RestTemplate(resource, new DefaultOAuth2ClientContext(request));
		OAuth2AccessToken accessToken = template.getAccessToken();
		
		return accessToken ;
	}

	public RestTemplate getRestTemplate() {
		RestTemplate client = new RestTemplate();
		HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
		client.setRequestFactory(requestFactory);
		client.setErrorHandler(new ResponseErrorHandler() {
			public boolean hasError(ClientHttpResponse response) throws IOException {
				return false;
			}

			public void handleError(ClientHttpResponse response) throws IOException {
			}
		});
		return client;
	}

	public HttpStatus getStatusCode(String path, final HttpHeaders headers) {
		RequestCallback requestCallback = createRequestCallback(headers);
		
		return execute(path, HttpMethod.GET, requestCallback);
	}

	private RequestCallback createRequestCallback(final HttpHeaders headers) {
		RequestCallback requestCallback = new NullRequestCallback();
		if (headers != null) {
			requestCallback = new RequestCallback() {
				public void doWithRequest(ClientHttpRequest request) throws IOException {
					request.getHeaders().putAll(headers);
				}
			};
		}
		return requestCallback;
	}
	
	public HttpStatus deleteToken(String path, OAuth2AccessToken accessToken) {
		final HttpHeaders headers = new HttpHeaders();
		headers.set("Authorization", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, accessToken.getValue()));
		RequestCallback requestCallback = createRequestCallback(headers);
		
		return execute(path, HttpMethod.DELETE, requestCallback);
	}

	private HttpStatus execute(String path, HttpMethod method, RequestCallback requestCallback) {
		HttpStatus statusCode = getRestTemplate().execute(path, method, requestCallback, new ResponseExtractor<ResponseEntity<String>>() {
			public ResponseEntity<String> extractData(ClientHttpResponse response) throws IOException {
				return new ResponseEntity<String>(response.getStatusCode());
			}
		}).getStatusCode();
		
		return statusCode;
	}
	

	public HttpStatus getStatusCode(String path) {
		return getStatusCode(path, null);
	}

	private static final class NullRequestCallback implements RequestCallback {
		public void doWithRequest(ClientHttpRequest request) throws IOException {
		}
	}
}
