package eu.dnetlib.openaire.community;

import java.util.*;
import java.util.stream.Collectors;

import com.google.gson.Gson;

import eu.dnetlib.openaire.exporter.exceptions.CommunityException;
import eu.dnetlib.openaire.exporter.exceptions.CommunityNotFoundException;
import eu.dnetlib.openaire.exporter.model.community.CommunityContentprovider;
import eu.dnetlib.openaire.exporter.model.community.CommunityDetails;
import eu.dnetlib.openaire.exporter.model.community.CommunityOpenAIRECommunities;
import eu.dnetlib.openaire.exporter.model.community.CommunityOrganization;
import eu.dnetlib.openaire.exporter.model.community.CommunityProject;
import eu.dnetlib.openaire.exporter.model.community.CommunitySummary;
import eu.dnetlib.openaire.exporter.model.community.CommunityWritableProperties;
import eu.dnetlib.openaire.exporter.model.community.CommunityZenodoCommunity;
import eu.dnetlib.openaire.exporter.model.community.selectioncriteria.SelectionCriteria;

import org.apache.commons.lang3.StringUtils;
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.cache.annotation.CacheEvict;
import org.springframework.stereotype.Component;

import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import eu.dnetlib.openaire.common.ISClient;

import static eu.dnetlib.openaire.community.CommunityConstants.*;

@Component
@ConditionalOnProperty(value = "openaire.exporter.enable.community", havingValue = "true")
public class CommunityApiCore {// implements CommunityClient{

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

	@Autowired
	private CommunityClient cci;

	@Autowired
	private ISClient isClient;

	@Autowired
	private CommunityCommon cc;

	public List<CommunitySummary> listCommunities() throws CommunityException {
		return cc.listCommunities();

	}

	public CommunityDetails getCommunity(final String id) throws CommunityException, CommunityNotFoundException {
		return cc.getCommunity(id);

	}

	private void removeAdvancedConstraint(String id) throws CommunityNotFoundException, CommunityException {
		cc.getCommunity(id);
		isClient.updateContextParam(id, CPROFILE_ADVANCED_CONSTRAINT, "", false);
		cc.removeAdvancedConstraint(id);

	}

	public void setCommunity(final String id, final CommunityWritableProperties details) throws CommunityException, CommunityNotFoundException {

		cc.getCommunity(id); // ensure the community exists.

		if (details.getShortName() != null) {
			isClient.updateContextAttribute(id, CLABEL, details.getShortName());

		}
		if (details.getName() != null) {
			isClient.updateContextParam(id, CSUMMARY_NAME, details.getName(), true);

		}
		if (details.getDescription() != null) {
			isClient.updateContextParam(id, CSUMMARY_DESCRIPTION, details.getDescription(), true);

		}
		if (details.getLogoUrl() != null) {
			isClient.updateContextParam(id, CSUMMARY_LOGOURL, details.getLogoUrl(), true);

		}
		if (details.getStatus() != null) {
			isClient.updateContextParam(id, CSUMMARY_STATUS, details.getStatus().name(), true);

		}
		if (details.getSubjects() != null) {
			isClient.updateContextParam(id, CPROFILE_SUBJECT, Joiner.on(CSV_DELIMITER).join(details.getSubjects()), true);

		}
		if (details.getFos() != null) {
			isClient.updateContextParam(id, CPROFILE_FOS, Joiner.on(CSV_DELIMITER).join(details.getFos()), true);

		}
		if (details.getSdg() != null) {
			isClient.updateContextParam(id, CPROFILE_SDG, Joiner.on(CSV_DELIMITER).join(details.getSdg()), true);

		}
		if (details.getAdvancedConstraints() != null) {
			isClient.updateContextParam(id, CPROFILE_ADVANCED_CONSTRAINT, "<![CDATA[" + new Gson().toJson(details.getAdvancedConstraints()) + "]]>", false);

		}
		if (details.getMainZenodoCommunity() != null) {
			isClient.updateContextParam(id, CSUMMARY_ZENODOC, details.getMainZenodoCommunity(), true);
		}

		cc.updateCommunity(id, details);
	}

