package eu.dnetlib.openaire.dsm;

import static eu.dnetlib.openaire.dsm.dao.utils.DsmMappingUtils.asDbEntry;
import static eu.dnetlib.openaire.dsm.dao.utils.DsmMappingUtils.asDetails;
import static eu.dnetlib.openaire.dsm.dao.utils.DsmMappingUtils.copyNonNullProperties;
import static eu.dnetlib.openaire.dsm.dao.utils.DsmMappingUtils.createId;

import java.nio.charset.Charset;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.transaction.Transactional;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.domain.Page;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;

import com.google.common.collect.Lists;

import eu.dnetlib.enabling.datasources.common.DsmException;
import eu.dnetlib.enabling.datasources.common.DsmForbiddenException;
import eu.dnetlib.enabling.datasources.common.DsmNotFoundException;
import eu.dnetlib.openaire.common.ISClient;
import eu.dnetlib.openaire.community.CommunityClient;
import eu.dnetlib.openaire.dsm.dao.CountryTermRepository;
import eu.dnetlib.openaire.dsm.dao.DatasourceDao;
import eu.dnetlib.openaire.dsm.dao.MongoLoggerClient;
import eu.dnetlib.openaire.dsm.dao.ResponseUtils;
import eu.dnetlib.openaire.dsm.dao.VocabularyClient;
import eu.dnetlib.openaire.dsm.dao.utils.DsmMappingUtils;
import eu.dnetlib.openaire.dsm.domain.db.ApiDbEntry;
import eu.dnetlib.openaire.dsm.domain.db.DatasourceDbEntry;
import eu.dnetlib.openaire.dsm.domain.db.IdentityDbEntry;
import eu.dnetlib.openaire.exporter.exceptions.DsmApiException;
import eu.dnetlib.openaire.exporter.model.dsm.AggregationHistoryResponseV1;
import eu.dnetlib.openaire.exporter.model.dsm.AggregationHistoryResponseV2;
import eu.dnetlib.openaire.exporter.model.dsm.AggregationInfo;
import eu.dnetlib.openaire.exporter.model.dsm.AggregationInfoV1;
import eu.dnetlib.openaire.exporter.model.dsm.ApiDetails;
import eu.dnetlib.openaire.exporter.model.dsm.ApiDetailsResponse;
import eu.dnetlib.openaire.exporter.model.dsm.DatasourceDetailResponse;
import eu.dnetlib.openaire.exporter.model.dsm.DatasourceDetails;
import eu.dnetlib.openaire.exporter.model.dsm.DatasourceDetailsUpdate;
import eu.dnetlib.openaire.exporter.model.dsm.DatasourceDetailsWithApis;
import eu.dnetlib.openaire.exporter.model.dsm.DatasourceSnippetResponse;
import eu.dnetlib.openaire.exporter.model.dsm.RegisteredDatasourceInfo;
import eu.dnetlib.openaire.exporter.model.dsm.RequestFilter;
import eu.dnetlib.openaire.exporter.model.dsm.RequestSort;
import eu.dnetlib.openaire.exporter.model.dsm.RequestSortOrder;
import eu.dnetlib.openaire.exporter.model.dsm.SimpleDatasourceInfo;
import eu.dnetlib.openaire.exporter.model.dsm.SimpleResponse;
import eu.dnetlib.openaire.exporter.model.vocabularies.Country;
import eu.dnetlib.openaire.exporter.model.vocabularies.Vocabulary;

@Component
@ConditionalOnProperty(value = "openaire.exporter.enable.dsm", havingValue = "true")
public class DsmCore {

	private static final Log log = LogFactory.getLog(DsmCore.class);

	@Autowired
	private MongoLoggerClient mongoLoggerClient;

	@Autowired
	private ISClient isClient;

	@Autowired
	private VocabularyClient vocabularyClient;

	@Autowired
	private DatasourceDao<DatasourceDbEntry, ApiDbEntry> dsDao;

