package eu.dnetlib.enabling.datasources;

import java.io.StringReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import javax.xml.ws.wsaddressing.W3CEndpointReference;

import org.antlr.stringtemplate.StringTemplate;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.dom4j.io.SAXReader;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import eu.dnetlib.enabling.database.rmi.DatabaseService;
import eu.dnetlib.enabling.datasources.rmi.DatasourceDesc;
import eu.dnetlib.enabling.datasources.rmi.DatasourceManagerService;
import eu.dnetlib.enabling.datasources.rmi.DatasourceManagerServiceException;
import eu.dnetlib.enabling.datasources.rmi.IfaceDesc;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpService;
import eu.dnetlib.enabling.is.registry.rmi.ISRegistryService;
import eu.dnetlib.enabling.resultset.IterableResultSetFactory;
import eu.dnetlib.enabling.resultset.client.ResultSetClientFactory;
import eu.dnetlib.enabling.tools.AbstractBaseService;
import eu.dnetlib.enabling.tools.ServiceLocator;
import eu.dnetlib.miscutils.functional.UnaryFunction;
import eu.dnetlib.miscutils.functional.xml.ApplyXslt;

public class DatasourceManagerServiceImpl extends AbstractBaseService implements DatasourceManagerService {

	private String db;

	private Resource xslt = new ClassPathResource("/eu/dnetlib/enabling/datasources/repo_2_is.xslt");

	private ResultSetClientFactory resultSetClientFactory;
	private IterableResultSetFactory iterableResultSetFactory;

	private ServiceLocator<DatabaseService> dbServiceLocator;
	private ServiceLocator<ISRegistryService> registryLocator;
	private ServiceLocator<ISLookUpService> lookupLocator;

	private static final Log log = LogFactory.getLog(DatasourceManagerServiceImpl.class);
	private static final String REPOSITORY_SERVICE_RESOURCE_TYPE = "RepositoryServiceResourceType";
	private static final String TMPLS_BASEDIR = "/eu/dnetlib/enabling/datasources/";

	@Override
	public boolean addDatasource(final DatasourceDesc ds) throws DatasourceManagerServiceException {
		final Map<String, Object> params = DatasourceFunctions.asMapOfSqlValues(ds);

		if ((ds.getOrganization() != null) && !ds.getOrganization().trim().isEmpty()) {
			params.put("hasOrganization", 1);
		}
		return updateSQL(ds.getId(), "addDatasource.sql.st", params, false);
	}

	@Override
	public boolean deleteDatasource(final String dsId) throws DatasourceManagerServiceException {
		final Map<String, Object> params = Maps.newHashMap();

		params.put("dsId", DatasourceFunctions.asSqlValue(dsId));

		return updateSQL(dsId, "deleteDatasource.sql.st", params, true);
	}

	@Override
	public DatasourceDesc getDatasource(final String dsId) throws DatasourceManagerServiceException {
		final List<DatasourceDesc> list = getDatasourcesByCondition("ds.id = " + DatasourceFunctions.asSqlValue(dsId));
		if (list.size() != 1) { throw new DatasourceManagerServiceException("Datasource not found, id=" + dsId); }
		return list.get(0);
	}

	private List<DatasourceDesc> getDatasourcesByCondition(final String condition) throws DatasourceManagerServiceException {
		final SAXReader reader = new SAXReader();
		final List<DatasourceDesc> list = Lists.newArrayList();
		try {
			for (String s : getXmlDatasourcesByCondition(condition)) {
				final Document doc = reader.read(new StringReader(s));
				list.add(DatasourceFunctions.xmlToDatasourceDesc(doc));
			}
		} catch (Exception e) {
			throw new DatasourceManagerServiceException("Error obtaining datasources from db", e);
		}
		return list;

	}

	private List<String> getTransformedDatasourcesByCondition(final String condition, final UnaryFunction<String, String> function)
			throws DatasourceManagerServiceException {
		final List<String> list = Lists.newArrayList();
		try {
			for (String s : getXmlDatasourcesByCondition(condition)) {
				list.add(function.evaluate(s));
			}
		} catch (Exception e) {
			throw new DatasourceManagerServiceException("Error obtaining datasources from db", e);
		}
		return list;
	}