	public List<CommunityProject> getCommunityProjects(final String id) throws CommunityException, CommunityNotFoundException {
		cc.getCommunity(id); // ensure the community exists.
		return cc.getCommunityInfo(id, PROJECTS_ID_SUFFIX, c -> CommunityMappingUtils.asCommunityProject(id, c));
	}

	public CommunityProject addCommunityProject(final String id, final CommunityProject project) throws CommunityException, CommunityNotFoundException {
		if (!StringUtils.equalsIgnoreCase(id, project.getCommunityId())) {
			throw new CommunityException("parameters 'id' and project.communityId must be coherent");
		}

		return updateProject(id, project);

	}

	private CommunityProject updateProject(String id, CommunityProject project) throws CommunityException, CommunityNotFoundException {
		final TreeMap<Integer, CommunityProject> projects = getCommunityProjectMap(id);
		String project_id = project.getId();

		if (project_id != null && projects.keySet().contains(Integer.valueOf(project_id))){
			if (project.getName() != null) {
				isClient.updateConceptParam(id + PROJECTS_ID_SUFFIX + ID_SEPARATOR + project_id, CPROJECT_FULLNAME, project.getName());

			}
			if(project.getAcronym()!= null){
				isClient.updateConceptParam(id + PROJECTS_ID_SUFFIX + ID_SEPARATOR + project_id, CPROJECT_ACRONYM, project.getAcronym());

			}
			if (project.getOpenaireId() != null){
				isClient.updateConceptParam(id + PROJECTS_ID_SUFFIX + ID_SEPARATOR + project_id, OPENAIRE_ID, project.getOpenaireId());

			}
			if (project.getFunder() != null){
				isClient.updateConceptParam(id + PROJECTS_ID_SUFFIX + ID_SEPARATOR + project_id, CPROJECT_FUNDER, project.getFunder());

			}
			if(project.getGrantId() != null){
				isClient.updateConceptParam(id + PROJECTS_ID_SUFFIX + ID_SEPARATOR + project_id, CPROJECT_NUMBER, project.getGrantId());

			}
		}else {
			project.setId(nextId(projects != null && !projects.isEmpty() ? projects.lastKey() : 0));

			isClient.addConcept(id, id + PROJECTS_ID_SUFFIX, CommunityMappingUtils.asProjectXML(id, project));

		}
		cc.updateProject(id, project );
		return project;
	}

	public List<CommunityProject> addCommunityProjectList(final String id, final List<CommunityProject> projectList) throws CommunityException, CommunityNotFoundException {
		if(projectList == null || projectList.size() == 0){
			throw new CommunityException("parameter 'projectList' must be present and should contain at least one project");
		}
		if (!StringUtils.equalsIgnoreCase(id, projectList.get(0).getCommunityId())) {
			throw new CommunityException("parameters 'id' and project.communityId must be coherent");
		}

		List<CommunityProject> projects = new ArrayList();

		for(CommunityProject project : projectList){
			projects.add(updateProject(id, project));
		}


		return projects;
	}

	private String nextId(final Integer id) {
		return String.valueOf(id + 1);
	}

	public void removeCommunityProject(final String id, final Integer projectId) throws CommunityException, CommunityNotFoundException {
		final Map<Integer, CommunityProject> projects = getCommunityProjectMap(id);
		if (!projects.containsKey(projectId)) {
			throw new CommunityNotFoundException(String.format("project '%s' doesn't exist within context '%s'", projectId, id));
		}
		isClient.removeConcept(id, id + PROJECTS_ID_SUFFIX, id + PROJECTS_ID_SUFFIX + ID_SEPARATOR + projectId);
		cc.removeFromCategory(id, PROJECTS_ID_SUFFIX, String.valueOf(projectId));
	}

