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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.bson.Document;
import org.gcube.application.cms.implementations.utils.UserUtils;
import org.gcube.application.cms.plugins.MaterializationPlugin;
import org.gcube.application.cms.plugins.faults.InitializationException;
import org.gcube.application.cms.plugins.faults.InvalidPluginRequestException;
import org.gcube.application.cms.plugins.faults.MaterializationException;
import org.gcube.application.cms.plugins.faults.ShutDownException;
import org.gcube.application.cms.plugins.implementations.AbstractPlugin;
import org.gcube.application.cms.plugins.reports.InitializationReport;
import org.gcube.application.cms.plugins.reports.MaterializationReport;
import org.gcube.application.cms.plugins.reports.Report;
import org.gcube.application.cms.plugins.requests.MaterializationRequest;
import org.gcube.application.cms.sdi.engine.SDIManagerWrapper;
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.document.Project;
import org.gcube.application.geoportal.common.model.document.filesets.Materialization;
import org.gcube.application.geoportal.common.model.document.filesets.RegisteredFileSet;
import org.gcube.application.geoportal.common.model.document.filesets.sdi.GCubeSDILayer;
import org.gcube.application.geoportal.common.model.plugins.MaterializerPluginDescriptor;
import org.gcube.application.geoportal.common.model.plugins.PluginDescriptor;
import org.gcube.application.geoportal.common.model.useCaseDescriptor.Field;
import org.gcube.application.geoportal.common.model.useCaseDescriptor.UseCaseDescriptor;

import com.vdurmont.semver4j.Semver;

import lombok.Data;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;