	@Autowired
	private CountryTermRepository countryTermRepository;

	@Autowired
	private JdbcTemplate jdbcTemplate;

	@Autowired
	private CommunityClient communityClient;

	public List<Country> listCountries() throws DsmApiException {
		final List<Country> countries = Lists.newArrayList();
		final Vocabulary v = vocabularyClient.getCountries();
		countries.addAll(countryTermRepository.findAll()
			.stream()
			.filter(Objects::nonNull)
			.map(t -> new Country(t.getTerm(), v.getEnglishName(t.getTerm())))
			.collect(Collectors.toList()));
		return countries;
	}

	public DatasourceDetailResponse searchDsDetails(final RequestSort requestSortBy,
		final RequestSortOrder order,
		final RequestFilter requestFilter,
		final int page,
		final int size)
		throws DsmApiException {

		try {
			final Page<DatasourceDbEntry> dsPage = dsDao.search(requestSortBy, order, requestFilter, page, size);
			return ResponseUtils.detailsResponse(dsPage.map(d -> asDetails(d)).getContent(), dsPage.getTotalElements());
		} catch (final DsmException e) {
			log.error("error searching datasources", e);
			throw new DsmApiException(e.getCode(), "error searching datasources", e);
		}
	}

	public DatasourceSnippetResponse searchSnippet(final RequestSort requestSortBy,
		final RequestSortOrder order,
		final RequestFilter requestFilter,
		final int page,
		final int size)
		throws DsmApiException {
		try {
			final Page<DatasourceDbEntry> dsPage = dsDao.search(requestSortBy, order, requestFilter, page, size);
			return ResponseUtils.snippetResponse(dsPage.map(DsmMappingUtils::asSnippetExtended).getContent(), dsPage.getTotalElements());
		} catch (final DsmException e) {
			log.error("error searching datasources", e);
			throw new DsmApiException(e.getCode(), "error searching datasources", e);
		}
	}

	public DatasourceSnippetResponse searchRegistered(final RequestSort requestSortBy,
		final RequestSortOrder order,
		final RequestFilter requestFilter,
		final int page,
		final int size)
		throws DsmApiException {
		try {
			final Page<DatasourceDbEntry> dsPage = dsDao.searchRegistered(requestSortBy, order, requestFilter, page, size);
			return ResponseUtils.snippetResponse(dsPage.map(DsmMappingUtils::asSnippetExtended).getContent(), dsPage.getTotalElements());
		} catch (final DsmException e) {
			log.error("error searching datasources", e);
			throw new DsmApiException(e.getCode(), "error searching datasources", e);
		}
	}

	public List<String> findBaseURLs(final RequestFilter requestFilter, final int page, final int size) throws DsmApiException {
		try {
			return dsDao.findApiBaseURLs(requestFilter, page, size);
		} catch (final DsmException e) {
			log.error("error searching datasource base urls", e);
			throw new DsmApiException(e.getCode(), "error searching datasource base urls", e);
		}
	}

	public ApiDetailsResponse getApis(final String dsId) throws DsmApiException {
		try {
			final DatasourceDbEntry ds = dsDao.getDs(dsId);
			final List<? extends ApiDbEntry> apis = dsDao.getApis(dsId);
			final List<ApiDetails> api = apis.stream()
				.map(DsmMappingUtils::asDetails)
				.map(a -> a.setEoscDatasourceType(ds.getEoscDatasourceType()))
				.map(a -> a.setTypology(ds.getTypology()))
				.collect(Collectors.toList());
			return ResponseUtils.apiResponse(api, api.size());
		} catch (final DsmException e) {
			log.error(String.format("error searching datasource api %s", dsId), e);
			throw new DsmApiException(e.getCode(), String.format("error searching datasource api %s", dsId), e);
		}
	}

	public void setManaged(final String dsId, final boolean managed) throws DsmApiException {
		log.info(String.format("updated ds '%s' managed with '%s'", dsId, managed));
		try {
			dsDao.setManaged(dsId, managed);
		} catch (final DsmException e) {
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		}
	}

