/*

    Copyright (C) 2019 AGNITAS AG (https://www.agnitas.org)

    This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
    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 Affero 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 <https://www.gnu.org/licenses/>.

*/

package org.agnitas.emm.core.commons.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.security.PublicKey;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;

import javax.naming.InitialContext;
import javax.sql.DataSource;

import org.agnitas.emm.core.velocity.VelocityCheck;
import org.agnitas.util.AgnUtils;
import org.agnitas.util.DataEncryptor;
import org.agnitas.util.DateUtilities;
import org.agnitas.util.ServerCommand.Command;
import org.agnitas.util.ServerCommand.Server;
import org.agnitas.util.Systemconfig;
import org.agnitas.util.Tuple;
import org.agnitas.util.XmlUtilities;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Required;
import org.w3c.dom.Document;

import com.agnitas.dao.ComAdminDao;
import com.agnitas.dao.ComCompanyDao;
import com.agnitas.dao.ComServerMessageDao;
import com.agnitas.dao.ConfigTableDao;
import com.agnitas.dao.LicenseDao;
import com.agnitas.dao.impl.ComServerMessageDaoImpl;
import com.agnitas.dao.impl.ConfigTableDaoImpl;
import com.agnitas.dao.impl.LicenseDaoImpl;
import com.agnitas.emm.core.Permission;
import com.agnitas.emm.core.supervisor.dao.ComSupervisorDao;
import com.agnitas.emm.wsmanager.dao.WebserviceUserDao;
import com.agnitas.service.LicenseError;
import com.agnitas.util.CryptographicUtilities;
import com.agnitas.util.Version;

/**
 * ConfigurationService for EMM
 * This class uses buffering of the values of the config_tbl for better performance.
 * The value for refreshing period is also stored in config_tbl and can be changed
 * manually with no need for restarting the server.
 * For refreshing the values the very next time the old period value will be used.
 * Afterwards the new one will take effect.
 * The value 0 means, there will be no buffering.
 */
public class ConfigService {
	
	/** The logger. */
	private static final transient Logger logger = Logger.getLogger(ConfigService.class);

	private static final String PUBLIC_LICENSE_KEYSTRING = "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCcdArGIy/hseE9bz53siYnClOQ\nABrRFRVs/zdN8HpweXxpFqa4SUcp9SFIjqgQ5l/FRdEE9EFc865oGZI1H2RK9Jl1\nb7NxFBwu6S4kWFpy+0Xlp+FCLMXVkBDxLB3vv96VR714n2bFh11/UanlfptqMYPQ\nq7gZCmP5Bc06ORaxrQIDAQAB\n-----END PUBLIC KEY-----";

    public static final int MAX_GENDER_VALUE_BASIC = 2;
    public static final int MAX_GENDER_VALUE_EXTENDED = 5;
    
    public static final int MAXIMUM_ALLOWED_MAILTYPE = 5;
    
    /** Application startup timestamp **/
    private static final Date STARTUP_TIME = new Date();
    private static Version applicationVersion = null;
    
    private static ConfigService instance;

	/** DAO for access config table. */
	protected ConfigTableDao configTableDao;
	
	/** DAO for access company_info_tbl. */
	protected CompanyInfoDao companyInfoDao;
	
	/** DAO for access company table. */
	protected ComCompanyDao companyDao;
	
	/** DAO for access admin table. */
	protected ComAdminDao adminDao; // TODO Replace by AdminService
	
	/** DAO for access webservice user table. */
	protected WebserviceUserDao webserviceUserDao;
	
	/** DAO for access supervisor table. */
	protected ComSupervisorDao supervisorDao;
	
	/** DAO for access license table. */
	protected LicenseDao licenseDao;
	
	/** DataEncryptor. */
	protected DataEncryptor dataEncryptor;
	
	/** DAO for access server_command_tbl table. */
	protected ComServerMessageDao serverMessageDao;

	private static Boolean IS_ORACLE_DB = null;
	private static Map<String, Map<Integer, String>> LICENSE_VALUES = null;
	private static Map<String, Map<Integer, String>> EMM_PROPERTIES_VALUES = null;
	private static Map<String, Map<Integer, String>> CONFIGURATIONVALUES = null;
	private static Date LASTREFRESHTIME = null;
	private static Calendar EXPIRATIONTIME = null;
		
	/**
	 * Method for gathering the ConfigService within JSP-files or in (BIRT-)environments without the spring context like "Listener" and "Filter" defined in web.xml
	 * All other access to ConfigService should be done via spring context or dependency injection
	 * 
	 * see also: singleton pattern
	 */
	public static synchronized ConfigService getInstance() {
		if (instance == null) {
			instance = new ConfigService();
			DataSource dataSource;
			try {
				dataSource = (DataSource) new InitialContext().lookup("java:comp/env/jdbc/emm_db");
			} catch (Exception e) {
				logger.error("Cannot find datasource in JNDI context: " + e.getMessage(), e);
				throw new RuntimeException("Cannot find datasource in JNDI context: " + e.getMessage(), e);
			}
			
			ConfigTableDao configTableDao = new ConfigTableDaoImpl();
			((ConfigTableDaoImpl) configTableDao).setDataSource(dataSource);
			instance.setConfigTableDao(configTableDao);
			
			LicenseDao licenseDao = new LicenseDaoImpl();
			((LicenseDaoImpl) licenseDao).setDataSource(dataSource);
			instance.setLicenseDao(licenseDao);
			
			CompanyInfoDao companyInfoDao = new CompanyInfoDao();
			companyInfoDao.setDataSource(dataSource);
			instance.setCompanyInfoDao(companyInfoDao);
			
			ComServerMessageDao serverMessageDao = new ComServerMessageDaoImpl();
			((ComServerMessageDaoImpl) serverMessageDao).setDataSource(dataSource);
			instance.setServerMessageDao(serverMessageDao);
		}
		return instance;
	}
	
