/*
 * WebTop Services is a Web Application framework developed by Sonicle S.r.l.
 * Copyright (C) 2014 Sonicle S.r.l.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY SONICLE, SONICLE DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA.
 *
 * You can contact Sonicle S.r.l. at email address sonicle@sonicle.com
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * Sonicle logo and Sonicle copyright notice. If the display of the logo is not
 * reasonably feasible for technical reasons, the Appropriate Legal Notices must
 * display the words "Copyright (C) 2014 Sonicle S.r.l.".
 */
package com.sonicle.webtop.core.app;

import com.sonicle.commons.EnumUtils;
import com.sonicle.commons.InternetAddressUtils;
import com.sonicle.commons.LangUtils;
import com.sonicle.commons.URIUtils;
import com.sonicle.commons.net.IPUtils;
import com.sonicle.commons.web.json.JsonResult;
import com.sonicle.commons.web.ServletUtils;
import com.sonicle.security.DomainAccount;
import com.sonicle.security.Principal;
import com.sonicle.security.otp.OTPKey;
import com.sonicle.security.otp.OTPProviderFactory;
import com.sonicle.security.otp.provider.GoogleAuth;
import com.sonicle.security.otp.provider.GoogleAuthOTPKey;
import com.sonicle.security.otp.provider.SonicleAuth;
import com.sonicle.webtop.core.CoreLocaleKey;
import com.sonicle.webtop.core.CoreServiceSettings;
import com.sonicle.webtop.core.CoreSettings;
import com.sonicle.webtop.core.CoreSettings.OtpDeliveryMode;
import com.sonicle.webtop.core.CoreUserSettings;
import com.sonicle.webtop.core.TplHelper;
import com.sonicle.webtop.core.bol.ODomain;
import com.sonicle.webtop.core.bol.OUserSetting;
import com.sonicle.webtop.core.bol.js.JsTrustedDevice;
import com.sonicle.webtop.core.bol.js.TrustedDeviceCookie;
import com.sonicle.webtop.core.sdk.UserProfile;
import com.sonicle.webtop.core.sdk.UserProfileId;
import com.sonicle.webtop.core.sdk.WTException;
import com.sonicle.webtop.core.util.NotificationHelper;
import freemarker.template.TemplateException;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.glxn.qrgen.javase.QRCode;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;

/**
 *
 * @author malbinola
 */
public class OTPManager {
	private static final Logger logger = WT.getLogger(OTPManager.class);
	private static boolean initialized = false;
	
	/**
	 * Initialization method. This method should be called once.
	 * 
	 * @param wta WebTopApp instance.
	 * @return The instance.
	 */
	static synchronized OTPManager initialize(WebTopApp wta) {
		if (initialized) throw new RuntimeException("Initialization already done");
		OTPManager otpm = new OTPManager(wta);
		initialized = true;
		logger.info("Initialized");
		return otpm;
	}
	
	private WebTopApp wta = null;
	
	/**
	 * Private constructor.
	 * Instances of this class must be created using static initialize method.
	 * @param wta WebTopApp instance.
	 */
	private OTPManager(WebTopApp wta) {
		this.wta = wta;
	}
	
	/**
	 * Performs cleanup process.
	 */
	void cleanup() {
		wta = null;
		logger.info("Cleaned up");
	}
	
	public boolean isEnabled(UserProfileId pid) {
		CoreUserSettings cus = new CoreUserSettings(pid);
		return (cus.getOTPDelivery() == null) ? false : cus.getOTPEnabled();
		//return StringUtils.isBlank(cus.getOTPDelivery()) ? false : cus.getOTPEnabled();
	}
	
	public OtpDeliveryMode getDeliveryMode(UserProfileId pid) {
		CoreUserSettings cus = new CoreUserSettings(pid);
		return cus.getOTPDelivery();
	}
	
	public String getEmailAddress(UserProfileId pid) {
		CoreUserSettings cus = new CoreUserSettings(pid);
		return cus.getOTPEmailAddress();
	}
	
	private String getSecret(UserProfileId pid) {
		CoreUserSettings cus = new CoreUserSettings(pid);
		return cus.getOTPSecret();
	}
	