/**
 * The Class SDIMaterializerPlugin.
 *
 * @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 SDIMaterializerPlugin extends AbstractPlugin implements MaterializationPlugin {

    /**
     * Instantiates a new materialization config.
     */
    @Data
    public static class MaterializationConfig{
        private String schemaField;
        private String documentPath;
    }

    Map<String, SDIManagerWrapper> sdiManagerMap=null;


    /**
     * Gets the SDI manager.
     *
     * @return the SDI manager
     */
    private SDIManagerWrapper getSDIManager(){
        String context = UserUtils.getCurrent().getContext();
        log.debug("Getting SDIManagerWrapper from cache map for context {}");
        return sdiManagerMap.get(context);
    }

    /**
     * Inits the in context.
     *
     * @return the initialization report
     * @throws InitializationException the initialization exception
     */
    @Override
    @Synchronized
    public InitializationReport initInContext() throws InitializationException {
        InitializationReport report = new InitializationReport();
        try{
            String context = UserUtils.getCurrent().getContext();
            if(getSDIManager()==null) {
                log.info("Initializing in " + context);
                sdiManagerMap.put(context,new SDIManagerWrapper());
            }
            report.setStatus(Report.Status.OK);
            report.putMessage("Initialized SDI Manager in "+context+" pointing to "+getSDIManager().getGeoserverHostName());
        } catch (SDIInteractionException e) {
            throw new InitializationException("Unable to initialize SDI Manager ",e);
        }
        return report;
    }

    /**
     * Inits the.
     *
     * @return the initialization report
     * @throws InitializationException the initialization exception
     */
    @Override
    @Synchronized
    public InitializationReport init() throws InitializationException {
        InitializationReport report = new InitializationReport();
        sdiManagerMap=new HashMap<>();
        report.setStatus(Report.Status.OK);
        return report;
    }

    /**
     * Shutdown.
     *
     * @throws ShutDownException the shut down exception
     */
    @Override
    public void shutdown() throws ShutDownException {

    }

    /**
     * Expected params :
     *          -workspace MANDATORY
     *          -titleField.
     *
     * @param request the request
     * @return the materialization report
     * @throws MaterializationException the materialization exception
     * @throws InvalidPluginRequestException the invalid plugin request exception
     */
    @Override
    public MaterializationReport materialize(MaterializationRequest request) throws MaterializationException, InvalidPluginRequestException {
        log.info("Serving Materialize {} : request CallParameters {}, request Context {}",this.getDescriptor().getId(), request.getCallParameters(), request.getContext());
        log.debug("Materialize request {}",request);
        Project project =request.getDocument();
        UseCaseDescriptor useCaseDescriptor = request.getUseCaseDescriptor();
        Document requestArguments=Serialization.convert(request.getCallParameters(),Document.class);

        MaterializationReport report= new MaterializationReport(request);

        try{
            Document profileConfiguration =getConfigurationFromProfile(useCaseDescriptor).getConfiguration();
            log.debug("UseCaseDescriptor Configuration is {} ",profileConfiguration);
            JSONPathWrapper documentNavigator=new JSONPathWrapper(project.getTheDocument().toJson());
            JSONPathWrapper schemaNavigator=new JSONPathWrapper(useCaseDescriptor.getSchema().toJson());

            for(Object fsConfigObj : profileConfiguration.get("registeredFileSetPaths", List.class)){
                log.debug("Managing {} ",fsConfigObj);
                MaterializationConfig fsConfig=Serialization.convert(fsConfigObj,MaterializationConfig.class);
                List matchingFieldDefinitions =schemaNavigator.getByPath(fsConfig.getSchemaField());
                if(matchingFieldDefinitions==null || matchingFieldDefinitions.isEmpty())
                    throw new MaterializationException("Invalid Field Definition path in configuration [NO MATCH] : "+
                            fsConfig.getSchemaField());
                if(matchingFieldDefinitions.size()>1)
                    throw new MaterializationException("Invalid Field Definition path in configuration [MATCHES "+matchingFieldDefinitions.size()+"] : "+
                            fsConfig.getSchemaField());

                Field f = Serialization.convert(matchingFieldDefinitions.get(0),Field.class);

                // TODO Check if schema points to RegisteredFileSet

                for(Object fsObject : documentNavigator.getByPath(fsConfig.getDocumentPath())){
                    RegisteredFileSet fs = Serialization.convert(fsObject,RegisteredFileSet.class);
                    log.debug("Found {} ",fs);
                    requestArguments.putIfAbsent("basePersistencePath", useCaseDescriptor.getId());
                    requestArguments.putIfAbsent("documentID", project.getId());
                    if(requestArguments.containsKey("titleField"))
                        requestArguments.putIfAbsent("layerTitle",fs.getString(requestArguments.getString("titleField")));
                    else requestArguments.putIfAbsent("layerTitle",fs.getUUID());

                    //Add FS uuid at ws_baseName
                    String ws = request.getMandatory("workspace")+"_"+ project.getId();
                    ws = ws.replaceAll("\\W","_");
                    log.trace("Escaped WS will be {} ",ws);

                    requestArguments.put("workspace",ws);

                    // Actually materializing
                    log.info("Calling materializeLayer: fs {} - requestArguments {}", fs, requestArguments);
                    RegisteredFileSet obtained = getSDIManager().materializeLayer(fs,requestArguments);
                    log.debug("Obtained {} ",obtained);
                    documentNavigator.setElement("$..[?(@."+ RegisteredFileSet.UUID+" == '"+fs.getUUID()+"')]",obtained);
                }
            }

            String finalDocument = documentNavigator.getValueCTX().jsonString();
            log.debug("Final document after materialization is {} ",finalDocument);
            report.setResultingDocument(Document.parse(finalDocument));
            report.setStatus(Report.Status.OK);
        }catch (SDIInteractionException e){
            log.error("Unable to materialize "+request,e);
            report.setStatus(Report.Status.ERROR);
            report.putMessage(e.getMessage());
        }catch (Throwable t){
            log.error("Exception, Unable to materialize "+request,t);
            report.setStatus(Report.Status.ERROR);
            report.putMessage(t.getMessage());
        }finally{
            return report;
        }
    }

    /**
     * Expected parameters
     *      - fileSetPath.
     *
     * @param request the request
     * @return the materialization report
     * @throws MaterializationException the materialization exception
     * @throws InvalidPluginRequestException the invalid plugin request exception
     */
    @Override
    public MaterializationReport dematerialize(MaterializationRequest request) throws MaterializationException, InvalidPluginRequestException {
        log.info("Serving DeMaterialize {} : request CallParameters {}, request Context {}: ",this.getDescriptor().getId(), request.getCallParameters(), request.getContext());
        log.debug("DeMaterialize request {}",request);
        MaterializationReport report= new MaterializationReport(request);
        try{
            SDIManagerWrapper sdi=getSDIManager();
            JSONPathWrapper wrapper = new JSONPathWrapper(request.getDocument().getTheDocument().toJson());
            for(String s : wrapper.getMatchingPaths(request.getMandatory("fileSetPath"))){
                log.debug("Found matching {} ",s);
                RegisteredFileSet registeredFileSet=Serialization.convert(wrapper.getByPath(s).get(0),RegisteredFileSet.class);
                List<Object> toKeep = new ArrayList<>();
                if(registeredFileSet.getMaterializations()!=null)
                    for(Object matObj : registeredFileSet.getMaterializations()){
                        Materialization mat = Serialization.convert(matObj,Materialization.class);
                        if(mat.getType().equals(GCubeSDILayer.GCUBE_SDY_LAYER_TYPE)) {
                            log.debug("Deleting Layer {} ",mat);
                            sdi.deleteLayer(Serialization.convert(matObj, GCubeSDILayer.class));
                        }else toKeep.add(matObj);
                    }
                // Resetting remaining materializations
                registeredFileSet.put(RegisteredFileSet.MATERIALIZATIONS,toKeep);
                // Update FS in doc
                wrapper.setElement(s,registeredFileSet);

            }
                // Resetting Document
                report.setResultingDocument(Document.parse(wrapper.getValueCTX().jsonString()));
        }catch (SDIInteractionException e){
            log.error("Unable to materialize "+request,e);
            report.setStatus(Report.Status.ERROR);
            report.putMessage(e.getMessage());
        }catch (Throwable t){
            log.error("Unable to materialize "+request,t);
            report.setStatus(Report.Status.ERROR);
            report.putMessage(t.getMessage());
        }finally{
            return report;
        }
    }

    static final PluginDescriptor DESCRIPTOR=new PluginDescriptor("SDI-Default-Materializer", MaterializerPluginDescriptor.MATERIALIZER);
    static {
        DESCRIPTOR.setDescription("SDI Materializer. " +
                "This plugin materialize FileSets in gCube SDI.");
        DESCRIPTOR.setVersion(new Semver("1.0.0"));
    }

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