	public static boolean isOracleDB() {
		return BooleanUtils.toBoolean(IS_ORACLE_DB);
	}
	
	/**
	 * This method may only be called in mock-test environments, where there is no real db to detect its vendor
	 * 
	 * @param isOracleDB
	 */
	public static void setDbVendorForMockTestingIsOracleDB(boolean isOracleDB) {
		IS_ORACLE_DB = isOracleDB;
	}
	public static Boolean getDbVendorForMockTestingIsOracleDB() {
		return IS_ORACLE_DB;
	}
	
	public ConfigService() {
		instance = this;
	}
	
	// ----------------------------------------------------------------------------------------------------------------
	// Dependency Injection
	
	/**
	 * Set DAO accessing configuration in DB.
	 * 
	 * @param configTableDao DAO accessing configuration in DB
	 */
	@Required
	public void setConfigTableDao(ConfigTableDao configTableDao) {
		this.configTableDao = configTableDao;
		invalidateCache();
		LICENSE_VALUES = null;
	}
	
	@Required
	public void setCompanyInfoDao(CompanyInfoDao companyInfoDao) {
		this.companyInfoDao = companyInfoDao;
		invalidateCache();
		LICENSE_VALUES = null;
	}
	
	/**
	 * Set DAO accessing company data.
	 * 
	 * @param companyDao DAO accessing company data
	 */
	@Required
	public void setCompanyDao(ComCompanyDao companyDao) {
		this.companyDao = companyDao;
		invalidateCache();
		LICENSE_VALUES = null;
	}
	
	/**
	 * Set DAO accessing license data.
	 * 
	 * @param licenseDao DAO accessing company data
	 */
	@Required
	public void setLicenseDao(LicenseDao licenseDao) {
		this.licenseDao = licenseDao;
		invalidateCache();
		LICENSE_VALUES = null;
	}
	
	/**
	 * Set DAO accessing admin data.
	 * 
	 * @param adminDao DAO accessing admin data
	 */
	@Required
	public void setAdminDao(ComAdminDao adminDao) {
		this.adminDao = adminDao;
		invalidateCache();
		LICENSE_VALUES = null;
	}
	
	/**
	 * Set DAO accessing admin data.
	 * 
	 * @param webserviceUserDao DAO accessing admin data
	 */
	@Required
	public void setWebserviceUserDao(WebserviceUserDao webserviceUserDao) {
		this.webserviceUserDao = webserviceUserDao;
		invalidateCache();
		LICENSE_VALUES = null;
	}
	
	/**
	 * Set DAO accessing supervisors.
	 * 
	 * @param supervisorDao DAO accessing supervisors
	 */
	@Required
	public void setSupervisorDao(ComSupervisorDao supervisorDao) {
		this.supervisorDao = supervisorDao;
		invalidateCache();
		LICENSE_VALUES = null;
	}
	
	/**
	 * Set data encryptor.
	 * 
	 * @param dataEncryptor data encryptor
	 */
	@Required
	public void setDataEncryptor(DataEncryptor dataEncryptor) {
		this.dataEncryptor = dataEncryptor;
	}
	
	@Required
	public void setServerMessageDao(ComServerMessageDao serverMessageDao) {
		this.serverMessageDao = serverMessageDao;
	}
	
	// ----------------------------------------------------------------------------------------------------------------
	// Business Logic

	private void invalidateCache() {
		CONFIGURATIONVALUES = null;
	}
	
	protected synchronized void refreshValues() {
		try {
			if (IS_ORACLE_DB == null && configTableDao != null) {
				// On ConfigService startup check db vendor
				IS_ORACLE_DB = ((ConfigTableDaoImpl) configTableDao).isOracleDB();
			}

			if (LICENSE_VALUES == null) {
				// On ConfigService startup read license data and check licensefile signature
				LICENSE_VALUES = readLicenseData();

				// On ConfigService startup check licensefile data
				checkLicenseData();
			}
			
			if (EMM_PROPERTIES_VALUES == null) {
				// On ConfigService startup read emm.properties file
				EMM_PROPERTIES_VALUES = readEmmPropertiesFile();
				try {
					applicationVersion = new Version(EMM_PROPERTIES_VALUES.get(ConfigValue.ApplicationVersion.toString()).get(0));
				} catch (Exception e) {
					// The version sign is not valid. Maybe it is text like 'Unknown'.
				}
			}
			
			if (CONFIGURATIONVALUES == null || EXPIRATIONTIME == null || GregorianCalendar.getInstance().after(EXPIRATIONTIME)) {
				Date now = new Date();
				
				// Check for server reset commands
				if (LICENSE_VALUES != null && serverMessageDao.getCommand(LASTREFRESHTIME, now, Server.ALL, Command.RELOAD_LICENSE_DATA).size() > 0) {
					// This is needed to allow checkLicenseData() to not get stuck in dead lock
					LASTREFRESHTIME = now;
					
					LICENSE_VALUES = readLicenseData();
					
					// On license reset check the new license data
					checkLicenseData();
				}
				
				LASTREFRESHTIME = now;
				
				Map<String, Map<Integer, String>> newValues = new HashMap<>();
				joinConfigValues(newValues, EMM_PROPERTIES_VALUES);

				if (configTableDao != null) {
					joinConfigValues(newValues, configTableDao.getAllEntriesForThisHost());
				}

				if (companyInfoDao != null) {
					joinConfigValues(newValues, companyInfoDao.getAllEntriesForThisHost());
				}

				int minutes;
				if (newValues.containsKey(ConfigValue.ConfigurationExpirationMinutes.toString())) {
					minutes = NumberUtils.toInt(newValues.get(ConfigValue.ConfigurationExpirationMinutes.toString()).get(0));
				} else {
					minutes = NumberUtils.toInt(ConfigValue.ConfigurationExpirationMinutes.getDefaultValue());
				}

				if (minutes > 0) {
					GregorianCalendar nextExpirationTime = new GregorianCalendar();
					nextExpirationTime.add(GregorianCalendar.MINUTE, minutes);
					EXPIRATIONTIME = nextExpirationTime;
				} else {
					EXPIRATIONTIME = null;
				}
				
				joinConfigValues(newValues, LICENSE_VALUES);
				
				if (CONFIGURATIONVALUES == null) {
					logger.info("Initialized ConfigService with " + newValues.size() + " values");
				}
				
				CONFIGURATIONVALUES = newValues;
			}
		} catch (LicenseError e) {
			IS_ORACLE_DB = null;
			LICENSE_VALUES = null;
			EMM_PROPERTIES_VALUES = null;
			CONFIGURATIONVALUES = null;
			EXPIRATIONTIME = null;
			
			logger.error(e.getMessage());
			throw e;
		} catch (Exception e) {
			logger.error("Cannot refresh config data from database", e);
		}
	}

