/*
 * 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.webtop.core.app.util.ClassHelper;
import com.sonicle.commons.LangUtils;
import com.sonicle.webtop.core.sdk.BaseRestApiEndpoint;
import com.sonicle.webtop.core.sdk.BaseController;
import com.sonicle.webtop.core.sdk.BasePublicService;
import com.sonicle.webtop.core.sdk.BaseService;
import com.sonicle.webtop.core.sdk.ServiceManifest;
import com.sonicle.webtop.core.sdk.BaseJobService;
import com.sonicle.webtop.core.sdk.BaseManager;
import com.sonicle.webtop.core.sdk.BaseUserOptionsService;
import com.sonicle.webtop.core.sdk.ServiceVersion;
import com.sonicle.webtop.core.versioning.ResourceNotFoundException;
import com.sonicle.webtop.core.versioning.SqlUpgradeScript;
import com.sonicle.webtop.core.versioning.Whatsnew;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import org.slf4j.Logger;

/**
 *
 * @author malbinola
 */
class ServiceDescriptor {
	private static final Logger logger = WT.getLogger(ServiceDescriptor.class);
	private ServiceManifest manifest = null;
	private Class controllerClass = null;
	private Class managerClass = null;
	private Class privateServiceClass = null;
	private Class publicServiceClass = null;
	private Class jobServiceClass = null;
	private Class userOptionsServiceClass = null;
	private final List<ApiEndpointClass> restApiEndpointClasses = new ArrayList<>();
	private final List<OpenApiDefinition> openApiDefinitions = new ArrayList<>();
	private boolean upgraded = false;
	private final HashMap<String, Whatsnew> whatsnewCache = new HashMap<>();

	public ServiceDescriptor(ServiceManifest manifest) {
		this.manifest = manifest;
		
		controllerClass = ClassHelper.loadClass(true, manifest.getControllerClassName(), BaseController.class, "Controller");
		managerClass = ClassHelper.loadClass(true, manifest.getManagerClassName(), BaseManager.class, "Manager");
		privateServiceClass = ClassHelper.loadClass(true, manifest.getPrivateServiceClassName(), BaseService.class, "Service");
		publicServiceClass = ClassHelper.loadClass(true, manifest.getPublicServiceClassName(), BasePublicService.class, "PublicService");
		jobServiceClass = ClassHelper.loadClass(true, manifest.getJobServiceClassName(), BaseJobService.class, "JobService");
		userOptionsServiceClass = ClassHelper.loadClass(true, manifest.getUserOptionsServiceClassName(), BaseUserOptionsService.class, "UserOptionsService");
		
		for (ServiceManifest.RestApiEndpoint rae : manifest.getApiEndpoints()) {
			final Class clazz = ClassHelper.loadClass(false, rae.className, BaseRestApiEndpoint.class, "RestApiEndpoint");
			if (clazz != null) {
				restApiEndpointClasses.add(new ApiEndpointClass(clazz, sanitizeApiEndpointPath(manifest.getId(), rae.path)));
			}
		}
		for (ServiceManifest.RestApi ra : manifest.getRestApis()) {
			Set<Class<?>> classes = findSubTypesClassesOf(ra.implPackage, com.sonicle.webtop.core.sdk.BaseRestApiResource.class);
			openApiDefinitions.add(new OpenApiDefinition(ra.oasFilePath, ra.context, ra.implPackage, classes));
		}
	}

	public ServiceManifest getManifest() {
		return manifest;
	}
	
	public boolean hasController() {
		return (controllerClass != null);
	}
	
	public Class getControllerClass() {
		return controllerClass;
	}
	
	public boolean doesControllerImplements(Class clazz) {
		if (!hasController()) return false;
		return ClassHelper.isImplementingInterface(controllerClass, clazz);
	}
	
	public boolean hasManager() {
		return (managerClass != null);
	}

	public Class getManagerClass() {
		return managerClass;
	}

	public boolean hasPrivateService() {
		return (privateServiceClass != null);
	}

	public Class getPrivateServiceClass() {
		return privateServiceClass;
	}
	
	public boolean hasUserOptionsService() {
		return (userOptionsServiceClass != null);
	}

	public Class getUserOptionsServiceClass() {
		return userOptionsServiceClass;
	}

	public boolean hasPublicService() {
		return (publicServiceClass != null);
	}

	public Class getPublicServiceClass() {
		return publicServiceClass;
	}

	public boolean hasJobService() {
		return (jobServiceClass != null);
	}

	public Class getJobServiceClass() {
		return jobServiceClass;
	}
	
	public boolean hasRestApiEndpoints() {
		return !restApiEndpointClasses.isEmpty();
	}
	
	public List<ApiEndpointClass> getRestApiEndpoints() {
		return restApiEndpointClasses;
	}
	
	public boolean hasOpenApiDefinitions() {
		return !openApiDefinitions.isEmpty();
	}
	
	public List<OpenApiDefinition> getOpenApiDefinitions() {
		return openApiDefinitions;
	}

	public boolean isUpgraded() {
		return upgraded;
	}

	public void setUpgraded(boolean upgraded) {
		this.upgraded = upgraded;
	}
	
	
	
