package org.gcube.application.cms.sdi.plugins;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.bson.Document;
import org.gcube.application.cms.plugins.IndexerPluginInterface;
import org.gcube.application.cms.plugins.faults.IndexingException;
import org.gcube.application.cms.plugins.faults.InitializationException;
import org.gcube.application.cms.plugins.faults.InvalidPluginRequestException;
import org.gcube.application.cms.plugins.faults.InvalidProfileException;
import org.gcube.application.cms.plugins.implementations.IndexConstants;
import org.gcube.application.cms.plugins.reports.IndexDocumentReport;
import org.gcube.application.cms.plugins.reports.InitializationReport;
import org.gcube.application.cms.plugins.reports.Report;
import org.gcube.application.cms.plugins.requests.BaseRequest;
import org.gcube.application.cms.plugins.requests.IndexDocumentRequest;
import org.gcube.application.cms.sdi.Constants;
import org.gcube.application.cms.sdi.engine.DBConstants;
import org.gcube.application.cms.sdi.engine.PostgisIndexer;
import org.gcube.application.cms.sdi.engine.PostgisTable;
import org.gcube.application.cms.sdi.engine.bboxes.BBOXByCoordinatePaths;
import org.gcube.application.cms.sdi.engine.bboxes.BBOXEvaluator;
import org.gcube.application.cms.sdi.engine.bboxes.BBOXPathScanner;
import org.gcube.application.cms.sdi.faults.SDIInteractionException;
import org.gcube.application.cms.sdi.model.MappingObject;
import org.gcube.application.cms.serialization.Serialization;
import org.gcube.application.geoportal.common.model.JSONPathWrapper;
import org.gcube.application.geoportal.common.model.configuration.Index;
import org.gcube.application.geoportal.common.model.document.Project;
import org.gcube.application.geoportal.common.model.document.filesets.sdi.GCubeSDILayer;
import org.gcube.application.geoportal.common.model.document.identification.IdentificationReference;
import org.gcube.application.geoportal.common.model.document.identification.SpatialReference;
import org.gcube.application.geoportal.common.model.plugins.IndexerPluginDescriptor;
import org.gcube.application.geoportal.common.model.plugins.PluginDescriptor;
import org.gcube.application.geoportal.common.model.rest.ConfigurationException;
import org.gcube.application.geoportal.common.model.useCaseDescriptor.UseCaseDescriptor;
import org.gcube.portlets.user.uriresolvermanager.UriResolverManager;
import org.gcube.portlets.user.uriresolvermanager.resolvers.query.GeoportalResolverQueryStringBuilder;
import org.gcube.portlets.user.uriresolvermanager.resolvers.query.GeoportalResolverQueryStringBuilder.RESOLVE_AS;
import org.geojson.Crs;
import org.geojson.GeoJsonObject;
import org.geojson.LngLatAlt;
import org.geojson.Point;

import com.vdurmont.semver4j.Semver;

import lombok.extern.slf4j.Slf4j;


/**
 * The Class SDIIndexerPlugin.
 * 
 * @author created by Fabio Sinibaldi
 * @author new architect and maintainer - Francesco Mangiacrapa at ISTI-CNR francesco.mangiacrapa@isti.cnr.it
 * 
 * Apr 28, 2023
 */
@Slf4j
public class SDIIndexerPlugin extends SDIAbstractPlugin implements IndexerPluginInterface {

	static final PluginDescriptor DESCRIPTOR = new PluginDescriptor(Constants.INDEXER_PLUGIN_ID,
			IndexerPluginDescriptor.INDEXER);

	static final ArrayList<BBOXEvaluator> BBOX_EVALUATORS = new ArrayList<>();

	static {
		DESCRIPTOR.setDescription("SDI Indexer. " + "Manage Centroids layers.");
		DESCRIPTOR.setVersion(new Semver("1.0.0"));

		BBOX_EVALUATORS.add(new BBOXPathScanner());
		BBOX_EVALUATORS.add(new BBOXByCoordinatePaths());

	}

	/**
	 * Gets the descriptor.
	 *
	 * @return the descriptor
	 */
	@Override
	public PluginDescriptor getDescriptor() {
		return DESCRIPTOR;
	}

	/**
	 * Inits the in context.
	 *
	 * @return the initialization report
	 * @throws InitializationException the initialization exception
	 */
	@Override
	public InitializationReport initInContext() throws InitializationException {
		InitializationReport report = new InitializationReport();
		report.setStatus(Report.Status.OK);
		return report;
	}