	private Iterable<String> getXmlDatasourcesByCondition(final String condition) throws DatasourceManagerServiceException {
		try {
			final Map<String, Object> params = Maps.newHashMap();

			if ((condition != null) && !condition.trim().isEmpty()) {
				params.put("condition", condition);
			}
			return searchSQL("getDatasources.sql.st", params);
		} catch (Exception e) {
			throw new DatasourceManagerServiceException("Error obtaining datasources from db", e);
		}
	}

	private Iterable<String> searchSQL(final String sqlTemplate, final Map<String, Object> params) throws DatasourceManagerServiceException {
		try {
			final Resource resource = new ClassPathResource(TMPLS_BASEDIR + sqlTemplate);
			final StringTemplate st = new StringTemplate(IOUtils.toString(resource.getInputStream()));

			st.setAttributes(params);

			final String sql = st.toString();

			log.info("Executing SQL: " + sql);

			final W3CEndpointReference epr = dbServiceLocator.getService().searchSQL(getDb(), sql);

			return resultSetClientFactory.getClient(epr);
		} catch (Exception e) {
			log.error("Error executing sql", e);

			throw new DatasourceManagerServiceException("Error obtaining datasources from db", e);
		}
	}

	@Override
	public List<DatasourceDesc> listAllDatasources() throws DatasourceManagerServiceException {
		return getDatasourcesByCondition(null);
	}

	@Override
	public List<DatasourceDesc> listDatasourcesUsingFilter(final String compliance, final String contentDescription, final String iisProcessingWorkflow)
			throws DatasourceManagerServiceException {

		String cond = "";

		if ((compliance != null) && !compliance.isEmpty()) {
			if (!cond.isEmpty()) {
				cond += " and ";
			}
			cond += "ag.compatibilityclass=" + DatasourceFunctions.asSqlValue(compliance);
		}

		if ((contentDescription != null) && !contentDescription.isEmpty()) {
			if (!cond.isEmpty()) {
				cond += " and ";
			}
			cond += "ag.contentdescriptionclass=" + DatasourceFunctions.asSqlValue(contentDescription);
		}

		if ((iisProcessingWorkflow != null) && !iisProcessingWorkflow.isEmpty()) {
			if (!cond.isEmpty()) {
				cond += " and ";
			}
			cond += "ag.accessinfopackage LIKE '%###" + DatasourceParams.iis_processing_workflow + "###" + iisProcessingWorkflow + "###%'";
		}

		return getDatasourcesByCondition(cond);
	}

	@Override
	public boolean updateActivationStatus(final String dsId, final String ifaceId, final boolean active) throws DatasourceManagerServiceException {
		final Map<String, Object> params = Maps.newHashMap();

		params.put("active", DatasourceFunctions.asSqlValue(active));
		params.put("ifaceId", DatasourceFunctions.asSqlValue(ifaceId));
		params.put("dsId", DatasourceFunctions.asSqlValue(dsId));

		return updateSQL(dsId, "updateActivationStatus.sql.st", params, false);
	}

	@Override
	public boolean updateLevelOfCompliance(final String dsId, final String ifaceId, final String level) throws DatasourceManagerServiceException {
		final Map<String, Object> params = Maps.newHashMap();

		params.put("level", DatasourceFunctions.asSqlValue(level));
		params.put("ifaceId", DatasourceFunctions.asSqlValue(ifaceId));
		params.put("dsId", DatasourceFunctions.asSqlValue(dsId));

		return updateSQL(dsId, "updateLevelOfCompliance.sql.st", params, false);
	}

	@Override
	public boolean updateBaseUrl(final String dsId, final String ifaceId, final String baseUrl) throws DatasourceManagerServiceException {
		return updateAccessParam(dsId, ifaceId, DatasourceParams.BASEURL.toString(), baseUrl, true);
	}