	private void joinConfigValues(Map<String, Map<Integer, String>> basicValues, Map<String, Map<Integer, String>> additionalValues) throws Exception {
		for (Entry<String, Map<Integer, String>> configEntry : additionalValues.entrySet()) {
			Map<Integer, String> configValuesMap = basicValues.get(configEntry.getKey());
			if (configValuesMap == null) {
				configValuesMap = new HashMap<>();
				basicValues.put(configEntry.getKey(), configValuesMap);
			}
			for (Entry<Integer, String> companyEntry : configEntry.getValue().entrySet()) {
				configValuesMap.put(companyEntry.getKey(), AgnUtils.replaceVersionPlaceholders(AgnUtils.replaceHomeVariables(companyEntry.getValue()), applicationVersion));
			}
		}
	}

	private Map<String, Map<Integer, String>> readEmmPropertiesFile() throws Exception {
		try {
			Map<String, Map<Integer, String>> emmPropertiesMap = new HashMap<>();
			Properties properties = new Properties();
			properties.load(getClass().getClassLoader().getResourceAsStream("emm.properties"));
			for (String key : properties.stringPropertyNames()) {
				Map<Integer, String> configValueMap = emmPropertiesMap.get(key);
				if (configValueMap == null) {
					configValueMap = new HashMap<>();
					emmPropertiesMap.put(key, configValueMap);
				}
				configValueMap.put(0, AgnUtils.replaceHomeVariables(properties.getProperty(key)));
			}
			return emmPropertiesMap;
		} catch (Exception e) {
			throw new Exception("Cannot read emm.properties: " + e.getMessage(), e);
		}
	}
	
	private Map<String, Map<Integer, String>> readLicenseData() throws Exception {
		ClassLoader classLoader = ConfigService.class.getClassLoader();
		URL licenseURL = classLoader.getResource("emm.license.xml");
		if (licenseURL != null) {
			Date licenseDate = null;
			if (licenseURL.toString().startsWith("file:")) {
				String licenseFilePath = URLDecoder.decode(licenseURL.toString().substring(5), "UTF-8");
				BasicFileAttributes fileAttributes = Files.readAttributes(Paths.get(licenseFilePath), BasicFileAttributes.class);
				FileTime licenseFileTime = fileAttributes.lastModifiedTime();
				if (licenseFileTime == null) {
					licenseFileTime = fileAttributes.creationTime();
				}
				if (licenseFileTime != null) {
					licenseDate = new Date(licenseFileTime.toMillis());
				}
			}
			byte[] licenseDataArray;
			try (InputStream licenseStream = classLoader.getResourceAsStream("emm.license.xml")) {
				licenseDataArray = IOUtils.toByteArray(licenseStream);
			}

			byte[] licenseSignatureDataArray;
			try (InputStream signatureStream = classLoader.getResourceAsStream("emm.license.xml.sig")) {
				licenseSignatureDataArray = IOUtils.toByteArray(signatureStream);
			} catch(Exception e) {
				licenseSignatureDataArray = null;
			}
			
			if (licenseSignatureDataArray != null) {
				PublicKey publicKey = CryptographicUtilities.getPublicKeyFromString(PUBLIC_LICENSE_KEYSTRING);
				boolean success = CryptographicUtilities.verifyData(licenseDataArray, publicKey, licenseSignatureDataArray);
				if (success) {
					licenseDao.storeLicense(licenseDataArray, licenseSignatureDataArray, licenseDate);
				}
			} else {
				// OpenEMM has no license signature
				licenseDao.storeLicense(licenseDataArray, null, licenseDate);
			}
		}
		
		if (licenseDao.hasLicenseData()) {
			byte[] licenseDataArray = licenseDao.getLicenseData();

			Map<String, Map<Integer, String>> licenseData = new HashMap<>();
			Document licenseDocument = XmlUtilities.parseXMLDataAndXSDVerifyByDOM(licenseDataArray, "UTF-8", null);
			Map<String, String> licenseDataFromXml = XmlUtilities.getSimpleValuesOfNode(licenseDocument.getElementsByTagName("emm.license").item(0));
			for (Entry<String, String> entry : licenseDataFromXml.entrySet()) {
				Map<Integer, String> configValueMap = licenseData.get(entry.getKey());
				if (configValueMap == null) {
					configValueMap = new HashMap<>();
					licenseData.put(entry.getKey(), configValueMap);
				}
				configValueMap.put(0, entry.getValue());
			}
	
			// Handle different names
			Map<Integer, String> licenseIdValueMap = licenseData.get(ConfigValue.System_Licence.toString());
			if (licenseIdValueMap == null) {
				licenseIdValueMap = new HashMap<>();
				licenseData.put(ConfigValue.System_Licence.toString(), licenseIdValueMap);
			}
			licenseIdValueMap.put(0, licenseData.get("licenseID").get(0));
			licenseData.remove("licenseID");
	
			int licenseID = Integer.parseInt(licenseData.get(ConfigValue.System_Licence.toString()).get(0));
			
			if (licenseID == 0) {
				// OpenEMM: No signature check, but remove all data for company_id > 1
				if (companyDao != null) {
					companyDao.deactivateExtendedCompanies();
				}
			} else {
				byte[] licenseSignatureDataArray = licenseDao.getLicenseSignatureData();
				if (licenseSignatureDataArray == null) {
					throw new Exception("LicenseSignature is missing");
				} else {
					PublicKey publicKey = CryptographicUtilities.getPublicKeyFromString(PUBLIC_LICENSE_KEYSTRING);
					boolean success = CryptographicUtilities.verifyData(licenseDataArray, publicKey, licenseSignatureDataArray);
					if (!success) {
						throw new Exception("LicenseSignature is invalid");
					}
				}
			}
			
			return licenseData;
		} else {
			throw new Exception("Missing license data");
		}
	}
	