	public boolean isManaged(final String dsId) throws DsmApiException {
		try {
			return dsDao.isManaged(dsId);
		} catch (final DsmException e) {
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		}
	}

	public boolean exist(final DatasourceDetails d) throws DsmApiException {
		try {
			return dsDao.existDs(d.getId());
		} catch (final DsmException e) {
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		}
	}

	public void save(final DatasourceDetails d) throws DsmApiException {
		try {
			dsDao.saveDs(asDbEntry(d));
			log.info("DS saved, " + d.getId());
		} catch (final DsmException e) {
			log.error(ExceptionUtils.getStackTrace(e));
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		}
	}

	@Transactional
	public void save(final DatasourceDetailsWithApis d) throws DsmApiException {
		try {
			dsDao.saveDs(asDbEntry(d.getDatasource()));
			final List<ApiDetails> apis = d.getApis();
			if (apis != null) {
				for (final ApiDetails api : apis) {
					api.setDatasource(d.getDatasource().getId());
					addApi(api);
				}
			}
		} catch (final DsmException e) {
			log.error(ExceptionUtils.getStackTrace(e));
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		}
	}

	public void updateDatasource(final DatasourceDetailsUpdate d) throws DsmApiException {
		try {
			// initialize with current values from DB
			final DatasourceDbEntry ds = dsDao.getDs(d.getId());

			if (ds == null) { throw new DsmNotFoundException(String.format("ds '%s' does not exist", d.getId())); }

			final DatasourceDbEntry update = asDbEntry(d);
			if (d.getIdentities() != null) {
				final Set<IdentityDbEntry> identities = new HashSet<>(
					Stream.of(update.getIdentities(), ds.getIdentities())
						.flatMap(Collection::stream)
						.collect(Collectors.toMap(i -> i.getIssuertype() + i.getPid(), Function.identity(), (i1, i2) -> i1))
						.values());
				copyNonNullProperties(update, ds);
				ds.setIdentities(identities);
			} else {
				copyNonNullProperties(update, ds);
			}

			dsDao.saveDs(ds);
		} catch (final DsmException e) {
			log.error(ExceptionUtils.getStackTrace(e));
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		}
	}

	// TODO remove if unused
	public void deleteDs(final String dsId) throws DsmApiException {
		log.info(String.format("deleted datasource '%s'", dsId));
		try {
			dsDao.deleteDs(dsId);
		} catch (final DsmException e) {
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		}
	}

	// API

	public void updateApiOaiSet(final String dsId, final String apiId, final String oaiSet) throws DsmApiException {
		try {
			dsDao.upsertApiOaiSet(apiId, oaiSet);
		} catch (final DsmException e) {
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		}
	}

	public void updateApiBaseurl(final String dsId, final String apiId, final String baseUrl) throws DsmApiException {
		log.info(String.format("updated api '%s' baseurl with '%s'", apiId, baseUrl));
		try {
			dsDao.updateApiBaseUrl(apiId, baseUrl);
		} catch (final DsmException e) {
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		}
	}

	public void updateApiCompatibility(final String dsId, final String apiId, final String compliance, final boolean override) throws DsmApiException {
		log.info(String.format("updated api '%s' compliance with '%s'", apiId, compliance));
		try {
			dsDao.updateCompliance(null, apiId, compliance, override);
		} catch (final DsmException e) {
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		}
	}

	public void addApi(final ApiDetails api) throws DsmApiException {
		if (StringUtils.isBlank(api.getId())) {
			api.setId(createId(api));
			log.info(String.format("missing api id, created '%s'", api.getId()));
		}
		try {
			dsDao.addApi(asDbEntry(api));
			log.info("API saved, id: " + api.getId());
		} catch (final DsmException e) {
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		}
	}