	@Override
	public boolean updateContentDescription(final String dsId, final String ifaceId, final String desc) throws DatasourceManagerServiceException {
		final Map<String, Object> params = Maps.newHashMap();
		params.put("desc", DatasourceFunctions.asSqlValue(desc));
		params.put("ifaceId", DatasourceFunctions.asSqlValue(ifaceId));
		params.put("dsId", DatasourceFunctions.asSqlValue(dsId));

		return updateSQL(dsId, "updateContentDescription.sql.st", params, false);
	}

	@Override
	public boolean setIisProcessingWorkflow(final String dsId, final String ifaceId, final String wf) throws DatasourceManagerServiceException {
		return updateExtraField(dsId, ifaceId, DatasourceParams.iis_processing_workflow.toString(), wf, false);
	}

	@Override
	public boolean addInterface(final String dsId, final IfaceDesc iface) throws DatasourceManagerServiceException {
		final Map<String, Object> params = Maps.newHashMap();

		params.put("datasource", DatasourceFunctions.asSqlValue(dsId));
		params.put("id", DatasourceFunctions.asSqlValue(iface.getId()));
		params.put("typology", DatasourceFunctions.asSqlValue(iface.getTypology()));
		params.put("protocol", DatasourceFunctions.asSqlValue(iface.getAccessProtocol()));
		params.put("baseUrl", DatasourceFunctions.asSqlValue(iface.getBaseUrl()));
		params.put("description", DatasourceFunctions.asSqlValue(iface.getContentDescription()));
		params.put("compliance", DatasourceFunctions.asSqlValue(iface.getCompliance()));

		final Map<String, String> accessParams = new HashMap<String, String>();
		if (iface.getAccessParams() != null) {
			for (Entry<String, String> e : iface.getAccessParams().entrySet()) {
				accessParams.put(DatasourceFunctions.asSqlValue(e.getKey()), DatasourceFunctions.asSqlValue(e.getValue()));
			}
		}
		params.put("accessParams", accessParams);
		if (accessParams.containsKey(DatasourceParams.FORMAT.toString())) {
			params.put("format", accessParams.get(DatasourceParams.FORMAT.toString()));
		}
		if (accessParams.containsKey(DatasourceParams.SET.toString())) {
			params.put("set", accessParams.get(DatasourceParams.SET.toString()));
		}

		final Map<String, String> extraFields = new HashMap<String, String>();
		if (iface.getExtraFields() != null) {
			for (Entry<String, String> e : iface.getExtraFields().entrySet()) {
				extraFields.put(DatasourceFunctions.asSqlValue(e.getKey()), DatasourceFunctions.asSqlValue(e.getValue()));
			}
		}

		params.put("extraFields", extraFields);

		return updateSQL(dsId, "insertInterface.sql.st", params, false);
	}

	@Override
	public boolean deleteInterface(final String dsId, final String ifcId) throws DatasourceManagerServiceException {
		final Map<String, Object> params = Maps.newHashMap();

		params.put("datasource", DatasourceFunctions.asSqlValue(dsId));
		params.put("id", DatasourceFunctions.asSqlValue(ifcId));

		return updateSQL(dsId, "deleteInterface.sql.st", params, false);
	}

	@Override
	public boolean updateExtraField(final String dsId, final String ifaceId, final String field, final String value, final boolean preserveOriginal)
			throws DatasourceManagerServiceException {
		return updateApiCollectionsRow(dsId, ifaceId, field, value, preserveOriginal, false);
	}

	@Override
	public boolean updateAccessParam(final String dsId, final String ifaceId, final String field, final String value, final boolean preserveOriginal)
			throws DatasourceManagerServiceException {
		return updateApiCollectionsRow(dsId, ifaceId, field, value, preserveOriginal, true);
	}