	private void checkLicenseData() throws Exception {
		// Check validity of license data to current db data
		
		if (LICENSE_VALUES == null) {
			throw new LicenseError("Missing License data");
		}
		
		if (configTableDao != null) {
			if (NumberUtils.toInt(LICENSE_VALUES.get(ConfigValue.System_Licence.toString()).get(0)) > 0) {
				// Check or set license ID in DB
				try {
					String storedLicenseID = configTableDao.getAllEntriesForThisHost().get(ConfigValue.System_Licence.toString()).get(0);
					if (StringUtils.isBlank(storedLicenseID)) {
						configTableDao.storeEntry("system", "licence", LICENSE_VALUES.get(ConfigValue.System_Licence.toString()).get(0));
						logger.info("Writing new LicenseID: " + LICENSE_VALUES.get(ConfigValue.System_Licence.toString()).get(0));
					} else if (!storedLicenseID.equals(LICENSE_VALUES.get(ConfigValue.System_Licence.toString()).get(0))) {
						throw new LicenseError("Invalid LicenseID", LICENSE_VALUES.get(ConfigValue.System_Licence.toString()).get(0), storedLicenseID);
					}
				} catch (Exception e) {
					throw new LicenseError("Error while checking license id: " + e.getMessage(), e);
				}
			
				// Check license ID in licence.cfg file, if exists
				String	licenceCfgLicenseId = (new Systemconfig ()).get ("licence");
				
				if ((licenceCfgLicenseId != null) && (!licenceCfgLicenseId.equals(LICENSE_VALUES.get(ConfigValue.System_Licence.toString()).get(0)))) {
					throw new LicenseError("Invalid LicenseID in licence.cfg", LICENSE_VALUES.get(ConfigValue.System_Licence.toString()).get(0), licenceCfgLicenseId);
				}
			}
		}
		
		// Check validity time limit
		Date validUntil;
		if (StringUtils.isNotBlank(LICENSE_VALUES.get(ConfigValue.System_License_ExpirationDate.toString()).get(0))) {
			try {
				validUntil = new SimpleDateFormat(DateUtilities.DD_MM_YYYY_HH_MM_SS).parse(LICENSE_VALUES.get(ConfigValue.System_License_ExpirationDate.toString()).get(0) + " 23:59:59");
			} catch (ParseException e) {
				throw new LicenseError("Invalid validity data: " + e.getMessage(), e);
			}
			if (new Date().after(validUntil)) {
				throw new LicenseError("error.license.outdated", LICENSE_VALUES.get(ConfigValue.System_License_ExpirationDate.toString()).get(0), EMM_PROPERTIES_VALUES.get(ConfigValue.Mailaddress_Support.toString()).get(0));
			}
		}

		if (companyDao != null) {
			// Check maximum number of companies
			int maximumNumberOfCompanies = NumberUtils.toInt(LICENSE_VALUES.get(ConfigValue.System_License_MaximumNumberOfCompanies.toString()).get(0));
			if (maximumNumberOfCompanies >= 0) {
				int numberOfCompanies = companyDao.getNumberOfCompanies();
				if (numberOfCompanies > maximumNumberOfCompanies) {
					throw new LicenseError("Invalid Number of accounts", maximumNumberOfCompanies, numberOfCompanies);
				}
			}
		}

		if (adminDao != null) {
			// Check maximum number of admins
			int maximumNumberOfAdmins = NumberUtils.toInt(LICENSE_VALUES.get(ConfigValue.System_License_MaximumNumberOfAdmins.toString()).get(0));
			if (maximumNumberOfAdmins >= 0) {
				int numberOfAdmins = adminDao.getNumberOfAdmins();
				if (numberOfAdmins > maximumNumberOfAdmins) {
					throw new LicenseError("Invalid Number of admins", maximumNumberOfAdmins, numberOfAdmins);
				}
			}
		}

		if (webserviceUserDao != null) {
			// Check maximum number of admins
			int maximumNumberOfWebserviceUsers = NumberUtils.toInt(LICENSE_VALUES.get(ConfigValue.System_License_MaximumNumberOfWebserviceUsers.toString()).get(0));
			if (maximumNumberOfWebserviceUsers >= 0) {
				int numberOfWebserviceUsers = webserviceUserDao.getNumberOfWebserviceUsers();
				if (numberOfWebserviceUsers > maximumNumberOfWebserviceUsers) {
					throw new LicenseError("Invalid Number of Webservice Users", maximumNumberOfWebserviceUsers, numberOfWebserviceUsers);
				}
			}
		}

		if (supervisorDao != null) {
			// Check maximum number of supervisors
			int maximumNumberOfSupervisors = NumberUtils.toInt(LICENSE_VALUES.get(ConfigValue.System_License_MaximumNumberOfSupervisors.toString()).get(0));
			if (maximumNumberOfSupervisors >= 0) {
				int numberOfSupervisors = supervisorDao.getNumberOfSupervisors();
				if (numberOfSupervisors > maximumNumberOfSupervisors) {
					throw new LicenseError("Invalid Number of supervisors", maximumNumberOfSupervisors, numberOfSupervisors);
				}
			}
		}

		if (companyDao != null) {
			// Check maximum number of customers
			int maximumNumberOfCustomers = NumberUtils.toInt(LICENSE_VALUES.get(ConfigValue.System_License_MaximumNumberOfCustomers.toString()).get(0));
			if (maximumNumberOfCustomers >= 0) {
				int numberOfCustomers = companyDao.getMaximumNumberOfCustomers();
				if (numberOfCustomers > maximumNumberOfCustomers) {
					throw new LicenseError("Invalid Number of customers", maximumNumberOfCustomers, numberOfCustomers);
				}
			}
		
			// Check maximum number of profile fields
			int maximumNumberOfProfileFields = NumberUtils.toInt(LICENSE_VALUES.get(ConfigValue.System_License_MaximumNumberOfProfileFields.toString()).get(0));
			if (maximumNumberOfProfileFields >= 0) {
				int numberOfProfileFields;
				try {
					numberOfProfileFields = companyDao.getMaximumNumberOfProfileFields();
				} catch (Exception e) {
					throw new LicenseError("Cannot detect number of profileFields: " + e.getMessage(), e);
				}
			 	if (numberOfProfileFields > maximumNumberOfProfileFields) {
			 		throw new LicenseError("Invalid Number of profileFields", maximumNumberOfProfileFields, numberOfProfileFields);
			 	}
			}
		
			// Check allowed premium features
			String allowedPremiumFeaturesData = LICENSE_VALUES.get(ConfigValue.System_License_AllowedPremiumFeatures.toString()).get(0);
			Set<String> allowedPremiumFeatures = new HashSet<>();
			Set<String> unAllowedPremiumFeatures = new HashSet<>();
			for (String allowedPremiumFeature : allowedPremiumFeaturesData.split(" |;|,|\\t|\\n")) {
				if (StringUtils.isNotBlank(allowedPremiumFeature)) {
					allowedPremiumFeatures.add(allowedPremiumFeature.trim());
				}
			}
			if (!allowedPremiumFeatures.contains("all") && !allowedPremiumFeatures.contains("ALL")) {
				for (Entry<Permission, String> permissionEntry : Permission.getAllPermissionsAndCategories().entrySet()) {
					if (permissionEntry.getKey().isPremium() && !allowedPremiumFeatures.contains(permissionEntry.getKey().toString())) {
						unAllowedPremiumFeatures.add(permissionEntry.getKey().toString());
					}
				}
				adminDao.deleteFeaturePermissions(unAllowedPremiumFeatures);
				companyDao.setupPremiumFeaturePermissions(allowedPremiumFeatures, unAllowedPremiumFeatures);
			} else {
				// Init the permission system anyway and assign categories to the rights etc.
				Permission.getAllPermissionsAndCategories();
			}
		}
	}
	