	public void deactivate(UserProfileId pid) {
		CoreUserSettings cus = new CoreUserSettings(pid);
		cus.clear(CoreSettings.OTP_SECRET);
		cus.clear(CoreSettings.OTP_EMAILADDRESS);
		cus.clear(CoreSettings.OTP_DELIVERY);
		cus.setOTPEnabled(false);
	}
	
	public EmailConfig configureEmail(UserProfileId pid, String emailAddress) throws WTException {
		SonicleAuth sa = (SonicleAuth)OTPProviderFactory.getInstance("SonicleAuth");
		UserProfile.Data ud = wta.getWebTopManager().userData(pid);
		
		InternetAddress to = InternetAddressUtils.toInternetAddress(emailAddress);
		if (to == null) throw new WTException("Invalid destination address [{0}]", emailAddress);
		OTPKey otp = sa.generateCredentials(ud.getEmail().getAddress());
		sendCodeEmail(pid, ud.getLocale(), to, otp.getVerificationCode());
		
		return new EmailConfig(otp, emailAddress);
	}
	
	public GoogleAuthConfig configureGoogleAuth(UserProfileId pid, int qrCodeSize) throws WTException {
		GoogleAuth ga = (GoogleAuth)OTPProviderFactory.getInstance("GoogleAuth");
		OTPKey otp = ga.generateCredentials();
		byte[] qrcode = generateGoogleAuthQRCode(pid, otp, qrCodeSize);
		return new GoogleAuthConfig(otp, qrcode);
	}
	
	public boolean activate(UserProfileId pid, Config config, int code) throws WTException {
		CoreUserSettings cus = new CoreUserSettings(pid);
		
		if(config instanceof EmailConfig) {
			SonicleAuth te = (SonicleAuth)OTPProviderFactory.getInstance("SonicleAuth");
			CoreServiceSettings css = new CoreServiceSettings(CoreManifest.ID, pid.getDomainId());
			long interval = css.getOTPProviderSonicleAuthKVI();
			
			if(te.check(code, config.otp.getVerificationCode(), Long.valueOf(config.otp.getKey()), interval)) {
				cus.setOTPEmailAddress(((EmailConfig)config).emailAddress);
				cus.setOTPDelivery(OtpDeliveryMode.EMAIL);
				cus.setOTPEnabled(true);
				return true;
			} else {
				return false;
			}
		} else if(config instanceof GoogleAuthConfig) {
			GoogleAuth ga = (GoogleAuth)OTPProviderFactory.getInstance("GoogleAuth");
			
			if(ga.check(code, config.otp.getKey())) {
				cus.setOTPSecret(config.otp.getKey());
				cus.setOTPDelivery(OtpDeliveryMode.GOOGLEAUTH);
				cus.setOTPEnabled(true);
				return true;
			} else {
				return false;
			}
		}
		return false;
	}
	
	private byte[] generateGoogleAuthQRCode(UserProfileId pid, OTPKey otp, int size) throws WTException {
		ODomain domain = wta.getWebTopManager().getDomain(pid.getDomainId());
		if(domain == null) throw new WTException("Domain not found [{0}]", pid.getDomainId());
		
		String issuer = URIUtils.encodeQuietly(MessageFormat.format("{0} ({1})", WT.getPlatformName(), domain.getInternetName()));
		InternetAddress ia = InternetAddressUtils.toInternetAddress(pid.getUserId(), domain.getInternetName(), null);
		if(ia == null) throw new WTException("Unable to build account address");
		
		String uri = GoogleAuthOTPKey.buildAuthenticatorURI(issuer, otp.getKey(), ia.getAddress());
		logger.debug("Generating OPT QRCode for {}", uri);
		return QRCode.from(uri).withSize(size, size).stream().toByteArray();
	}
	