	private boolean updateApiCollectionsRow(final String dsId,
			final String ifaceId,
			final String field,
			final String value,
			final boolean preserveOriginal,
			final boolean accessParam) throws DatasourceManagerServiceException {

		final Map<String, Object> params = Maps.newHashMap();

		params.put("dsId", DatasourceFunctions.asSqlValue(dsId));
		params.put("ifaceId", DatasourceFunctions.asSqlValue(ifaceId));
		params.put("field", DatasourceFunctions.asSqlValue(field));
		params.put("accessParam", accessParam);

		if (value != null) {
			params.put("value", DatasourceFunctions.asSqlValue(value));
		}

		if (isExtraFieldDefined(ifaceId, field)) {
			params.put("update", 1);
		}
		if (preserveOriginal) {
			params.put("preserveOriginal", 1);
		}

		return updateSQL(dsId, "updateApiCollectionsRow.sql.st", params, false);
	}

	private boolean isExtraFieldDefined(final String ifaceId, final String field) throws DatasourceManagerServiceException {
		final Map<String, Object> params = Maps.newHashMap();
		params.put("ifaceId", DatasourceFunctions.asSqlValue(ifaceId));
		params.put("field", DatasourceFunctions.asSqlValue(field));

		final List<String> list = Lists.newArrayList(searchSQL("searchExtraField.sql.st", params));

		return !list.isEmpty();
	}

	@Override
	public boolean deleteAccessParamOrExtraField(final String dsId, final String ifaceId, final String field) throws DatasourceManagerServiceException {
		final Map<String, Object> params = Maps.newHashMap();

		params.put("dsId", DatasourceFunctions.asSqlValue(dsId));
		params.put("ifaceId", DatasourceFunctions.asSqlValue(ifaceId));
		params.put("field", DatasourceFunctions.asSqlValue(field));

		return updateSQL(dsId, "deleteApiCollectionsRow.sql.st", params, false);
	}

	private boolean regenerateProfile(final String dsId) throws DatasourceManagerServiceException {
		verifyParams(dsId);

		try {
			final SAXReader reader = new SAXReader();

			final List<String> list = getTransformedDatasourcesByCondition("ds.id= '" + dsId + "'", new ApplyXslt(xslt));

			if (list.size() != 1) { throw new DatasourceManagerServiceException("Illegal number of datasource with id " + dsId + ", size: " + list.size()); }

			final Document doc = reader.read(new StringReader(list.get(0)));

			final int nActiveInterfaces = doc.selectNodes("//INTERFACE[@active='true']").size();

			final String profile = getDatasourceProfile(dsId);

			if (profile != null) {
				final Document docOld = reader.read(new StringReader(profile));
				final String profId = docOld.valueOf("//RESOURCE_IDENTIFIER/@value");
				final String profKind = docOld.valueOf("//RESOURCE_KIND/@value");
				doc.selectSingleNode("//RESOURCE_IDENTIFIER/@value").setText(profId);
				doc.selectSingleNode("//RESOURCE_KIND/@value").setText(profKind);
				registryLocator.getService().updateProfile(profId, doc.asXML(), REPOSITORY_SERVICE_RESOURCE_TYPE);
				log.info("Profile " + profId + " UPDATED for ds " + dsId);

				if (DatasourceFunctions.verifyCompliance(doc) && profKind.equals("PendingRepositoryResources")) {
					registryLocator.getService().validateProfile(profId);
				}

				if (!DatasourceFunctions.verifyCompliance(doc) && profKind.equals("RepositoryServiceResources")) {
					if (nActiveInterfaces == 0) {
						registryLocator.getService().invalidateProfile(profId);
					} else {
						throw new DatasourceManagerServiceException("I cannot invalidate a datasource with active interfaces");
					}
				}
			} else {
				if (DatasourceFunctions.verifyCompliance(doc) || (nActiveInterfaces > 0)) {
					if (!DatasourceFunctions.verifyCompliance(doc)) { throw new DatasourceManagerServiceException(
							"I cannot register an invalide datasource with active interfaces"); }

					final String profId = registryLocator.getService().registerProfile(doc.asXML());
					log.info("Valid Profile " + profId + " REGISTERED for ds " + dsId);
				} else {
					final String profId = registryLocator.getService().insertProfileForValidation(REPOSITORY_SERVICE_RESOURCE_TYPE, doc.asXML());
					log.info("Pending Profile " + profId + " REGISTERED for ds " + dsId);
				}
			}
			return true;
		} catch (Exception e) {
			throw new DatasourceManagerServiceException("Error regenerating profile", e);
		}
	}

