/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.openmeetings.web.pages.auth;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_IGNORE_BAD_SSL;
import static org.apache.openmeetings.util.OpenmeetingsVariables.getBaseUrl;
import static org.apache.openmeetings.util.OpenmeetingsVariables.isAllowRegisterFrontend;
import static org.apache.openmeetings.web.app.Application.urlForPage;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.io.IOUtils;
import org.apache.openmeetings.db.dao.basic.ConfigurationDao;
import org.apache.openmeetings.db.dao.server.OAuth2Dao;
import org.apache.openmeetings.db.dao.user.IUserManager;
import org.apache.openmeetings.db.dto.user.OAuthUser;
import org.apache.openmeetings.db.entity.server.OAuthServer;
import org.apache.openmeetings.db.entity.server.OAuthServer.RequestInfoMethod;
import org.apache.openmeetings.db.entity.user.User;
import org.apache.openmeetings.db.entity.user.User.Type;
import org.apache.openmeetings.util.OmException;
import org.apache.openmeetings.web.app.Application;
import org.apache.openmeetings.web.app.WebSession;
import org.apache.openmeetings.web.common.OmModalCloseButton;
import org.apache.openmeetings.web.pages.BaseInitedPage;
import org.apache.openmeetings.web.room.IconTextModal;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.ResourceModel;
import org.apache.wicket.request.IRequestParameters;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.flow.RedirectToUrlException;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.spring.injection.annot.SpringBean;
import org.apache.wicket.util.string.StringValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.openjson.JSONException;
import com.github.openjson.JSONObject;

import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.TextContentModal;

public class SignInPage extends BaseInitedPage {
	private static final long serialVersionUID = 1L;
	private static final Logger log = LoggerFactory.getLogger(SignInPage.class);
	public static final String TOKEN_PARAM = "token";
	private SignInDialog signin;
	private final Modal<String> kick = new IconTextModal("kick") {
		private static final long serialVersionUID = 1L;
		{
			withLabel(new ResourceModel("606"));
			withErrorIcon();
			setCloseOnEscapeKey(false);
			show(true);
			setUseCloseHandler(true);
			addButton(OmModalCloseButton.of("54"));
		}

		@Override
		protected void onClose(IPartialPageRequestHandler target) {
			WebSession.setKickedByAdmin(false);
			Application.get().restartResponseAtSignInPage();
		}
	};
	private final Modal<String> forgetInfoDialog = new TextContentModal("forgetInfo", new ResourceModel("321")) {
		private static final long serialVersionUID = 1L;

		@Override
		protected void onClose(IPartialPageRequestHandler handler) {
			signin.show(handler);
		}
	};
	private final ForgetPasswordDialog forget = new ForgetPasswordDialog("forget", forgetInfoDialog);
	private final Modal<String> registerInfoDialog = new TextContentModal("registerInfo", Model.of("")) {
		private static final long serialVersionUID = 1L;

		@Override
		protected void onInitialize() {
			super.onInitialize();
			setModelObject(getString("warn.notverified"));
			get("content").setOutputMarkupId(true);
		}

		@Override
		public Modal<String> setModelObject(String obj) {
			super.setModelObject(obj);
			get("content").setDefaultModelObject(obj);
			return this;
		}

		@Override
		protected void onClose(IPartialPageRequestHandler handler) {
			signin.show(handler);
		}
	};
	RegisterDialog r = new RegisterDialog("register", registerInfoDialog);
	@SpringBean
	private ConfigurationDao cfgDao;
	@SpringBean
	private IUserManager userManager;
	@SpringBean
	private OAuth2Dao oauthDao;

	public SignInPage() {
		this(new PageParameters());
	}