	public Config prepareCheckCode(UserProfileId pid) throws WTException {
		OtpDeliveryMode deliveryMode = getDeliveryMode(pid);
		if (OtpDeliveryMode.EMAIL.equals(deliveryMode)) {
			SonicleAuth te = (SonicleAuth)OTPProviderFactory.getInstance("SonicleAuth");
			UserProfile.Data ud = wta.getWebTopManager().userData(pid);
			
			String emailAddress = getEmailAddress(pid);
			InternetAddress to = InternetAddressUtils.toInternetAddress(emailAddress);
			if (to == null) throw new WTException("Invalid destination address [{0}]", emailAddress);
			OTPKey otp = te.generateCredentials(ud.getEmail().getAddress());
			sendCodeEmail(pid, ud.getLocale(), to, otp.getVerificationCode());
			
			return new Config(EnumUtils.toSerializedName(OtpDeliveryMode.EMAIL), otp);
		} else {
			return new Config(EnumUtils.toSerializedName(OtpDeliveryMode.GOOGLEAUTH), null);
		}
	}
	
	public boolean checkCode(UserProfileId pid, Config data, int code) {
		OtpDeliveryMode deliveryMode = getDeliveryMode(pid);
		if (OtpDeliveryMode.EMAIL.equals(deliveryMode)) {
			CoreServiceSettings css = new CoreServiceSettings(CoreManifest.ID, pid.getDomainId());
			SonicleAuth te = (SonicleAuth)OTPProviderFactory.getInstance("SonicleAuth");
			long interval = css.getOTPProviderSonicleAuthKVI();
			return te.check(code, data.otp.getVerificationCode(), Long.valueOf(data.otp.getKey()), interval);
		} else {
			GoogleAuth ga = (GoogleAuth)OTPProviderFactory.getInstance("GoogleAuth");
			return ga.check(code, getSecret(pid));
		}
	}
	
	public void registerTrustedDevice(UserProfileId pid, JsTrustedDevice td) {
		SettingsManager sm = wta.getSettingsManager();
		String key = CoreSettings.OTP_TRUSTED_DEVICE + "@" + td.deviceId;
		sm.setUserSetting(pid.getDomainId(), pid.getUserId(), CoreManifest.ID, key, JsonResult.gson.toJson(td));
	}
	
	public boolean removeTrustedDevice(UserProfileId pid, String deviceId) {
		SettingsManager sm = wta.getSettingsManager();
		String key = CoreSettings.OTP_TRUSTED_DEVICE + "@" + deviceId;
		return sm.deleteUserSetting(pid.getDomainId(), pid.getUserId(), CoreManifest.ID, key);
	}
	
	public JsTrustedDevice getTrustedDevice(UserProfileId pid, String deviceId) {
		SettingsManager sm = wta.getSettingsManager();
		String key = CoreSettings.OTP_TRUSTED_DEVICE + "@" + deviceId;
		return LangUtils.value(sm.getUserSetting(pid.getDomainId(), pid.getUserId(), CoreManifest.ID, key), null, JsTrustedDevice.class);
	}
	
	public ArrayList<JsTrustedDevice> listTrustedDevices(UserProfileId pid) {
		SettingsManager sm = wta.getSettingsManager();
		List<OUserSetting> items = sm.getUserSettings(pid.getDomainId(), pid.getUserId(), CoreManifest.ID, CoreSettings.OTP_TRUSTED_DEVICE+"%");
		return JsTrustedDevice.asList(items);
	}
	
	public JsTrustedDevice trustThisDevice(UserProfileId pid, String userAgentHeader) {
		String deviceId = DigestUtils.shaHex(UUID.randomUUID().toString() + userAgentHeader);
		long now = new Date().getTime();
		JsTrustedDevice td = new JsTrustedDevice(deviceId, DomainAccount.buildName(pid.getDomainId(), pid.getUserId()), now, userAgentHeader);
		registerTrustedDevice(pid, td);
		return td;
	}
	
