/*
 * Copyright 2013 Robert von Burg <eitch@eitchnet.ch>
 *
 * Licensed 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 li.strolch.agent.api;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import li.strolch.agent.impl.ComponentContainerImpl;
import li.strolch.exception.StrolchException;
import li.strolch.model.Locator;
import li.strolch.persistence.api.StrolchTransaction;
import li.strolch.privilege.model.Certificate;
import li.strolch.runtime.configuration.ComponentConfiguration;
import li.strolch.runtime.configuration.RuntimeConfiguration;
import li.strolch.runtime.configuration.StrolchConfiguration;
import li.strolch.runtime.privilege.PrivilegeHandler;
import li.strolch.runtime.privilege.PrivilegedRunnable;
import li.strolch.runtime.privilege.PrivilegedRunnableWithResult;
import li.strolch.utils.ExecutorPool;
import li.strolch.utils.concurrent.ElementLockingHandler;
import li.strolch.utils.helper.StringHelper;
import li.strolch.utils.helper.SystemHelper;
import li.strolch.utils.iso8601.ISO8601;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.management.*;
import java.text.MessageFormat;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import static li.strolch.model.Tags.Json.*;
import static li.strolch.runtime.configuration.ConfigurationParser.parseConfiguration;

/**
 * @author Robert von Burg <eitch@eitchnet.ch>
 */
public class StrolchAgent {

	public static final String AGENT_VERSION_PROPERTIES = "/agentVersion.properties";

	public static final String PROP_TRY_LOCK_TIME_UNIT = "tryLockTimeUnit";
	public static final String PROP_TRY_LOCK_TIME = "tryLockTime";

	private static final Logger logger = LoggerFactory.getLogger(StrolchAgent.class);

	private final StrolchVersion appVersion;

	private ComponentContainerImpl container;
	private StrolchConfiguration strolchConfiguration;

	private ExecutorPool executorPool;
	private ElementLockingHandler<Locator> lockHandler;

	private JsonObject systemState;
	private long systemStateUpdateTime;

	private VersionQueryResult versionQueryResult;

	public StrolchAgent(StrolchVersion appVersion) {
		this.appVersion = appVersion;
	}

	public StrolchConfiguration getStrolchConfiguration() {
		return this.strolchConfiguration;
	}

	public RuntimeConfiguration getRuntimeConfiguration() {
		return this.strolchConfiguration.getRuntimeConfiguration();
	}

	public ComponentContainer getContainer() {
		return this.container;
	}

	/**
	 * @see ComponentContainer#hasComponent(Class)
	 */
	public boolean hasComponent(Class<?> clazz) {
		return this.container.hasComponent(clazz);
	}

	/**
	 * @see ComponentContainer#getComponent(Class)
	 */
	public <T> T getComponent(Class<T> clazz) throws IllegalArgumentException {
		return this.container.getComponent(clazz);
	}

	/**
	 * @see ComponentContainer#getComponentByName(String)
	 */
	public <T extends StrolchComponent> T getComponentByName(String name) {
		return getContainer().getComponentByName(name);
	}

	/**
	 * @see ComponentContainer#getComponentsOrderedByRoot()
	 */
	public List<StrolchComponent> getComponentsOrderedByRoot() {
		return this.container.getComponentsOrderedByRoot();
	}

	public PrivilegeHandler getPrivilegeHandler() throws IllegalArgumentException {
		return this.container.getPrivilegeHandler();
	}

	/**
	 * @see ComponentContainer#getRealm(String)
	 */
	public StrolchRealm getRealm(String realm) throws StrolchException {
		return getComponent(RealmHandler.class).getRealm(realm);
	}

	/**
	 * @see ComponentContainer#getRealm(Certificate)
	 */
	public StrolchRealm getRealm(Certificate certificate) throws StrolchException {
		return this.container.getRealm(certificate);
	}

	/**
	 * @see StrolchRealm#openTx(Certificate, Class, boolean)
	 */
	public StrolchTransaction openTx(Certificate certificate, Class<?> clazz, boolean readOnly) {
		return this.container.getRealm(certificate).openTx(certificate, clazz, readOnly);
	}

	/**
	 * @see StrolchRealm#openTx(Certificate, String, boolean)
	 */
	public StrolchTransaction openTx(Certificate certificate, String action, boolean readOnly) {
		return this.container.getRealm(certificate).openTx(certificate, action, readOnly);
	}