	public void writeValue(final ConfigValue configurationValueID, final String value) {
		String[] parts = configurationValueID.toString().split("\\.", 2);
		
		configTableDao.storeEntry(parts[0], parts[1], value);
		
		invalidateCache();
	}

	public void writeValue(final ConfigValue configurationValueID, final int companyID, final String value) {
		writeValue(configurationValueID, companyID, value, null);
	}

	public void writeValue(final ConfigValue configurationValueID, final int companyID, final String value, final String description) {
		companyInfoDao.writeConfigValue(companyID, configurationValueID.toString(), value, description);
		
		invalidateCache();
	}

	public void writeOrDeleteIfDefaultValue(final ConfigValue configurationValueID, final int companyID, final String value){
		String defaultValue = getValue(configurationValueID, 0);
		if (StringUtils.equals(defaultValue, value) && companyID > 0){
			String[] parts = configurationValueID.toString().split("\\.", 2);
			if (parts.length > 1) {
				configTableDao.deleteEntry(parts[0], parts[1] + "." + companyID);
			}
			companyInfoDao.deleteValue(companyID, configurationValueID.toString());
			invalidateCache();
		} else {
			writeValue(configurationValueID, companyID, value);
		}
	}

	public void writeBooleanValue(final ConfigValue configurationValueID, final boolean value) {
		String[] parts = configurationValueID.toString().split("\\.", 2);
		
		configTableDao.storeEntry(parts[0], parts[1], value ? "true" : "false");
		
		invalidateCache();
	}

	public void writeBooleanValue(final ConfigValue configurationValueID, final int companyID, final boolean value) {
		writeBooleanValue(configurationValueID, companyID, value, null);
	}