	public void removeCommunityProjectList(final String id, final List<Integer> projectIdList) throws CommunityException, CommunityNotFoundException {
		for(Integer projectId: projectIdList){
			removeCommunityProject(id, projectId);
		}
	}

	public List<CommunityContentprovider> getCommunityContentproviders(final String id) throws CommunityException, CommunityNotFoundException {
		cc.getCommunity(id); // ensure the community exists.
		return cc.getCommunityInfo(id, CONTENTPROVIDERS_ID_SUFFIX, c -> CommunityMappingUtils.asCommunityDataprovider(id, c));
	}

	public CommunityContentprovider addCommunityContentprovider(final String id, final CommunityContentprovider cp)
		throws CommunityException, CommunityNotFoundException {
		log.info("content provider to add " + cp.toString());
		if (!StringUtils.equalsIgnoreCase(id, cp.getCommunityId())) { throw new CommunityException("parameters 'id' and cp.communityId must be coherent"); }

		return updateContentprovider(id, cp);
	}

	private CommunityContentprovider updateContentprovider(String id, CommunityContentprovider cp) throws CommunityException, CommunityNotFoundException {
		final TreeMap<Integer, CommunityContentprovider> cps = getCommunityContentproviderMap(id);
		final String concept_id = cp.getId();
		if (concept_id != null && cps.keySet().contains(Integer.valueOf(concept_id))) {
			if (cp.getName() != null) {
				isClient.updateConceptParam(id + CONTENTPROVIDERS_ID_SUFFIX + ID_SEPARATOR + concept_id, CCONTENTPROVIDER_NAME, cp.getName());
			}
			if (cp.getOfficialname() != null) {
				isClient.updateConceptParam(id + CONTENTPROVIDERS_ID_SUFFIX + ID_SEPARATOR + concept_id, CCONTENTPROVIDER_OFFICIALNAME, cp.getOfficialname());
			}
			if (cp.getOpenaireId() != null) {
				isClient.updateConceptParam(id + CONTENTPROVIDERS_ID_SUFFIX + ID_SEPARATOR + concept_id, OPENAIRE_ID, cp.getOpenaireId());
			}
			if (cp.getSelectioncriteria() != null) {
				isClient.updateConceptParamNoEscape(id + CONTENTPROVIDERS_ID_SUFFIX + ID_SEPARATOR + concept_id, CCONTENTPROVIDER_SELCRITERIA, cp.toXML());
			}
		} else {
			log.info("adding new concept for community " + id);
			cp.setId(nextId(!cps.isEmpty() ? cps.lastKey() : 0));

			isClient.addConcept(id, id + CONTENTPROVIDERS_ID_SUFFIX, CommunityMappingUtils.asContentProviderXML(id, cp));
		}

		cc.updateDatasource(id, cp);
		return cp;
	}

	public void removeCommunityContentProvider(final String id, final Integer contentproviderId) throws CommunityException, CommunityNotFoundException {
		final Map<Integer, CommunityContentprovider> providers = getCommunityContentproviderMap(id);
		if (!providers.containsKey(contentproviderId)) {
			throw new CommunityNotFoundException(String.format("content provider '%s' doesn't exist within context '%s'", contentproviderId, id));
		}
		isClient.removeConcept(id, id + CONTENTPROVIDERS_ID_SUFFIX, id + CONTENTPROVIDERS_ID_SUFFIX + ID_SEPARATOR + contentproviderId);
		cc.removeFromCategory(id, CONTENTPROVIDERS_ID_SUFFIX, String.valueOf(contentproviderId));
	}