	private boolean deleteProfile(final String dsId) throws DatasourceManagerServiceException {
		verifyParams(dsId);

		try {
			final SAXReader reader = new SAXReader();

			final String profile = getDatasourceProfile(dsId);

			if (profile != null) {
				final Document docOld = reader.read(new StringReader(profile));
				final String profId = docOld.valueOf("//RESOURCE_IDENTIFIER/@value");
				registryLocator.getService().deleteProfile(profId);
			}
			return true;
		} catch (Exception e) {
			throw new DatasourceManagerServiceException("Error deleting profile", e);
		}
	}

	@Override
	public boolean updateSQL(final String dsId, final String sql, final boolean delete) throws DatasourceManagerServiceException {
		log.info("Executing query SQL: " + sql);
		if (dbServiceLocator.getService().updateSQL(getDb(), sql)) {
			if (delete) {
				return deleteProfile(dsId);
			} else {
				return regenerateProfile(dsId);
			}
		}
		return false;
	}

	private boolean updateSQL(final String dsId, final String sqlTemplate, final Map<String, Object> params, final boolean delete)
			throws DatasourceManagerServiceException {

		try {
			final Resource resource = new ClassPathResource(TMPLS_BASEDIR + sqlTemplate);
			final StringTemplate st = new StringTemplate(IOUtils.toString(resource.getInputStream()));
			st.setAttributes(params);
			return updateSQL(dsId, st.toString(), delete);
		} catch (Exception e) {
			throw new DatasourceManagerServiceException("Error in updateSQL", e);
		}
	}

	private void verifyParams(final String... params) throws DatasourceManagerServiceException {

		final Pattern pattern = Pattern.compile("\\n");

		for (String p : params) {
			log.debug("TESTING SQL PARAM:" + p);
			if ((p == null) || p.isEmpty()) {
				throw new DatasourceManagerServiceException("Parameter null or empty");
			} else if (pattern.matcher(p).matches()) {
				throw new DatasourceManagerServiceException("Parameter [" + p + "] contains an invalid character");
			} else {
				log.debug("TEST OK");
			}
		}
	}

	private String getDatasourceProfile(final String dsId) throws DatasourceManagerServiceException {
		verifyParams(dsId);

		try {
			return lookupLocator.getService()
					.getResourceProfileByQuery("/*[.//EXTRA_FIELDS/FIELD[./key='OpenAireDataSourceId']/value/text() = '" + dsId + "']");
		} catch (Exception e) {
			return null;
		}
	}

	public String getDb() {
		return db;
	}

	@Required
	public void setDb(final String db) {
		this.db = db;
	}

	public ServiceLocator<DatabaseService> getDbServiceLocator() {
		return dbServiceLocator;
	}

	@Required
	public void setDbServiceLocator(final ServiceLocator<DatabaseService> dbServiceLocator) {
		this.dbServiceLocator = dbServiceLocator;
	}

	public ServiceLocator<ISRegistryService> getRegistryLocator() {
		return registryLocator;
	}

	@Required
	public void setRegistryLocator(final ServiceLocator<ISRegistryService> registryLocator) {
		this.registryLocator = registryLocator;
	}

	public ServiceLocator<ISLookUpService> getLookupLocator() {
		return lookupLocator;
	}

	@Required
	public void setLookupLocator(final ServiceLocator<ISLookUpService> lookupLocator) {
		this.lookupLocator = lookupLocator;
	}

	public ResultSetClientFactory getResultSetClientFactory() {
		return resultSetClientFactory;
	}

	@Required
	public void setResultSetClientFactory(final ResultSetClientFactory resultSetClientFactory) {
		this.resultSetClientFactory = resultSetClientFactory;
	}

	public IterableResultSetFactory getIterableResultSetFactory() {
		return iterableResultSetFactory;
	}

	@Required
	public void setIterableResultSetFactory(final IterableResultSetFactory iterableResultSetFactory) {
		this.iterableResultSetFactory = iterableResultSetFactory;
	}

}