	/**
	 * Expected parameters : - indexName (unique) - workspace - flagInternalIndex as
	 * Boolean; boolean - centroidRecord (OPT).
	 *
	 * @param request the request
	 * @return the index document report
	 * @throws InvalidPluginRequestException the invalid plugin request exception
	 */

	@Override
	public IndexDocumentReport index(IndexDocumentRequest request) throws InvalidPluginRequestException {
		log.info("Serving Indexer {} : request CallParameters {}, request Context {}: ", this.getDescriptor().getId(),
				request.getCallParameters(), request.getContext());
		log.debug("Indexer {} : Serving Request {} ", this.getDescriptor().getId(), request);

		Project project = request.getDocument();
		UseCaseDescriptor useCaseDescriptor = request.getUseCaseDescriptor();
		Document requestArguments = request.getCallParameters();

		log.debug("requestArguments is {} ", requestArguments);

		IndexDocumentReport report = new IndexDocumentReport(request);

		try {
			// ********* INIT INDEX
			// TODO CACHE
			PostgisIndexer indexer = getIndexer(useCaseDescriptor, requestArguments);

			Document profileConfiguration = getConfigurationFromProfile(useCaseDescriptor).getConfiguration();
			log.debug("UseCaseDescriptor Configuration is {} ", profileConfiguration);

			// ************* PREPARE RECORD

			JSONPathWrapper documentNavigator = new JSONPathWrapper(Serialization.write(project));

			Document centroidDoc = new Document();
			if (requestArguments.containsKey("centroidRecord"))
				centroidDoc.putAll(requestArguments.get("centroidRecords", Document.class));
			// DEFAULT VALUES
			centroidDoc.put(DBConstants.Defaults.PROJECT_ID, project.getId());
			centroidDoc.put(DBConstants.Defaults.DISPLAYED, true);

			//*********** Added by Francesco. Creating Gis Viewer Link as public or private
			Boolean isInternalIndex = null;
			try {
				isInternalIndex = requestArguments.getBoolean(IndexConstants.INDEX_PARAMETER_FLAGINTERNALINDEX);
				log.debug(IndexConstants.INDEX_PARAMETER_FLAGINTERNALINDEX + " read as {} ", isInternalIndex);
			} catch (Exception e) {
				// TODO: handle exception
			}
			log.info(IndexConstants.INDEX_PARAMETER_FLAGINTERNALINDEX + " is {} ", isInternalIndex);

			if (isInternalIndex != null) {
				try {
					log.debug("Trying to generate Geoportal Gis Link...");
					// Contacting the Geoportal-Resolver via UriResolverManager
					UriResolverManager uriResolverManager = new UriResolverManager("GEO");
					GeoportalResolverQueryStringBuilder builder = new GeoportalResolverQueryStringBuilder(
							project.getProfileID(), project.getId());
					builder.scope(request.getContext().getId());

					if (isInternalIndex) {
						builder.resolverAs(RESOLVE_AS.PRIVATE);
					} else {
						builder.resolverAs(RESOLVE_AS.PUBLIC);
					}
					Map<String, String> params = builder.buildQueryParameters();
					String shortLink = uriResolverManager.getLink(params, true);
					log.info("Geoportal GisViewer link is {} ", shortLink);
					centroidDoc.put(DBConstants.Defaults.GEOVIEWER_LINK_FIELD, shortLink);
				} catch (Exception e) {
					log.error("Error on creating the Geoportal GisViewer link for project id {}", project.getId(), e);
				}
			}
			
			// ********************** Updated by Francesco, see #25056, Calculating Spatial Reference as Centroid Object
			//**** EVALUATE POSITION
			log.debug("indexing UseCaseDescriptor {} : Evaluating Centroid... ", useCaseDescriptor.getId());
			SpatialReference reference = null;
			List<IdentificationReference> refs = project
					.getIdentificationReferenceByType(SpatialReference.SPATIAL_REFERENCE_TYPE);
			
			GCubeSDILayer.BBOX bbox = null;
			if (!refs.isEmpty()) {
				// Use existing Reference
				try {
					reference = Serialization.convert(refs.get(0), SpatialReference.class);
	
					log.debug("Using already defined spatial reference " + reference);
	
					GeoJsonObject object = Serialization.convert(reference.getGeoJson(), GeoJsonObject.class);
	
					bbox = GCubeSDILayer.BBOX.fromGeoJSON(object.getBbox());
	
					log.info("Found declared BBOX {} ", bbox);
					Double pointX = (bbox.getMaxX() + bbox.getMinX()) / 2;
					Double pointY = (bbox.getMaxY() + bbox.getMinY()) / 2;
					String wkt = String.format("POINT (%1$f %2$f) ", pointX, pointY);
	
					centroidDoc.put("geom", wkt);
					//No overriding SpatialReference in the IndexDocumentReport, using the existing one.
				}catch (Exception e) {
					log.info("The defined spatial reference is wrong or empty: " + reference);
				}

			}
			
			//Calculating and overriding the SpatialReference (the Centroid JSON) in the IndexDocumentReport.
			if(bbox==null) {
				log.info("No bbox defined in the current spatial reference going to calculate it...");
				// unable to use current Spatial reference, try evaluating it
				log.debug("UseCaseDescriptor {} : Getting evaluation paths from useCaseDescriptor.. ",
						useCaseDescriptor.getId());

				// for each configuration option try until found
				GCubeSDILayer.BBOX toSet = null;
				for (BBOXEvaluator evaluator : BBOX_EVALUATORS) {
					log.trace("UCD {}, Project {}. Evaluating BBOX with {}", useCaseDescriptor.getId(), project.getId(),
							evaluator);
					try {
						if (evaluator.isConfigured(profileConfiguration)) {
							toSet = evaluator.evaluate(profileConfiguration, useCaseDescriptor, documentNavigator);
							if (toSet != null) {
								log.info("UCD {}, Project {}. Evaluated BBOX {} with method {}",
										useCaseDescriptor.getId(), project.getId(), toSet, evaluator);
								break;
							}
						}
					} catch (Throwable t) {
						log.warn("UCD {}, Project {}. Exception with {}", useCaseDescriptor.getId(), project.getId(),
								evaluator, t);
					}
				}
				if (toSet == null)
					throw new IndexingException("No BBOX has been evaluated from project");

				Double pointX = (toSet.getMaxX() + toSet.getMinX()) / 2;
				Double pointY = (toSet.getMaxY() + toSet.getMinY()) / 2;
				log.info("Evaluated BBOX {} ", toSet);
				String wkt = String.format("POINT (%1$f %2$f) ", pointX, pointY);
				// TODO support altitude
				Double pointZ = 0d;

				centroidDoc.put("geom", wkt);

				Point point = new Point();
				point.setCoordinates(new LngLatAlt(pointX, pointY, pointZ));
				point.setBbox(toSet.asGeoJSONArray());

				// TODO Manage CRS
				point.setCrs(new Crs());
				reference = new SpatialReference(Serialization.asDocument(point));
				log.info("UCD {} project {}, Setting Spatial Reference {} ", useCaseDescriptor.getId(), project.getId(),
						Serialization.write(reference));
				report.addIdentificationReference(reference);
			}

			// *********** Additional Values from useCaseDescriptor

			log.info("Setting additional values to centroid from mappings ..");
			for (MappingObject m : getMappings(useCaseDescriptor)) {
				List<Object> foundValues = documentNavigator.getByPath(m.getPath());
				Object toSetValue = null;
				if (!foundValues.isEmpty()) {
					// NB CSV for multiple values
					StringBuilder b = new StringBuilder();
					foundValues.forEach(o -> {
						// Parser returns list of list
						if (o instanceof Collection)
							((Collection<?>) o).forEach(v -> b.append(v + ","));
						else
							b.append(o + ",");
					});
					b.deleteCharAt(b.length() - 1);
					toSetValue = b.toString();
				}
				log.trace("Setting {} = {} in centroid doc ", m.getName(), toSetValue);
				centroidDoc.put(m.getName(), toSetValue);
			}

			log.info("Inserting Centroid {} into {} ", Serialization.write(centroidDoc.toJson()), indexer);
			indexer.insert(centroidDoc);

			// Support to HIDE AND DISPLAY as requested by invoker
			if (requestArguments.containsKey("_toHideIds")) {

				List<String> ids = Serialization.convert(requestArguments.get("_toHideIds"), List.class);
				log.info("Requested to hide centroids {} ", ids);
				indexer.updateIsVisible(false, ids);
			}

			if (requestArguments.containsKey("_toDisplayIds")) {
				List<String> ids = Serialization.convert(requestArguments.get("_toDisplayIds"), List.class);

				log.info("Requested to display centroids {} ", ids);
				indexer.updateIsVisible(true, ids);
			}

			report.setStatus(Report.Status.OK);
		} catch (SDIInteractionException e) {
			log.error("Unable to index " + request, e);
			report.setStatus(Report.Status.ERROR);
			report.putMessage(e.getMessage());
		} catch (Throwable t) {
			log.error("Unable to index " + request, t);
			report.setStatus(Report.Status.ERROR);
			report.putMessage(t.getMessage());
		} finally {
			return report;
		}
	}