	public void writeBooleanValue(final ConfigValue configurationValueID, final int companyID, final boolean value, String description) {
		companyInfoDao.writeConfigValue(companyID, configurationValueID.toString(), value ? "true" : "false", description);

		invalidateCache();
	}

	public void writeOrDeleteIfDefaultBooleanValue(final ConfigValue configurationValueID, final int companyID, final boolean value){
		boolean defaultValue = getBooleanValue(configurationValueID, 0);
		if (defaultValue == value && companyID > 0){
			String[] parts = configurationValueID.toString().split("\\.", 2);
			if (parts.length > 1) {
				configTableDao.deleteEntry(parts[0], parts[1] + "." + companyID);
			}
			companyInfoDao.deleteValue(companyID, configurationValueID.toString());
			invalidateCache();
		} else {
			writeBooleanValue(configurationValueID, companyID, value);
		}
	}
	
	public String getValue(ConfigValue configurationValueID) {
		try {
			refreshValues();
			
			String value = null;
			
			Map<Integer, String> companyValueMap = CONFIGURATIONVALUES.get(configurationValueID.toString());
			if (companyValueMap != null) {
				value = companyValueMap.get(0);
			}
			
			if (value == null) {
				value = configurationValueID.getDefaultValue();
			}
			return value;
		} catch (LicenseError e) {
			if (ConfigValue.SupportEmergencyUrl.equals(configurationValueID))  {
				try {
					return configTableDao.getAllEntriesForThisHost().get(ConfigValue.SupportEmergencyUrl.toString()).get(0);
				} catch (Exception e1) {
					throw new RuntimeException("Cannot load SupportEmergencyUrl from database", e1);
				}
			} else {
				throw e;
			}
		}
	}
	
	public String getValue(ConfigValue configurationValueID, String defaultValue) {
		String returnValue = getValue(configurationValueID);
		if (returnValue == null || returnValue.length() == 0) {
			return defaultValue;
		} else {
			return returnValue;
		}
	}
	
	public String getEncryptedValue(ConfigValue configurationValueID, @VelocityCheck int companyID) throws Exception {
		String encryptedDataBase64 = getValue(configurationValueID, companyID);
		
		if (StringUtils.isNotEmpty(encryptedDataBase64)) {
			return dataEncryptor.decrypt(encryptedDataBase64);
		} else {
			return null;
		}
	}
	
	/**
	 * Does a chained search for specified configuration. Search stops at first match:
	 * 
	 * <ol>
	 *   <li>Search configuration value for given company ID and hostname.</li>
	 *   <li>Search configuration value for given hostname and the default company ID 0</li>
	 *   <li>Search configuration value for given company ID.</li>
	 *   <li>Search configuration value for company ID 0.</li>
	 *   <li>Search configuration value without company ID</li>
	 * </ol>
	 * 
	 * If there is no matching configuration, {@code null} is returned.
	 * 
	 * @param configurationValueID configuration value ID
	 * @param companyID company ID

	 * @return configuration value as String or {@code null}
	 */
	public String getValue(ConfigValue configurationValueID, @VelocityCheck int companyID) {
		refreshValues();
		
		String value = null;
		
		Map<Integer, String> companyValueMap = CONFIGURATIONVALUES.get(configurationValueID.toString());
		if (companyValueMap != null) {
			if (companyValueMap.containsKey(companyID)) {
				value = companyValueMap.get(companyID);
			} else {
				value = companyValueMap.get(0);
			}
		}
		
		if (value == null) {
			value = configurationValueID.getDefaultValue();
		}
		
		return value;
	}
	
	/**
	 * Same as {@link #getValue(ConfigValue, int)} with additional default value.
	 * 
	 * If no matching configuration is found, the specified default value is returned.
	 * 
	 * @param configurationValueID configuration value ID
	 * @param companyID company ID
	 * @param defaultValue default value
	 * 
	 * @return configuration value or specified default value
	 */
	public String getValue(ConfigValue configurationValueID, @VelocityCheck int companyID, String defaultValue) {
		String value = getValue(configurationValueID, companyID);
		
		if (value == null) {
			return defaultValue;
		} else {
			return value;
		}
	}
	
	public boolean getBooleanValue(ConfigValue configurationValueID) {
		String value = getValue(configurationValueID);
		
		return AgnUtils.interpretAsBoolean(value);
	}
	
	public boolean getBooleanValue(ConfigValue configurationValueID, @VelocityCheck int companyID) {
		String value = getValue(configurationValueID, companyID);
		
		return AgnUtils.interpretAsBoolean(value);
	}
	
	/**
	 * Returns an integer value using logic with fallback and default value.
	 * See {@link #getValue(ConfigValue, int, String)} for details on fallback.
	 * 
	 * @param configurationValueID ID of configuration value
	 * @param companyID company ID
	 * @param defaultValue default value
	 * 
	 * @return integer configuration value of default value
	 */
	public int getIntegerValue(ConfigValue configurationValueID, @VelocityCheck int companyID, int defaultValue) {
		String value = getValue(configurationValueID, companyID, Integer.toString(defaultValue));
		
		return Integer.parseInt(value);
	}
	
	public int getIntegerValue(ConfigValue configurationValueID, @VelocityCheck int companyID) {
		String value = getValue(configurationValueID, companyID);
		
		return Integer.parseInt(value);
	}
	
	public long getLongValue(ConfigValue configurationValueID, @VelocityCheck int companyID, int defaultValue) {
		String value = getValue(configurationValueID, companyID, Integer.toString(defaultValue));
		
		return Long.parseLong(value);
	}