	public SignInPage(PageParameters p) {
		super();
		WebSession.get().checkToken(p.get(TOKEN_PARAM));
		if (WebSession.get().isSignedIn()) {
			setResponsePage(Application.get().getHomePage());
		}
		StringValue oauthid = p.get("oauthid");
		if (!oauthid.isEmpty()) { // oauth2 login
			try {
				long serverId = oauthid.toLong(-1);
				OAuthServer server = oauthDao.get(serverId);
				log.debug("OAuthServer={}", server);
				if (server == null) {
					log.warn("OAuth server id={} not found", serverId);
					return;
				}

				if (!p.get("code").isNull()) { // got code
					String code = p.get("code").toString();
					log.debug("OAuth response code={}", code);
					AuthInfo authInfo = getToken(code, server);
					if (authInfo == null) {
						return;
					}
					log.debug("OAuthInfo={}", authInfo);
					OAuthUser user = getAuthParams(authInfo, code, server);
					loginViaOAuth2(user, serverId);
				} else { // redirect to get code
					showAuth(server);
				}
			} catch (IOException|NoSuchAlgorithmException|JSONException e) {
				log.error("OAuth2 login error", e);
			}
		}
		//will try to login directly using parameters sent by POST
		IRequestParameters pp = RequestCycle.get().getRequest().getPostParameters();
		StringValue login = pp.getParameterValue("login"), password = pp.getParameterValue("password");
		if (!login.isEmpty() && !password.isEmpty()) {
			try {
				if (WebSession.get().signIn(login.toString(), password.toString(), Type.USER, null)) {
					setResponsePage(Application.get().getHomePage());
				} else {
					log.error("Failed to login using POST parameters passed");
				}
			} catch (OmException e) {
				log.error("Exception while login with POST parameters passed", e);
			}
		}
	}

	@Override
	protected void onInitialize() {
		super.onInitialize();

		signin = new SignInDialog("signin");
		signin.setRegisterDialog(r);
		signin.setForgetPasswordDialog(forget);
		r.setSignInDialog(signin);
		forget.setSignInDialog(signin);
		add(signin.setVisible(!WebSession.get().isKickedByAdmin()),
				r.setVisible(allowRegister()), forget, kick.setVisible(WebSession.get().isKickedByAdmin()));
		add(forgetInfoDialog
				.header(new ResourceModel("312"))
				.addButton(OmModalCloseButton.of("54"))
				.setUseCloseHandler(true)
		);
		add(registerInfoDialog
				.header(new ResourceModel("235"))
				.addButton(OmModalCloseButton.of("54"))
				.setUseCloseHandler(true)
		);
	}

	@Override
	public void renderHead(IHeaderResponse response) {
		super.renderHead(response);
		response.render(OnDomReadyHeaderItem.forScript(""
				+ "$('#signin-dialog, #register-dialog, #forget-dialog').on('shown.bs.modal', function () {\n"
				+ "	$(this).find('.auto-focus').trigger('focus');\n"
				+ "})"
				));
	}

	boolean allowRegister() {
		return isAllowRegisterFrontend();
	}

	boolean allowOAuthLogin() {
		return !oauthDao.getActive().isEmpty();
	}

	@Override
	protected void onParameterArrival(IRequestParameters params, AjaxRequestTarget arg1) {
		WebSession.get().setArea(getUrlFragment(params));
	}

	// ============= OAuth2 methods =============
	private static Map<String, String> getInitParams(final OAuthServer s) {
		Map<String, String> params = new HashMap<>();
		params.put("{$client_id}", s.getClientId());
		params.put("{$redirect_uri}", getRedirectUri(s));
		return params;
	}

	public static void showAuth(final OAuthServer s) {
		String authUrl = prepareUrl(s.getRequestKeyUrl(), getInitParams(s));
		log.debug("redirectUrl={}", authUrl);
		throw new RedirectToUrlException(authUrl);
	}

	private static String prepareUrl(String urlTemplate, Map<String, String> params) {
		String result = urlTemplate;
		for (Entry<String, String> e : params.entrySet()) {
			if (e.getValue() != null) {
				try {
					result = result.replace(e.getKey(), URLEncoder.encode(e.getValue(), UTF_8.name()));
				} catch (UnsupportedEncodingException err) {
					log.error("Unexpected exception while encoding URI param {}", e, err);
				}
			}
		}
		return result;
	}

	public static String getRedirectUri(OAuthServer server) {
		String result = "";
		if (server.getId() != null) {
			String base = getBaseUrl();
			result = urlForPage(SignInPage.class, new PageParameters().add("oauthid", server.getId()), base);
		}
		return result;
	}

	private void prepareConnection(URLConnection _connection) {
		if (!(_connection instanceof HttpsURLConnection)) {
			return;
		}
		if (!cfgDao.getBool(CONFIG_IGNORE_BAD_SSL, false)) {
			return;
		}
		TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() {
			@Override
			public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
				//no-op
			}

			@Override
			public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
				//no-op
			}