	/**
	 * @see PrivilegeHandler#runAs(String, PrivilegedRunnable)
	 */
	public void runAs(String systemUser, PrivilegedRunnable runnable) throws Exception {
		getPrivilegeHandler().runAs(systemUser, runnable);
	}

	/**
	 * @see PrivilegeHandler#runAsAgent(PrivilegedRunnable)
	 */
	public void runAsAgent(PrivilegedRunnable runnable) throws Exception {
		getPrivilegeHandler().runAsAgent(runnable);
	}

	/**
	 * @see PrivilegeHandler#runAsAgentWithResult(PrivilegedRunnableWithResult)
	 */
	public <T> T runAsWithResult(String systemUser, PrivilegedRunnableWithResult<T> runnable) throws Exception {
		return getPrivilegeHandler().runAsWithResult(systemUser, runnable);
	}

	/**
	 * @see PrivilegeHandler#runAsAgentWithResult(PrivilegedRunnableWithResult)
	 */
	public <T> T runAsAgentWithResult(PrivilegedRunnableWithResult<T> runnable) throws Exception {
		return getPrivilegeHandler().runAsAgentWithResult(runnable);
	}

	/**
	 * @return the name of this application as is defined in the configuration
	 */
	public String getApplicationName() {
		return this.strolchConfiguration.getRuntimeConfiguration().getApplicationName();
	}

	/**
	 * @return the currently loaded environment of this application as is defined in the configuration
	 */
	public String getEnvironment() {
		return this.strolchConfiguration.getRuntimeConfiguration().getEnvironment();
	}

	/**
	 * @return the agent's {@link Locale}
	 */
	public Locale getLocale() {
		return this.strolchConfiguration.getRuntimeConfiguration().getLocale();
	}

	/**
	 * @return the agent's time zone
	 */
	public String getTimezone() {
		return this.strolchConfiguration.getRuntimeConfiguration().getTimezone();
	}

	/**
	 * Return the {@link ExecutorService} instantiated for this agent
	 *
	 * @return the {@link ExecutorService} instantiated for this agent
	 */
	public ExecutorService getExecutor() {
		return getExecutor("Agent");
	}

	public ExecutorService getExecutor(String poolName) {
		return this.executorPool.getExecutor(poolName);
	}

	/**
	 * Return the {@link ExecutorService} instantiated for this agent
	 *
	 * @return the {@link ExecutorService} instantiated for this agent
	 */
	public ExecutorService getSingleThreadExecutor() {
		return getSingleThreadExecutor("Agent");
	}

	public ExecutorService getSingleThreadExecutor(String poolName) {
		return this.executorPool.getSingleThreadExecutor(poolName);
	}

	/**
	 * Return the {@link ScheduledExecutorService} instantiated for this agent
	 *
	 * @return the {@link ScheduledExecutorService} instantiated for this agent
	 */
	public ScheduledExecutorService getScheduledExecutor() {
		return getScheduledExecutor("Agent");
	}

	public ScheduledExecutorService getScheduledExecutor(String poolName) {
		return this.executorPool.getScheduledExecutor(poolName);
	}

	public ElementLockingHandler<Locator> getLockHandler() {
		return this.lockHandler;
	}

	/**
	 * Initializes the underlying container and prepares the executor services. Before calling this method,
	 * {@link #setup(String, File, File, File)} must have ben called
	 */
	public void initialize() {
		if (this.container == null)
			throw new RuntimeException("Please call setup first!");

		this.executorPool = new ExecutorPool();

		RuntimeConfiguration configuration = this.strolchConfiguration.getRuntimeConfiguration();
		TimeUnit timeUnit = TimeUnit.valueOf(configuration.getString(PROP_TRY_LOCK_TIME_UNIT, TimeUnit.SECONDS.name()));
		long time = configuration.getLong(PROP_TRY_LOCK_TIME, 10L);
		this.lockHandler = new ElementLockingHandler<>(getScheduledExecutor(), timeUnit, time);
		this.container.initialize();
	}

	/**
	 * Starts the container
	 */
	public void start() {
		if (this.container == null)
			throw new RuntimeException("Please call setup first!");
		this.lockHandler.start();
		this.container.start();
	}

	/**
	 * Stops the container
	 */
	public void stop() {
		if (this.container != null)
			this.container.stop();
		if (this.lockHandler != null)
			this.lockHandler.stop();
	}