	public long getLongValue(ConfigValue configurationValueID) {
		String value = getValue(configurationValueID);
		
		return Long.parseLong(value);
	}
	
	public long getLongValue(ConfigValue configurationValueID, @VelocityCheck int companyID) {
		String value = getValue(configurationValueID, companyID);
		
		return Long.parseLong(value);
	}

	public float getFloatValue(ConfigValue configurationValueID, @VelocityCheck int companyID){
		String value = getValue(configurationValueID, companyID);

		return Float.parseFloat(value);
	}
	
	public int getIntegerValue(ConfigValue configurationValueID) {
		String value = getValue(configurationValueID);
		if (StringUtils.isNotEmpty(value)) {
			return Integer.parseInt(value);
		} else {
			return 0;
		}
	}

	public float getFloatValue(ConfigValue configurationValueID) {
		String value = getValue(configurationValueID);
		if (StringUtils.isNotEmpty(value)) {
			return Float.parseFloat(value);
		} else {
			return 0;
		}
	}
	
	public List<String> getListValue(ConfigValue configurationValueID) {
		String value = getValue(configurationValueID);
		if (StringUtils.isNotEmpty(value)) {
			return Arrays.asList(value.split(";"));
		} else {
			return Collections.emptyList();
		}
	}
	
	public String getDescription(ConfigValue configurationValueID, @VelocityCheck int companyID) {
		String description = companyInfoDao.getDescription(configurationValueID.toString(), companyID);
		if (description == null) {
			description = companyInfoDao.getDescription(configurationValueID.toString(), 0);
		}
		return description;
	}
	
	/**
	 * Checks, if runtime checks for Velocity are enabled. If no settings for
	 * given company ID are found, checks for company ID 0. If no settings for
	 * company ID 0 are found, runtime checks are enabled globally.
	 * 
	 * @param companyId company ID to check
	 * 
	 * @return true, if runtime checks are enabled
	 */
	public boolean isVelocityRuntimeCheckEnabled( int companyId) {
		String value = getValue( ConfigValue.VelocityRuntimeCheck, companyId);
		
		if (value != null) {
			return AgnUtils.interpretAsBoolean( value);
		} else {
			if (companyId != 0) {
				value = getValue(ConfigValue.VelocityRuntimeCheck);
				
				if (value != null) {
					return AgnUtils.interpretAsBoolean( value);
				} else {
					return true;
				}
			} else {
				return true;
			}
		}
	}

	/**
	 * Checks, if invalid Velocity scripts are to be aborted. If no settings for
	 * given company ID are found, checks for company ID 0. If no settings for
	 * company ID 0 are found, abortion of scripts is globally enabled.
	 * 
	 * @param companyId company ID to check
	 * 
	 * @return true if scripts are to be aborted
	 */
	public boolean isVelocityScriptAbortEnabled(int companyId) {
		String value = getValue(ConfigValue.VelocityScriptAbort, companyId);
		
		if (value != null) {
			return AgnUtils.interpretAsBoolean(value);
		} else {
			if (companyId != 0) {
				value = getValue( ConfigValue.VelocityScriptAbort);
				
				if (value != null) {
					return AgnUtils.interpretAsBoolean(value);
				} else {
					return true;
				}
			} else {
				return true;
			}
		}
	}
	
	/**
	 * Checks, if company is allowed to use webservice &quot;&SendServiceMailing&quot;.
	 * 
	 * @param companyID company ID
	 * 
	 * @return <code>true</code> if company is allowed to use webservice
	 */
	public boolean isWebserviceSendServiceMailingEnabled(int companyID) {
		return getBooleanValue(ConfigValue.WebserviceEnableSendServiceMailing, companyID);
	}

	/**
	 * Returns the size limit for bulk webservices.
	 * 
	 * @param companyID company ID
	 * 
	 * @return size limit
	 */
	public int getWebserviceBulkSizeLimit(final int companyID) {
		return getIntegerValue(ConfigValue.WebserviceBulkSizeLimit, companyID, 1000);
	}
	
	/**
	 * Returns <code>true</code>, if history on profile fields is enabled.
	 * 
	 * @param companyID company ID
	 * 
	 * @return <code>true</code> if history on profile fields is enabled
	 */
	public boolean isRecipientProfileHistoryEnabled(final int companyID) {
		return getBooleanValue(ConfigValue.RecipientProfileFieldHistory, companyID);
	}

	/**
	 * Returns the maximum allowed number of user-selected profile fields in history.
	 * 
	 * @param companyID company ID
	 * 
	 * @return maximum allowed number of user-selected profile fields in history
	 */
	public int getMaximumNumberOfUserDefinedHistoryProfileFields(final int companyID) {
		return getIntegerValue(ConfigValue.MaximumNumberOfUserSelectedProfileFieldsInHistory, companyID, 5);
	}

	public boolean useUnsharpRecipientQuery(int companyID) {
		return getBooleanValue(ConfigValue.UseUnsharpRecipientQuery, companyID);
	}

	/**
	 * Returns <code>true</code> if push notifications are enabled for given company ID.
	 * 
	 * @param companyID company ID
	 * 
	 * @return <code>true</code> if push notifications are enabled
	 */
	public final boolean isPushNotificationEnabled(final int companyID) {
		return getBooleanValue(ConfigValue.PushNotificationsEnabled, companyID);
	}

	/**
	 * Returns the provider credentials for push server implementation.
	 * The content is JSON encoded.
	 * 
	 * @param companyID company ID
	 * 
	 * @return JSON-encoded provider credentials
	 */
	public final String pushNotificationProviderCredentials(final int companyID) {
		// "{}" represents empty map in JSON
		return getValue(ConfigValue.PushNotificationProviderCredentials, companyID, "{}");
	}