	/**
	 * Gets the mappings.
	 *
	 * @param useCaseDescriptor the use case descriptor
	 * @return the mappings
	 * @throws InvalidProfileException the invalid profile exception
	 */
	private List<MappingObject> getMappings(UseCaseDescriptor useCaseDescriptor) throws InvalidProfileException {
		return MappingObject.getMappingsFromUCD(useCaseDescriptor, getDescriptor().getId());
	}

	/**
	 * Deindex.
	 *
	 * @param request the request
	 * @return the index document report
	 * @throws InvalidPluginRequestException the invalid plugin request exception
	 */
	@Override
	public IndexDocumentReport deindex(IndexDocumentRequest request) throws InvalidPluginRequestException {
		log.info("Serving DeIndexer {} : request CallParameters {}, request Context {}: ", this.getDescriptor().getId(),
				request.getCallParameters(), request.getContext());
		log.debug("DeIndexer {} : Serving Request {} ", this.getDescriptor().getId(), request);
		IndexDocumentReport report = new IndexDocumentReport(request);
		try {
			PostgisIndexer indexer = getIndexer(request.getUseCaseDescriptor(), request.getCallParameters());
			indexer.removeByFieldValue(PostgisIndexer.StandardFields.PROJECT_ID, request.getDocument().getId());
			
			//***** Added by Francesco, see #25056
			//Replacing the "old" centroid if any. Creating an empty SpatialReference
			SpatialReference reference = new SpatialReference(new Document());
			log.info("DeIndexer project {}, Setting Spatial Reference empty {} ", request.getDocument().getId(), Serialization.write(reference));
			report.addIdentificationReference(reference);
			
		} catch (SDIInteractionException e) {
			log.error("Unable to index " + request, e);
			report.setStatus(Report.Status.ERROR);
			report.putMessage(e.getMessage());
		} catch (Throwable t) {
			log.error("Unable to index " + request, t);
			report.setStatus(Report.Status.ERROR);
			report.putMessage(t.getMessage());
		} finally {
			return report;
		}
	}