	public void deleteApi(final String apiId) throws DsmApiException {
		// TODO handle the api removal in case of associated workflows.
		try {
			dsDao.deleteApi(null, apiId);
		} catch (final DsmForbiddenException e) {
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		} catch (final DsmNotFoundException e) {
			throw new DsmApiException(e.getCode(), e.getMessage(), e);
		}
	}

	public void dropCaches() {
		mongoLoggerClient.dropCache();
		isClient.dropCache();
		vocabularyClient.dropCache();
		communityClient.dropCache();
	}

	// HELPERS //////////////

	public SimpleResponse<RegisteredDatasourceInfo> searchRecentRegistered(final int size) throws Throwable {
		try {
			final String sql =
				IOUtils.toString(getClass().getResourceAsStream("/eu/dnetlib/openaire/sql/recent_registered_datasources.sql.st"), Charset.defaultCharset());

			final List<RegisteredDatasourceInfo> list = jdbcTemplate.query(sql, BeanPropertyRowMapper.newInstance(RegisteredDatasourceInfo.class), size);

			return ResponseUtils.simpleResponse(list);
		} catch (final Throwable e) {
			log.error("error searching recent datasources", e);
			throw e;
		}
	}

	public Long countRegisteredAfter(final String fromDate, final String typeFilter) throws Throwable {
		try {
			if (StringUtils.isNotBlank(typeFilter)) {
				final String sql =
					IOUtils.toString(getClass().getResourceAsStream("/eu/dnetlib/openaire/sql/recent_registered_datasources_fromDate_typology.st.sql"), Charset
						.defaultCharset());

				return jdbcTemplate.queryForObject(sql, Long.class, fromDate, typeFilter + "%");
			} else {
				final String sql =
					IOUtils.toString(getClass().getResourceAsStream("/eu/dnetlib/openaire/sql/recent_registered_datasources_fromDate.st.sql"), Charset
						.defaultCharset());

				return jdbcTemplate.queryForObject(sql, Long.class, fromDate);
			}

		} catch (final Throwable e) {
			log.error("error searching recent datasources", e);
			throw e;
		}
	}

	@Deprecated
	public AggregationHistoryResponseV1 aggregationhistoryV1(final String dsId) throws DsmApiException {
		final List<AggregationInfoV1> history = mongoLoggerClient.getAggregationHistoryV1(dsId);
		final AggregationHistoryResponseV1 rsp = new AggregationHistoryResponseV1(history);
		rsp.setHeader(ResponseUtils.header(history.size()));
		return rsp;
	}

	public AggregationHistoryResponseV2 aggregationhistoryV2(final String dsId) throws DsmApiException {
		final List<AggregationInfo> history = mongoLoggerClient.getAggregationHistoryV2(dsId);
		final AggregationHistoryResponseV2 rsp = new AggregationHistoryResponseV2(history);
		rsp.setHeader(ResponseUtils.header(history.size()));
		return rsp;

	}

	public SimpleResponse<SimpleDatasourceInfo> searchRecentRegisteredV2(final int size) throws Throwable {
		try {
			final String sql =
				IOUtils.toString(getClass().getResourceAsStream("/eu/dnetlib/openaire/sql/recent_registered_datasources_v2.sql.st"), Charset.defaultCharset());

			final List<SimpleDatasourceInfo> list =
				jdbcTemplate.query(sql, rowMapperForSimpleDatasourceInfo(), size);

			return ResponseUtils.simpleResponse(list);
		} catch (final Throwable e) {
			log.error("error searching recent datasources", e);
			throw e;
		}
	}

	public Long countFirstCollect(final String fromDate, final String typeFilter) throws Throwable {
		try {
			if (StringUtils.isNotBlank(typeFilter)) {
				final String sql =
					IOUtils
						.toString(getClass().getResourceAsStream("/eu/dnetlib/openaire/sql/count_first_collected_datasources_fromDate_typology.st.sql"), Charset
							.defaultCharset());

				return jdbcTemplate.queryForObject(sql, Long.class, typeFilter + "%", fromDate);
			} else {
				final String sql =
					IOUtils.toString(getClass().getResourceAsStream("/eu/dnetlib/openaire/sql/count_first_collected_datasources_fromDate.st.sql"), Charset
						.defaultCharset());

				return jdbcTemplate.queryForObject(sql, Long.class, fromDate);
			}

		} catch (final Throwable e) {
			log.error("error searching datasources using the first collection date", e);
			throw e;
		}
	}