	public List<CommunityContentprovider> addCommunityContentProvidersList(String id, List<CommunityContentprovider> contentprovidersList) throws CommunityException, CommunityNotFoundException {
		if(contentprovidersList == null || contentprovidersList.size() == 0){
			throw new CommunityException("parameter 'contentprovidersList' must be present and should contain at least one content provider");
		}
		if (!StringUtils.equalsIgnoreCase(id, contentprovidersList.get(0).getCommunityId())) {
			throw new CommunityException("parameters 'id' and contentprovider.communityId must be coherent");
		}

		List<CommunityContentprovider> contentproviders = new ArrayList();

		for(CommunityContentprovider contentProvider : contentprovidersList){
			contentproviders.add(updateContentprovider(id, contentProvider));
		}


		return contentproviders;
	}

	public void removeCommunityContentProviderList(final String id, final List<Integer> contentProviderIdList) throws CommunityException, CommunityNotFoundException {
		for(Integer contentProviderId: contentProviderIdList){
			removeCommunityContentProvider(id, contentProviderId);
		}
	}

	public void removeCommunityOrganization(final String id, final Integer organizationId) throws CommunityException, CommunityNotFoundException {
		final Map<Integer, CommunityOrganization> organizations = getCommunityOrganizationMap(id);
		if (!organizations.containsKey(organizationId)) {
			throw new CommunityNotFoundException(String.format("organization '%s' doesn't exist within context '%s'", organizationId, id));
		}
		isClient.removeConcept(id, id + ORGANIZATION_ID_SUFFIX, id + ORGANIZATION_ID_SUFFIX + ID_SEPARATOR + organizationId);
		cc.removeFromCategory(id, ORGANIZATION_ID_SUFFIX, String.valueOf(organizationId));
	}

	public List<CommunityZenodoCommunity> getCommunityZenodoCommunities(final String id) throws CommunityException, CommunityNotFoundException {

		return cc.getCommunityZenodoCommunities(id);
	}

	public List<CommunityOrganization> getCommunityOrganizations(final String id) throws CommunityException, CommunityNotFoundException {
		cc.getCommunity(id);
		return cc.getCommunityInfo(id, ORGANIZATION_ID_SUFFIX, c -> CommunityMappingUtils.asCommunityOrganization(id, c));
	}

	public CommunityDetails addCommunitySubjects(final String id, final List<String> subjects) throws CommunityException, CommunityNotFoundException {

		final CommunityDetails cd = new CommunityDetails();

		final Set<String> current = Sets.newHashSet(cc.getCommunity(id).getSubjects());

		current.addAll(subjects);

		cd.setSubjects(Lists.newArrayList(current));

		setCommunity(id, CommunityWritableProperties.fromDetails(cd));

		return cd;
	}

	public CommunityDetails removeCommunitySubjects(final String id, final List<String> subjects) throws CommunityException, CommunityNotFoundException {

		final CommunityDetails cd = new CommunityDetails();

		final Set<String> current = Sets.newHashSet(cc.getCommunity(id).getSubjects());

		current.removeAll(subjects);

		cd.setSubjects(Lists.newArrayList(current));

		setCommunity(id, CommunityWritableProperties.fromDetails(cd));

		return cd;
	}

	public CommunityDetails addCommunityFOS(final String id, final List<String> foss) throws CommunityException, CommunityNotFoundException {

		final CommunityDetails cd = new CommunityDetails();

		final Set<String> current = Sets.newHashSet();
		if(Optional.ofNullable(cc.getCommunity(id).getFos()).isPresent()){
			current.addAll(cc.getCommunity(id).getFos());
		}

		current.addAll(foss);

		cd.setFos(Lists.newArrayList(current));

		setCommunity(id, CommunityWritableProperties.fromDetails(cd));

		return cd;
	}

	public CommunityDetails removeCommunityFOS(final String id, final List<String> foss) throws CommunityException, CommunityNotFoundException {

		final CommunityDetails cd = new CommunityDetails();

		final Set<String> current = Sets.newHashSet(cc.getCommunity(id).getFos());

		current.removeAll(foss);

		cd.setFos(Lists.newArrayList(current));

		setCommunity(id, CommunityWritableProperties.fromDetails(cd));

		return cd;
	}