	/**
	 * Loads what's new file for specified service.
	 * Contents will be taken and interpreted starting from desired version number.
	 * @param serviceId Service ID
	 * @param locale Locale of contents (ex. it_IT)
	 * @param fromVersion Starting version
	 * @return HTML translated representation of loaded file.
	 */
	public String getWhatsnew(Locale locale, ServiceVersion fromVersion) {
		String resName = null;
		Whatsnew wn = null;
		
		synchronized(whatsnewCache) {
			try {
				String slocale = locale.toString();
				if(whatsnewCache.containsKey(slocale)) {
					logger.trace("Getting whatsnew from cache [{}, {}]", manifest.getId(), slocale);
					wn = whatsnewCache.get(slocale);
				} else {
					resName = MessageFormat.format("/{0}/meta/whatsnew/{1}.txt", LangUtils.packageToPath(manifest.getId()), slocale);
					logger.debug("Loading whatsnew [{}, {}, ver. >= {}]", manifest.getId(), resName, fromVersion);
					wn = new Whatsnew(resName, defineWhatsnewVariables());
					whatsnewCache.put(slocale, wn);
				}
				return wn.toHtml(fromVersion, manifest.getVersion());

			} catch(ResourceNotFoundException ex) {
				logger.trace("Whatsnew file not available for service [{}]", manifest.getId());
			} catch(IOException ex) {
				logger.trace(ex.getMessage());
			} catch(Exception ex) {
				logger.error("Error getting whatsnew for service {}", manifest.getId(), ex);
			}
			return StringUtils.EMPTY;
		}
	}
	
	public ArrayList<SqlUpgradeScript> getUpgradeScripts() {
		ArrayList<SqlUpgradeScript> scripts = new ArrayList<>();
		String resName = null;
		
		try {
			String pkgPath = MessageFormat.format("{0}/meta/db/", LangUtils.packageToPath(manifest.getId()));
			ServiceVersion fileVersion = null;
			// List all .sql files in dedicated package and defines the right set
			for(String file: LangUtils.listPackageFiles(getClass(), pkgPath)) {
				try {
					if (StringUtils.startsWithIgnoreCase(file, "init")) continue;
					fileVersion = SqlUpgradeScript.extractVersion(file);
					if (fileVersion.compareTo(manifest.getOldVersion()) <= 0) continue; // Skip all version sections below oldVersion (included)
					if (fileVersion.compareTo(manifest.getVersion()) > 0) continue; // Skip all version sections after manifestVersion
					
					resName = "/" + pkgPath + file;
					logger.debug("Reading upgrade script [{}]", resName);
					scripts.add(new SqlUpgradeScript(resName));
					
				} catch(IOException | UnsupportedOperationException ex1) {
					logger.warn(ex1.getMessage());
					//TODO: increment error counter...
				}
			}
			
			// Filename ordering may not be valid, so reorder the set
			// using the version information
			Collections.sort(scripts, new Comparator<SqlUpgradeScript>() {
				@Override
				public int compare(SqlUpgradeScript o1, SqlUpgradeScript o2) {
					return o1.getFileVersion().compareTo(o2.getFileVersion());
				}
			});
			
		} catch(Exception ex) {
			logger.error("Error loading upgrade scripts", ex);
		}
		//TODO: return also error counter...
		return scripts;
	}
	
	public Info buildOpenApiInfo(OpenApiDefinition apiDefinition) {
		String title = WT.lookupResource(manifest.getId(), WT.LOCALE_ENGLISH, BaseService.RESOURCE_SERVICE_NAME);
		Contact contact = null;
		if (!StringUtils.isBlank(manifest.getSupportEmail())) {
			new Contact().email(manifest.getSupportEmail());
		}
		
		return new Info()
				.title(title + " Rest API")
				.version("v1")
				.contact(contact);
	}
	
	private String sanitizeApiEndpointPath(String serviceId, String path) {
		if (StringUtils.isBlank(path)) {
			return serviceId;
		} else {
			return serviceId + "/" + StringUtils.removeStart(path, "/");
		}
	}
	
	private HashMap<String, String> defineWhatsnewVariables() {
		HashMap<String, String> variables = new HashMap<>();
		// Defines variables to place substitutions in whatsnew html
		variables.put("WHATSNEW_URL", MessageFormat.format("resources/{0}/whatsnew", manifest.getJarPath()));
		return variables;
	}
	
	private Set<Class<?>> findSubTypesClassesOf(String targetPackage, Class subTypeOf) {
		Set<Class<?>> output = new HashSet<>();
		
		Reflections reflections = new Reflections(new ConfigurationBuilder()
				.setUrls(ClasspathHelper.forPackage(targetPackage))
				.setScanners(new SubTypesScanner())
				.filterInputsBy(new FilterBuilder().includePackage(targetPackage)));
		
		Set<Class<?>> classes = reflections.getSubTypesOf(subTypeOf);
		for (Class<?> clazz : classes) {
			if (clazz.getPackage().getName().startsWith(targetPackage)) {
				output.add(clazz);
			}
		}
		return output;
	}
	
	
	public static class ApiEndpointClass {
		public final Class clazz;
		public final String path;
		
		public ApiEndpointClass(Class clazz, String path) {
			this.clazz = clazz;
			this.path = path;
		}
	}
	
	public static class OpenApiDefinition {
		public final String oasFile;
		public final String context;
		public final String implPackage;
		public final Set<Class<?>> resourceClasses;
		
		public OpenApiDefinition(String oasFile, String context, String implPackage, Set<Class<?>> resourceClasses) {
			this.oasFile = oasFile;
			this.context = context;
			this.implPackage = implPackage;
			this.resourceClasses = resourceClasses;
		}
	}
}
