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

import com.vdurmont.semver4j.Semver;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
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.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.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.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.geojson.Crs;
import org.geojson.GeoJsonObject;
import org.geojson.LngLatAlt;
import org.geojson.Point;

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

@Slf4j
public class SDIIndexerPlugin extends SDIAbstractPlugin implements IndexerPluginInterface {

    @Data
    private static class MappingObject{
        private String name;
        private String type;
        private String path;

        public void validate () throws InvalidProfileException {
            if(name==null) throw new InvalidProfileException("Invalid mapping "+this+" : name is null");
            if(type==null) throw new InvalidProfileException("Invalid mapping "+this+" : type is null");
            if(path==null) throw new InvalidProfileException("Invalid mapping "+this+" : path is null");
        }
    }



    static final PluginDescriptor DESCRIPTOR=new PluginDescriptor("SDI-Indexer-Plugin",
            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());



    }

    @Override
    public PluginDescriptor getDescriptor() {
        return DESCRIPTOR;
    }


    @Override
    public InitializationReport initInContext() throws InitializationException {
        InitializationReport report = new InitializationReport();
        report.setStatus(Report.Status.OK);
        return report;
    }

    /**
     * Expected parameters :
     *          - indexName (unique)
     *          - workspace
     *          - centroidRecord (OPT)
     *
     * @param request
     * @return
     */

    @Override
    public IndexDocumentReport index(IndexDocumentRequest request) throws InvalidPluginRequestException {

        log.info("Indexer {} : Serving Index Request {} ",this.getDescriptor().getId(),request);

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

        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("projectid", project.getId());

            // **********************   EVALAUTE POSITION
            log.debug("indexing UseCaseDescriptor {} : Evaluating Centroid... ", useCaseDescriptor.getId());
            SpatialReference reference =null;
            List<IdentificationReference> refs=project.getIdentificationReferenceByType(SpatialReference.SPATIAL_REFERENCE_TYPE);
            if(!refs.isEmpty()){

                // Use existing Reference

                reference = Serialization.convert(refs.get(0), SpatialReference.class);

                log.debug("Using already defined spatial reference " + reference);


                GeoJsonObject object = Serialization.convert(reference.getGeoJson(), GeoJsonObject.class);

                GCubeSDILayer.BBOX bbox = GCubeSDILayer.BBOX.fromGeoJSON(object.getBbox());

                log.info("Found declared BBOX {} ", bbox);
                Double pointX = (bbox.getMaxX() - bbox.getMinX());
                Double pointY = bbox.getMaxY() - bbox.getMinY();
                String wkt = String.format("POINT (%1$f %2$f) ",
                        pointX, pointY);


                centroidDoc.put("geom", wkt);

            } else{
                // 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());
                Double pointY = toSet.getMaxY()-toSet.getMinY();
                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())
                    toSetValue=foundValues.get(0);
                log.trace("Setting {} = {} in centroid doc ",m.getName(),toSetValue);
                centroidDoc.put(m.getName(),foundValues.get(0));
            }

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

            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;
        }
    }

    @Override
    public IndexDocumentReport deindex(IndexDocumentRequest request) throws InvalidPluginRequestException {
        log.info("Indexer {} : Serving Index Request {} ",this.getDescriptor().getId(),request);
        IndexDocumentReport report= new IndexDocumentReport(request);
        try{
            PostgisIndexer indexer = getIndexer(request.getUseCaseDescriptor(),request.getCallParameters());
            indexer.removeByFieldValue(Fields.PROJECT_ID,request.getDocument().getId());
        }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
     * @return
     * @throws ConfigurationException
     */
    @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
    // 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 = getFields(mappingObjects);


        indexer.initIndex(params.getString("indexName"),
                fields,
                params.getString("workspace"),
                params.getString("indexName"));
        return indexer;
    }


    private static class Fields{
        public static final PostgisTable.Field PROJECT_ID= new PostgisTable.Field("projectid", PostgisTable.FieldType.TEXT);
        public static final PostgisTable.Field GEOM= new PostgisTable.Field("geom", PostgisTable.FieldType.GEOMETRY);

    }


    private List<PostgisTable.Field> getFields(List<MappingObject> mappings){
        List<PostgisTable.Field> fields = new ArrayList<>(); // TODO From UseCaseDescriptor
        fields.add(Fields.GEOM);
        fields.add(Fields.PROJECT_ID);

        mappings.forEach(m -> {
            fields.add(new PostgisTable.Field(m.getName(), PostgisTable.FieldType.valueOf(m.getType())));
        });

        return fields;
    }


    private List<MappingObject> getMappings(UseCaseDescriptor ucd) throws InvalidProfileException {
        log.debug("UseCaseDescriptor {} : Evaluating Index schema.. ", ucd.getId());
        Document profileConfiguration = getConfigurationFromProfile(ucd).getConfiguration();
        List mappingObjs= profileConfiguration.get("explicitFieldMapping",List.class);
        log.trace("Loading mappings from useCaseDescriptor.. ");
        List<MappingObject> mappingObjects= new ArrayList<>();
        if(mappingObjs!=null){
            for (Object mappingObj : mappingObjs) {
                log.trace("Mapping is {} ",mappingObj);
                MappingObject m = Serialization.convert(mappingObj,MappingObject.class);
                m.validate();
            }
        }
        return mappingObjects;
    }
}