	public CommunityDetails addCommunitySDG(final String id, final List<String> sdgs) throws CommunityException, CommunityNotFoundException {

		final CommunityDetails cd = new CommunityDetails();
		final Set<String> current = Sets.newHashSet();
		if(Optional.ofNullable(cc.getCommunity(id).getSdg()).isPresent()){
			current.addAll(cc.getCommunity(id).getSdg());
		}

		current.addAll(sdgs);

		cd.setSdg(Lists.newArrayList(current));

		setCommunity(id, CommunityWritableProperties.fromDetails(cd));

		return cd;
	}

	public CommunityDetails removeCommunitySDG(final String id, final List<String> sdgs) throws CommunityException, CommunityNotFoundException {

		final CommunityDetails cd = new CommunityDetails();

		final Set<String> current = Sets.newHashSet(cc.getCommunity(id).getSdg());

		current.removeAll(sdgs);

		cd.setSdg(Lists.newArrayList(current));

		setCommunity(id, CommunityWritableProperties.fromDetails(cd));

		return cd;
	}

	public CommunityDetails addCommunityAdvancedConstraint(final String id, final SelectionCriteria advancedCosntraint) throws CommunityException, CommunityNotFoundException {

		final CommunityDetails cd = new CommunityDetails();

		cd.setAdvancedConstraints(advancedCosntraint);

		setCommunity(id, CommunityWritableProperties.fromDetails(cd));

		return cd;
	}

	public CommunityDetails removeCommunityAdvancedConstraint(final String id) throws CommunityNotFoundException, CommunityException {

		removeAdvancedConstraint(id);

		return new CommunityDetails();
	}



	@CacheEvict(value = "community-cache", allEntries = true)
	public void removeCommunityZenodoCommunity(final String id, final Integer zenodoCommId) throws CommunityException, CommunityNotFoundException {

		final Map<Integer, CommunityZenodoCommunity> zcomms = getZenodoCommunityMap(id);
		if (!zcomms.containsKey(zenodoCommId)) {
			throw new CommunityNotFoundException(String.format("Zenodo community '%s' doesn't exist within context '%s'", zenodoCommId, id));
		}
		isClient.removeConcept(id, id + ZENODOCOMMUNITY_ID_SUFFIX, id + ZENODOCOMMUNITY_ID_SUFFIX + ID_SEPARATOR + zenodoCommId);

		cc.removeFromCategory(id, ZENODOCOMMUNITY_ID_SUFFIX, String.valueOf(zenodoCommId));

	}

	@CacheEvict(value = "community-cache", allEntries = true)
	public CommunityZenodoCommunity addCommunityZenodoCommunity(final String id, final CommunityZenodoCommunity zc)
		throws CommunityException, CommunityNotFoundException {
		if (!StringUtils.equalsIgnoreCase(id, zc.getCommunityId())) { throw new CommunityException("parameters 'id' and zc.communityId must be coherent"); }
		if (!StringUtils.isNotBlank(zc.getZenodoid())) { throw new CommunityException("parameter zenodoid cannot be null or empty"); }
		final TreeMap<Integer, CommunityZenodoCommunity> zcs = getZenodoCommunityMap(id);

		for (final CommunityZenodoCommunity czc : zcs.values()) {
			if (czc.getZenodoid().equals(zc.getZenodoid())) { throw new CommunityException("Zenodo community already associated to the RCD"); }
		}

		zc.setId(nextId(!zcs.isEmpty() ? zcs.lastKey() : 0));

		isClient.addConcept(id, id + ZENODOCOMMUNITY_ID_SUFFIX, CommunityMappingUtils.asZenodoCommunityXML(id, zc));
		cc.updateZenodoCommunity(id, zc);

		return zc;
	}