	public String getPushNotificationFileSinkBaseDirectory(int companyID) {
		return getValue(ConfigValue.PushNotificationFileSinkBaseDirectory, companyID, "/tmp");
	}
	
	public final String getPushNotificationResultBaseDirectory() {
		return getValue(ConfigValue.PushNotificationResultBaseDirectory, "/tmp");
	}
	
	public final int getLicenseID() {
		return getIntegerValue(ConfigValue.System_Licence);
	}

	public final String getPushNotificationSftpHost() {
		return getValue(ConfigValue.PushNotificationSftpHost);
	}

	public String getPushNotificationSftpUser() {
		return getValue(ConfigValue.PushNotificationSftpUser);
	}

	public String getPushNotificationSftpBasePath() {
		return getValue(ConfigValue.PushNotificationSftpBasePath);
	}

	public String getPushNotificationSftpKeyFileName() {
		return getValue(ConfigValue.PushNotificationSftpSshKeyFile);
	}

	public String getPushNotificationEncryptedSftpPassphrase() {
		return getValue(ConfigValue.PushNotificationSftpEncryptedSshKeyPassphrase);
	}
	
	public String getPushNotificationClickTrackingUrl(final int companyID) {
		return getValue(ConfigValue.PushNotificationClickTrackingUrl, companyID);
	}
	
	public String getPushNotificationOpenTrackingUrl(final int companyID) {
		return getValue(ConfigValue.PushNotificationOpenTrackingUrl, companyID);
	}
	
	public final int getPushNotificationMaxRedirectTokenGenerationAttempts(final int companyID) {
		return getIntegerValue(ConfigValue.PushNotificationMaxRedirectTokenGenerationAttempts, companyID);
	}
	
	public final int getPushNotificationMaxRedirectTokenAge() {
		return getIntegerValue(ConfigValue.PushNotificationMaxRedirectTokenAge);
	}
	
	public final int getPushNotificationMaxTrackingIdGenerationAttempts(final int companyID) {
		return getIntegerValue(ConfigValue.PushNotificationMaxTrackingIdGenerationAttempts, companyID);
	}
	
	public static Date getBuildTime() {
		try {
	        URL resource = ConfigService.class.getResource(ConfigService.class.getSimpleName() + ".class");
	        if (resource == null) {
	            throw new IllegalStateException("Failed to find class file for class: " + ConfigService.class.getName());
	        } else if (resource.getProtocol().equals("file")) {
	        	return new Date(new File(resource.toURI()).lastModified());
	        } else if (resource.getProtocol().equals("jar")) {
	            String path = resource.getPath();
	            return new Date(new File(path.substring(5, path.indexOf("!"))).lastModified());
	        } else {
	            throw new IllegalArgumentException("Unhandled url protocol: " + resource.getProtocol() + " for class: " + ConfigService.class.getName() + " resource: " + resource.toString());
	        }
	    } catch (Exception e) {
	    	logger.error("Error in getDeploymentTime: " + e.getMessage(), e);
	        return null;
	    }
	}

	public Date getStartupTime() {
		return STARTUP_TIME == null ? null : (Date) STARTUP_TIME.clone();
	}
	
	public Date getConfigurationExpirationTime() {
		return EXPIRATIONTIME == null ? null : EXPIRATIONTIME.getTime();
	}

	public final String getHostAuthenticationHostIdCookieName() {
		return getValue(ConfigValue.HostAuthenticationHostIdCookieName);
	}
	
	public final int getMaxPendingHostAuthenticationsAgeMinutes() {
		return getIntegerValue(ConfigValue.PendingHostAuthenticationMaxAgeMinutes);
	}

	public final String getLitmusStatusURL(final int companyID) {
		return getValue(ConfigValue.Predelivery_LitmusStatusUrl);
	}

	public final String getFullviewFormName(final int companyID) {
		return getValue(ConfigValue.FullviewFormName, companyID);
	}
	
	public final boolean isSessionHijackingPreventionEnabled() {
		return getBooleanValue(ConfigValue.SessionHijackingPrevention);
	}
	
	public final Map<String, String> getHostSystemProperties() {
		Map<String, String> hostProperties = new HashMap<>();
		Map<String, String> systemProperties = new Systemconfig().get();
		
		if(MapUtils.isNotEmpty(systemProperties)) {
			Map<String, String> hostNames = systemProperties.entrySet().stream()
					.filter(pair -> StringUtils.startsWith(pair.getKey(), "hostname-"))
					.map(ConfigService::createHostProperty)
					.collect(Collectors.toMap(Tuple::getFirst, Tuple::getSecond));
			hostProperties.putAll(hostNames);
		} else {
			hostProperties.put("host.nosystemcfg", "true");
		}
		
		return hostProperties;
	}
	
	private static Tuple<String, String> createHostProperty(Entry<String, String> propertyEntry) {
		String hostFunction = StringUtils.removeStart(propertyEntry.getKey(), "hostname-");
		String hostName = propertyEntry.getValue();
		String propertyName = String.format("host.%s.%s.ping", hostFunction, hostName);
		String reachable = "ERROR";
		try {
			InetAddress address = InetAddress.getByName(hostName);
			reachable = address.isReachable(1000) ? "OK" : "ERROR";
		} catch (IOException e) {
			logger.error("Cannot reach to host " + hostName, e);
		}
		return new Tuple<>(propertyName, reachable);
	}

	public void enforceExpiration() {
		EXPIRATIONTIME = new GregorianCalendar();
	}
}