	/**
	 * Expected parameters : workspace indexName.
	 *
	 * @param request the request
	 * @return the index
	 * @throws ConfigurationException the configuration exception
	 */
	@Override
	public Index getIndex(BaseRequest request) throws ConfigurationException {
		try {
			return getIndexer(request.getUseCaseDescriptor(), request.getCallParameters()).getIndexConfiguration();
		} catch (Throwable t) {
			throw new ConfigurationException("Unable to get Postgis index for ucd "
					+ request.getUseCaseDescriptor().getId() + " in " + request.getContext(), t);
		}
	}

	// Inits index
	/**
	 * Gets the indexer.
	 *
	 * @param ucd the ucd
	 * @param params the params
	 * @return the indexer
	 * @throws ConfigurationException the configuration exception
	 * @throws SQLException the SQL exception
	 * @throws InvalidProfileException the invalid profile exception
	 * @throws SDIInteractionException the SDI interaction exception
	 */
	// TODO CACHE
	private PostgisIndexer getIndexer(UseCaseDescriptor ucd, Document params)
			throws ConfigurationException, SQLException, InvalidProfileException, SDIInteractionException {
		PostgisIndexer indexer = new PostgisIndexer(sdiCache.getObject(), ucd, postgisCache.getObject());

		List<MappingObject> mappingObjects = getMappings(ucd);
		List<PostgisTable.Field> fields = PostgisTable.Field.fromMappings(mappingObjects);

		indexer.initIndex(params.getString(IndexConstants.INDEX_PARAMETER_INDEXNAME), fields,
				params.getString(IndexConstants.INDEX_PARAMETER_WORKSPACE),
				params.getString(IndexConstants.INDEX_PARAMETER_INDEXNAME));
		return indexer;
	}

}