	public CommunityOpenAIRECommunities getOpenAIRECommunities(final String zenodoId) throws CommunityException, CommunityNotFoundException {

		if (cci.getInverseZenodoCommunityMap().containsKey(zenodoId)) {
			return new CommunityOpenAIRECommunities().setZenodoid(zenodoId)
				.setOpenAirecommunitylist(cci.getInverseZenodoCommunityMap().get(zenodoId).stream().collect(Collectors.toList()));
		}
		return new CommunityOpenAIRECommunities();

	}

	// HELPERS

	private TreeMap<Integer, CommunityProject> getCommunityProjectMap(final String id) throws CommunityException, CommunityNotFoundException {
		return getCommunityProjects(id).stream()
			.collect(Collectors.toMap(p -> Integer.valueOf(p.getId()), Functions.identity(), (p1, p2) -> {
				log.warn(String.format("duplicate project found: '%s'", p1.getId()));
				return p2;
			}, TreeMap::new));
	}

	private TreeMap<Integer, CommunityContentprovider> getCommunityContentproviderMap(final String id) throws CommunityException, CommunityNotFoundException {
		log.info("getting community content provider map");
		return getCommunityContentproviders(id).stream()
			.collect(Collectors.toMap(cp -> Integer.valueOf(cp.getId()), Functions.identity(), (cp1, cp2) -> {
				log.warn(String.format("duplicate content provider found: '%s'", cp1.getId()));
				return cp2;
			}, TreeMap::new));
	}

	private TreeMap<Integer, CommunityZenodoCommunity> getZenodoCommunityMap(final String id) throws CommunityException, CommunityNotFoundException {
		return getCommunityZenodoCommunities(id).stream()
			.collect(Collectors.toMap(cp -> Integer.valueOf(cp.getId()), Functions.identity(), (cp1, cp2) -> {
				log.warn(String.format("duplicate Zenodo community found: '%s'", cp1.getId()));
				return cp2;
			}, TreeMap::new));
	}

	private TreeMap<Integer, CommunityOrganization> getCommunityOrganizationMap(final String id) throws CommunityException, CommunityNotFoundException {
		return getCommunityOrganizations(id).stream()
			.collect(Collectors.toMap(o -> Integer.valueOf(o.getId()), Functions.identity(), (o1, o2) -> {
				log.warn(String.format("duplicate content provider found: '%s'", o1.getId()));
				return o2;
			}, TreeMap::new));
	}

	public CommunityOrganization addCommunityOrganization(final String id, final CommunityOrganization organization)
		throws CommunityException, CommunityNotFoundException {
		if (!StringUtils.equalsIgnoreCase(id, organization.getCommunityId())) {
			throw new CommunityException("parameters 'id' and organization.communityId must be coherent");
		}

		final TreeMap<Integer, CommunityOrganization> cps = getCommunityOrganizationMap(id);

		final String organization_id = organization.getId();
		if (organization_id != null && cps.keySet().contains(Integer.valueOf(organization_id))) {
			if (organization.getName() != null) {
				isClient.updateConceptParam(id + ORGANIZATION_ID_SUFFIX + ID_SEPARATOR + organization_id, CORGANIZATION_NAME, organization.getName());
			}
			if (organization.getLogo_url() != null) {
				isClient.updateConceptParam(id + ORGANIZATION_ID_SUFFIX + ID_SEPARATOR + organization_id, CORGANIZATION_LOGOURL, Base64.getEncoder()
					.encodeToString(organization.getLogo_url().getBytes()));
			}
			if (organization.getWebsite_url() != null) {
				isClient.updateConceptParam(id + ORGANIZATION_ID_SUFFIX + ID_SEPARATOR + organization_id, CORGANIZATION_WEBSITEURL, Base64.getEncoder()
					.encodeToString(organization.getWebsite_url().getBytes()));
			}
		} else {
			organization.setId(nextId(!cps.isEmpty() ? cps.lastKey() : 0));
			isClient.addConcept(id, id + ORGANIZATION_ID_SUFFIX, CommunityMappingUtils.asOrganizationXML(id, organization));
		}

		cc.updateOrganization(id, organization);
		return organization;
	}


}