	public List<SimpleDatasourceInfo> getFirstCollectedAfter(final String fromDate, final String typeFilter) throws Throwable {
		try {
			if (StringUtils.isNotBlank(typeFilter)) {
				final String sql =
					IOUtils
						.toString(getClass().getResourceAsStream("/eu/dnetlib/openaire/sql/first_collected_datasources_fromDate_typology.st.sql"), Charset
							.defaultCharset());

				return jdbcTemplate.query(sql, rowMapperForSimpleDatasourceInfo(), typeFilter + "%", fromDate);

			} else {
				final String sql =
					IOUtils
						.toString(getClass().getResourceAsStream("/eu/dnetlib/openaire/sql/first_collected_datasources_fromDate.st.sql"), Charset
							.defaultCharset());

				return jdbcTemplate.query(sql, rowMapperForSimpleDatasourceInfo(), fromDate);

			}
		} catch (final Throwable e) {
			log.error("error searching datasources using the first collection date", e);
			throw e;
		}
	}

	private RowMapper<SimpleDatasourceInfo> rowMapperForSimpleDatasourceInfo() {

		return (rs, rowNum) -> {
			final SimpleDatasourceInfo info = new SimpleDatasourceInfo();

			info.setId(rs.getString("id"));
			info.setOfficialName(rs.getString("officialName"));
			info.setEnglishName(rs.getString("englishName"));
			info.setTypology(rs.getString("typology"));
			info.setEoscType(rs.getString("eoscType"));
			info.setEoscDatasourceType(rs.getString("eoscDatasourceType"));
			info.setRegisteredBy(rs.getString("registeredBy"));
			info.setRegistrationDate(rs.getString("registrationDate"));
			info.setFirstCollectionDate(rs.getString("firstCollectionDate"));
			info.setLastCollectionDate(rs.getString("lastCollectionDate"));
			info.setLastCollectionTotal(rs.getLong("lastCollectionTotal"));

			final Set<String> compatibilities = new HashSet<>();
			for (final String s : (String[]) rs.getArray("compatibilities").getArray()) {
				compatibilities.add(s);
			}

			// The order of the condition is important
			if (compatibilities.contains("openaire-cris_1.1")) {
				info.setCompatibility("openaire-cris_1.1");
			} else if (compatibilities.contains("openaire4.0")) {
				info.setCompatibility("openaire4.0");
			} else if (compatibilities.contains("driver") && compatibilities.contains("openaire2.0")) {
				info.setCompatibility("driver-openaire2.0");
			} else if (compatibilities.contains("driver")) {
				info.setCompatibility("driver");
			} else if (compatibilities.contains("openaire2.0")) {
				info.setCompatibility("openaire2.0");
			} else if (compatibilities.contains("openaire3.0")) {
				info.setCompatibility("openaire3.0");
			} else if (compatibilities.contains("openaire2.0_data")) {
				info.setCompatibility("openaire2.0_data");
			} else if (compatibilities.contains("native")) {
				info.setCompatibility("native");
			} else if (compatibilities.contains("hostedBy")) {
				info.setCompatibility("hostedBy");
			} else if (compatibilities.contains("notCompatible")) {
				info.setCompatibility("notCompatible");
			} else {
				info.setCompatibility("UNKNOWN");
			}

			for (final String s : (String[]) rs.getArray("organizations").getArray()) {
				info.getOrganizations().put(StringUtils.substringBefore(s, "@@@").trim(), StringUtils.substringAfter(s, "@@@").trim());
			}

			return info;
		};
	}

}