	public boolean isThisDeviceTrusted(UserProfileId pid, TrustedDeviceCookie tdc) {
		if(tdc == null) return false;
		CoreServiceSettings css = new CoreServiceSettings(CoreManifest.ID, pid.getDomainId());
		
		// Checks (if enabled) cookie duration
		int duration = css.getOTPDeviceTrustDuration();
		if(duration > 0) {
			long now = new Date().getTime();
			long expires = tdc.timestamp + TimeUnit.DAYS.toMillis(duration);
			if(now > expires) {
				logger.trace("Device cookie expired [{}days, {} > {}]", duration, now, expires);
				return false;
			}
		}
		
		// Checks if device is registered
		JsTrustedDevice td = getTrustedDevice(pid, tdc.deviceId);
		if(td == null) {
			logger.trace("Device ID not registered before [{}]", tdc.deviceId);
			return false;
		}
		
		// Checks account match
		if(!td.account.equals(tdc.account)) {
			logger.trace("Device ID not bound to the right account [{} != {}]", tdc.account, td.account);
			return false;
		}
		return true;
	}
	
	public boolean isTrusted(UserProfileId pid, String remoteIP) {
		CoreServiceSettings css = new CoreServiceSettings(CoreManifest.ID, pid.getDomainId());
		String addresses = css.getOTPTrustedAddresses();
		if(addresses != null) {
			String[] cidrs = SettingsManager.asArray(addresses);
			try {
				boolean inRange = IPUtils.isIPInRange(cidrs, remoteIP);
				if(inRange) return true;
			} catch(Exception ex) {
				logger.error("Problem performing IP range check", ex);
			}
		}
		return false;
	}
	
	public void clearTrustedDeviceCookie(UserProfileId pid, HttpServletResponse response) {
		String name = MessageFormat.format("TD_{0}", Principal.buildHashedName(pid.getDomainId(), pid.getUserId()));
		ServletUtils.eraseCookie(response, name);
	}
	
	public TrustedDeviceCookie readTrustedDeviceCookie(UserProfileId pid, HttpServletRequest request) {
		String secret = getSecret(pid);
		String name = MessageFormat.format("TD_{0}", Principal.buildHashedName(pid.getDomainId(), pid.getUserId()));
		return ServletUtils.getEncryptedCookie(secret, request, name, TrustedDeviceCookie.class);
	}
	
	public void writeTrustedDeviceCookie(UserProfileId pid, HttpServletResponse response, TrustedDeviceCookie tdc) {
		String secret = getSecret(pid);
		String name = MessageFormat.format("TD_{0}", Principal.buildHashedName(pid.getDomainId(), pid.getUserId()));
		int duration = 60*60*24*365*2; // 2 years
		ServletUtils.setEncryptedCookie(secret, response, name, tdc, TrustedDeviceCookie.class, duration);
	}
	
	private void sendCodeEmail(UserProfileId pid, Locale locale, InternetAddress to, int code) throws WTException {
		try {
			String bodyHeader = WT.lookupResource(CoreManifest.ID, locale, CoreLocaleKey.TPL_EMAIL_OTPCODEVERIFICATION_BODY_HEADER);
			String subject = NotificationHelper.buildSubject(locale, CoreManifest.ID, bodyHeader);
			String html = TplHelper.buildOtpCodeVerificationEmail(locale, String.valueOf(code));
			
			InternetAddress from = WT.getNoReplyAddress(pid.getDomainId());
			if (from == null) throw new WTException("Error building sender address");
			WT.sendEmail(WT.getGlobalMailSession(pid), true, from, to, subject, html);

		} catch(IOException | TemplateException ex) {
			logger.error("Unable to build email template", ex);
		} catch(MessagingException ex) {
			logger.error("Unable to send email", ex);
		}
	}
	
	public static class Config {
		public String delivery;
		public OTPKey otp;
		
		private Config(String delivery, OTPKey otp) {
			this.delivery = delivery;
			this.otp = otp;
		}
	}
	
	public static class EmailConfig extends Config {
		public String emailAddress;
		
		private EmailConfig(OTPKey otp, String emailAddress) {
			super(EnumUtils.toSerializedName(OtpDeliveryMode.EMAIL), otp);
			this.emailAddress = emailAddress;
		}
	}
	
	public static class GoogleAuthConfig extends Config {
		public byte[] qrcode;
		
		private GoogleAuthConfig(OTPKey otp, byte[] qrcode) {
			super(EnumUtils.toSerializedName(OtpDeliveryMode.GOOGLEAUTH), otp);
			this.qrcode = qrcode;
		}
	}
}