	/**
	 * Destroys the container and the executor services
	 */
	public void destroy() {

		if (this.executorPool != null)
			this.executorPool.destroy();

		if (this.container != null)
			this.container.destroy();
		this.container = null;
	}

	/**
	 * <p>
	 * <b>Note:</b> Use {@link StrolchBootstrapper} instead of calling this method directly!
	 * </p>
	 *
	 * <p>
	 * Sets up the agent by parsing the configuration file and initializes the given environment
	 * </p>
	 *
	 * @param environment the current environment
	 * @param configPathF the path to the config directory
	 * @param dataPathF   the path to the data directory
	 * @param tempPathF   the path to the temp directory
	 */
	void setup(String environment, File configPathF, File dataPathF, File tempPathF) {

		String msg = "[{0}] Setting up Strolch Container using the following paths:";
		logger.info(MessageFormat.format(msg, environment));
		logger.info(" - Config: " + configPathF.getAbsolutePath());
		logger.info(" - Data: " + dataPathF.getAbsolutePath());
		logger.info(" - Temp: " + tempPathF.getAbsolutePath());
		logger.info(" - user.dir: " + SystemHelper.getUserDir());

		this.strolchConfiguration = parseConfiguration(environment, configPathF, dataPathF, tempPathF);

		ComponentContainerImpl container = new ComponentContainerImpl(this);
		container.setup(this.strolchConfiguration);

		this.container = container;

		RuntimeConfiguration config = this.strolchConfiguration.getRuntimeConfiguration();
		logger.info(MessageFormat.format("Setup Agent {0}:{1}", config.getApplicationName(), config.getEnvironment()));
	}

	protected void assertContainerStarted() {
		if (this.container == null || this.container.getState() != ComponentState.STARTED) {
			String msg = "Container is not yet started!";
			throw new IllegalStateException(msg);
		}
	}

	/**
	 * @return Returns the pseudo unique Id to be used during object creation from external services.
	 */
	public static String getUniqueId() {
		return StringHelper.getUniqueId();
	}

	/**
	 * @return Returns the pseudo unique Id to be used during object creation from external services.
	 */
	public static Long getUniqueIdLong() {
		return StringHelper.getUniqueIdLong();
	}

	/**
	 * Returns the version of this agent
	 *
	 * @return the version of this agent
	 */
	public VersionQueryResult getVersion() {
		if (this.versionQueryResult == null) {

			VersionQueryResult queryResult = new VersionQueryResult();
			queryResult.setAppVersion(this.appVersion);

			Properties properties = new Properties();

			try (InputStream stream = getClass().getResourceAsStream(AGENT_VERSION_PROPERTIES)) {
				properties.load(stream);
				RuntimeConfiguration runtimeConfiguration = getStrolchConfiguration().getRuntimeConfiguration();
				AgentVersion agentVersion = new AgentVersion(runtimeConfiguration.getApplicationName(),
						runtimeConfiguration.getEnvironment(), runtimeConfiguration.getLocale(),
						runtimeConfiguration.getTimezone(), properties);
				queryResult.setAgentVersion(agentVersion);
			} catch (IOException e) {
				String msg = MessageFormat.format("Failed to read version properties for agent: {0}", e.getMessage());
				queryResult.getErrors().add(msg);
				logger.error(msg, e);
			}

			Set<Class<?>> componentTypes = this.container.getComponentTypes();
			for (Class<?> componentType : componentTypes) {
				StrolchComponent component = (StrolchComponent) this.container.getComponent(componentType);
				try {
					ComponentVersion componentVersion = component.getVersion();
					queryResult.add(componentVersion);
				} catch (Exception e) {
					String msg = "Failed to read version properties for component {0} due to: {1}";
					msg = MessageFormat.format(msg, component.getName(), e.getMessage());
					queryResult.getErrors().add(msg);
					logger.error(msg, e);
				}
			}

			this.versionQueryResult = queryResult;
		}

		return this.versionQueryResult;
	}