			@Override
			public X509Certificate[] getAcceptedIssuers() {
				return new X509Certificate[] {};
			}
		}};
		try {
			HttpsURLConnection connection = (HttpsURLConnection)_connection;
			SSLContext sslContext = SSLContext.getInstance("SSL");
			sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
			SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
			connection.setSSLSocketFactory(sslSocketFactory);
			connection.setHostnameVerifier((arg0, arg1) -> true);
		} catch (Exception e) {
			log.error("[prepareConnection]", e);
		}
	}

	private static Map<String, String> getParams(final OAuthServer s, String code, AuthInfo authInfo) {
		Map<String, String> params = getInitParams(s);
		params.put("{$client_id}", s.getClientId());
		params.put("{$client_secret}", s.getClientSecret());
		if (authInfo != null) {
			params.put("{$access_token}", authInfo.accessToken);
			params.put("{$user_id}", authInfo.userId);
		}
		if (code != null) {
			params.put("{$code}", code);
		}
		return params;
	}

	private AuthInfo getToken(String code, OAuthServer server) throws IOException {
		String requestTokenBaseUrl = server.getRequestTokenUrl();
		// build url params to request auth token
		String requestTokenParams = server.getRequestTokenAttributes();
		requestTokenParams = prepareUrl(requestTokenParams, getParams(server, code, null));
		// request auth token
		HttpURLConnection connection = (HttpURLConnection) new URL(requestTokenBaseUrl).openConnection();
		prepareConnection(connection);
		connection.setRequestMethod(server.getRequestTokenMethod().name());
		connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
		connection.setRequestProperty("charset", UTF_8.name());
		connection.setRequestProperty("Content-Length", String.valueOf(requestTokenParams.length()));
		connection.setDoInput(true);
		connection.setDoOutput(true);
		connection.setUseCaches(false);
		DataOutputStream paramsOutputStream = new DataOutputStream(connection.getOutputStream());
		paramsOutputStream.writeBytes(requestTokenParams);
		paramsOutputStream.flush();
		String sourceResponse = IOUtils.toString(connection.getInputStream(), UTF_8);
		// parse json result
		AuthInfo result = new AuthInfo(sourceResponse);
		// access token must be specified
		if (result.accessToken == null) {
			log.error("Response doesn't contain access_token field:\n {}", sourceResponse);
			return null;
		}
		return result;
	}

	private OAuthUser getAuthParams(AuthInfo authInfo, String code, OAuthServer server) throws IOException {
		// prepare url
		String requestInfoUrl = server.getRequestInfoUrl();
		requestInfoUrl = prepareUrl(requestInfoUrl, getParams(server, code, authInfo));
		// send request
		HttpURLConnection connection = (HttpURLConnection) new URL(requestInfoUrl).openConnection();
		if (server.getRequestInfoMethod() == RequestInfoMethod.HEADER) {
			connection.setRequestProperty("Authorization", String.format("Bearer %s", authInfo.accessToken));
		} else {
			connection.setRequestMethod(server.getRequestInfoMethod().name());
		}
		prepareConnection(connection);
		String json = IOUtils.toString(connection.getInputStream(), UTF_8);
		log.debug("User info={}", json);
		// parse json result
		return new OAuthUser(json, server);
	}

	private void loginViaOAuth2(OAuthUser user, long serverId) throws IOException, NoSuchAlgorithmException {
		User u = userManager.loginOAuth(user, serverId);

		if (u != null && WebSession.get().signIn(u)) {
			setResponsePage(Application.get().getHomePage());
		} else {
			log.error("Failed to login via OAuth2!");
		}
	}

	private static class AuthInfo {
		final String accessToken;
		final String refreshToken;
		final String tokenType;
		final String userId;
		final long expiresIn;

		AuthInfo(String jsonStr) {
			log.debug("AuthInfo={}", jsonStr);
			JSONObject json = new JSONObject(jsonStr);
			accessToken = json.optString("access_token");
			refreshToken = json.optString("refresh_token");
			tokenType = json.optString("token_type");
			userId = json.optString("user_id");
			expiresIn = json.optLong("expires_in");
		}

		@Override
		public String toString() {
			return new StringBuilder()
				.append("AuthInfo [accessToken=").append(accessToken)
				.append(", refreshToken=").append(refreshToken)
				.append(", tokenType=").append(tokenType)
				.append(", userId=").append(userId)
				.append(", expiresIn=").append(expiresIn)
				.append("]").toString();
		}
	}
}