	public JsonObject getSystemState(long updateInterval, TimeUnit updateIntervalUnit) {

		if (this.systemState == null
				|| System.currentTimeMillis() - this.systemStateUpdateTime > updateIntervalUnit.toMillis(
				updateInterval)) {
			this.systemState = new JsonObject();

			JsonObject osJ = new JsonObject();
			this.systemState.add(OPERATING_SYSTEM, osJ);
			osJ.addProperty(OS_NAME, SystemHelper.osName);
			osJ.addProperty(OS_ARCH, SystemHelper.osArch);
			osJ.addProperty(OS_VERSION, SystemHelper.osVersion);
			osJ.addProperty(JAVA_VENDOR, SystemHelper.javaVendor);
			osJ.addProperty(JAVA_VERSION, SystemHelper.javaVersion);

			OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
			osJ.addProperty(AVAILABLE_PROCESSORS, osMXBean.getAvailableProcessors());
			osJ.addProperty(SYSTEM_LOAD_AVERAGE, osMXBean.getSystemLoadAverage());

			RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
			osJ.addProperty(START_TIME, ISO8601.toString(runtimeMXBean.getStartTime()));
			osJ.addProperty(UPTIME, runtimeMXBean.getUptime());

			// memory
			JsonObject memoryJ = new JsonObject();
			this.systemState.add(MEMORY, memoryJ);

			if (osMXBean instanceof com.sun.management.OperatingSystemMXBean os) {
				memoryJ.addProperty(TOTAL_PHYSICAL_MEMORY_SIZE, os.getTotalMemorySize());
				memoryJ.addProperty(FREE_PHYSICAL_MEMORY_SIZE, os.getFreeMemorySize());
				memoryJ.addProperty(FREE_SWAP_SPACE_SIZE, os.getFreeSwapSpaceSize());
				memoryJ.addProperty(COMMITTED_VIRTUAL_MEMORY_SIZE, os.getCommittedVirtualMemorySize());
			}

			MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
			MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
			memoryJ.addProperty(HEAP_MEMORY_USAGE_INIT, heapMemoryUsage.getInit());
			memoryJ.addProperty(HEAP_MEMORY_USAGE_USED, heapMemoryUsage.getUsed());
			memoryJ.addProperty(HEAP_MEMORY_USAGE_MAX, heapMemoryUsage.getMax());
			memoryJ.addProperty(HEAP_MEMORY_USAGE_COMMITTED, heapMemoryUsage.getCommitted());

			// disk space
			JsonArray rootsJ = new JsonArray();
			this.systemState.add(ROOTS, rootsJ);
			File[] roots = File.listRoots();
			for (File root : roots) {
				JsonObject rootJ = new JsonObject();
				rootsJ.add(rootJ);
				rootJ.addProperty(PATH, root.getAbsolutePath());
				rootJ.addProperty(USABLE_SPACE, root.getUsableSpace());
				rootJ.addProperty(USED_SPACE, root.getTotalSpace() - root.getFreeSpace());
				rootJ.addProperty(FREE_SPACE, root.getFreeSpace());
				rootJ.addProperty(TOTAL_SPACE, root.getTotalSpace());
			}

			this.systemStateUpdateTime = System.currentTimeMillis();
		}

		return this.systemState;
	}

	public void reloadStrolchConfiguration() {
		RuntimeConfiguration runtimeConfig = this.strolchConfiguration.getRuntimeConfiguration();
		File configPathF = runtimeConfig.getConfigPath();
		File dataPathF = runtimeConfig.getDataPath();
		File tempPathF = runtimeConfig.getTempPath();
		StrolchConfiguration newConfig = parseConfiguration(runtimeConfig.getEnvironment(), configPathF, dataPathF,
				tempPathF);

		for (String name : this.container.getComponentNames()) {
			ComponentConfiguration newComponentConfig = newConfig.getComponentConfiguration(name);
			StrolchComponent existingComponent = this.container.getComponentByName(name);
			ComponentConfiguration existingComponentConfiguration = existingComponent.getConfiguration();
			existingComponentConfiguration.updateProperties(newComponentConfig.getAsMap());
		}

		RuntimeConfiguration newRuntimeConfiguration = newConfig.getRuntimeConfiguration();
		runtimeConfig.updateProperties(newRuntimeConfiguration.getAsMap());
		runtimeConfig.setLocale(newRuntimeConfiguration.getLocale());
		runtimeConfig.setSupportedLanguages(newRuntimeConfiguration.getSupportedLanguages());
	}

	public JsonObject toJson() {
		JsonObject agentJ = getStrolchConfiguration().getRuntimeConfiguration().toJson();

		JsonArray componentsJ = new JsonArray();
		List<StrolchComponent> components = getComponentsOrderedByRoot();
		for (StrolchComponent component : components) {
			componentsJ.add(component.toJson());
		}
		agentJ.add(COMPONENTS, componentsJ);

		return agentJ;
	}
}
