package org.gcube.application.geoportal.service.rest;

import com.webcohesion.enunciate.metadata.rs.RequestHeader;
import com.webcohesion.enunciate.metadata.rs.RequestHeaders;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.gcube.application.cms.implementations.ImplementationProvider;
import org.gcube.application.cms.serialization.Serialization;
import org.gcube.application.geoportal.common.model.configuration.Configuration;
import org.gcube.application.geoportal.common.model.document.Project;
import org.gcube.application.geoportal.common.model.document.access.Access;
import org.gcube.application.geoportal.common.model.document.relationships.Relationship;
import org.gcube.application.geoportal.common.model.document.relationships.RelationshipNavigationObject;
import org.gcube.application.geoportal.common.model.rest.ConfigurationException;
import org.gcube.application.geoportal.common.model.rest.QueryRequest;
import org.gcube.application.geoportal.common.model.rest.RegisterFileSetRequest;
import org.gcube.application.geoportal.common.model.rest.StepExecutionRequest;
import org.gcube.application.geoportal.common.rest.InterfaceConstants;
import org.gcube.application.geoportal.service.engine.mongo.ProfiledMongoManager;
import org.gcube.application.geoportal.service.engine.providers.ConfigurationCache;

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.List;

@Path(InterfaceConstants.Methods.PROJECTS+"/{"+InterfaceConstants.Parameters.UCID +"}")
@Slf4j
@RequestHeaders({
        @RequestHeader( name = "Authorization", description = "Bearer token, see https://dev.d4science.org/how-to-access-resources"),
        @RequestHeader( name = "Content-Type", description = "application/json")
})
public class ProfiledDocuments {

    private ProfiledMongoManager manager;

    public ProfiledDocuments(@PathParam(InterfaceConstants.Parameters.UCID) String profileID) throws ConfigurationException {
        log.info("Accessing profile "+profileID);
        manager=new GuardedMethod<ProfiledMongoManager>(){
            @Override
            protected ProfiledMongoManager run() throws Exception {
                    return new ProfiledMongoManager(profileID);
            }
        }.execute().getResult();
    }

    @GET
    @Path(InterfaceConstants.Methods.CONFIGURATION_PATH)
    @Produces(MediaType.APPLICATION_JSON)
    public Configuration getConfiguration(@PathParam(InterfaceConstants.Parameters.UCID) String profileID){
        return new GuardedMethod<Configuration>(){

            @Override
            protected Configuration run() throws Exception, WebApplicationException {
                return ImplementationProvider.get().getProvidedObjectByClass(ConfigurationCache.ConfigurationMap.class).get(profileID);
            }
        }.execute().getResult();
    }


    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Project createNew(Document d) {
        return new GuardedMethod<Project>() {
            @Override
            protected Project run() throws Exception, WebApplicationException {
                log.info("Creating new Project ({})",manager.getUseCaseDescriptor().getId());
                Project toReturn= manager.registerNew(d);
                log.info("Created new Project ({}, ID {})",manager.getUseCaseDescriptor().getId(),toReturn.getId());
                return toReturn;
            }
        }.execute().getResult();
    }


    @PUT
    @Path("{"+InterfaceConstants.Parameters.PROJECT_ID+"}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Project update(@PathParam(InterfaceConstants.Parameters.PROJECT_ID) String documentId, Document d) {
        return new GuardedMethod<Project>() {
            @Override
            protected Project run() throws Exception, WebApplicationException {
                log.info("Updating Project ({}, ID {})",manager.getUseCaseDescriptor().getId(),documentId);
                return manager.update(documentId,d);
            }
        }.execute().getResult();
    }


    @DELETE
    @Produces(MediaType.APPLICATION_JSON)
    @Path("{"+InterfaceConstants.Parameters.PROJECT_ID+"}")
    public Boolean delete(@PathParam(InterfaceConstants.Parameters.PROJECT_ID) String id,
                           @DefaultValue("false")
                           @QueryParam(InterfaceConstants.Parameters.FORCE) Boolean force) {
        return new GuardedMethod<Boolean>() {
            @Override
            protected Boolean run() throws Exception, WebApplicationException {
                log.info("Deleting Project ({}, ID {}). Force is {}",manager.getUseCaseDescriptor().getId(),id,force);
                manager.delete(id,force);
                return true;
            }
        }.execute().getResult();
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/"+InterfaceConstants.Methods.REGISTER_FILES_PATH+"/{"+InterfaceConstants.Parameters.PROJECT_ID+"}")
    public Project registerFileSet(@PathParam(InterfaceConstants.Parameters.PROJECT_ID) String id,
                                   RegisterFileSetRequest request) {
        return new GuardedMethod<Project>() {
            @Override
            protected Project run() throws Exception, WebApplicationException {
                log.info("UCD {} : Project {} Registering Fileset. Request is {}",
                        manager.getUseCaseDescriptor().getId(),
                        id,request);
                request.validate();
                return manager.registerFileSet(id,request);
            }
        }.execute().getResult();
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/"+InterfaceConstants.Methods.DELETE_FILES_PATH+"/{"+InterfaceConstants.Parameters.PROJECT_ID+"}")
    public Project deleteFileSet(
            @PathParam(InterfaceConstants.Parameters.PROJECT_ID) String id,
            @DefaultValue("false")
            @QueryParam(InterfaceConstants.Parameters.FORCE) Boolean force,
            String path) {
        return new GuardedMethod<Project>() {
            @Override
            protected Project run() throws Exception, WebApplicationException {
                log.info("Deleting FileSet of Project ({}, ID {}) at path {}. Force is {}",
                        manager.getUseCaseDescriptor().getId(),
                        id,path,force);
                return manager.deleteFileSet(id,path,force);
            }
        }.execute().getResult();
    }


    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/"+InterfaceConstants.Methods.STEP+"/{"+InterfaceConstants.Parameters.PROJECT_ID+"}")
    public Project performStep(
            @PathParam(InterfaceConstants.Parameters.PROJECT_ID) String id,
            StepExecutionRequest request) {
        return new GuardedMethod<Project>() {
            @Override
            protected Project run() throws Exception, WebApplicationException {
                log.info("Executing step {} on Project ({},ID,{}) with options {}",
                        request.getStepID(),
                        manager.getUseCaseDescriptor().getId(),
                        id,request.getOptions());
                return manager.performStep(id,request.getStepID(),request.getOptions());
            }
        }.execute().getResult();
    }

    @PUT
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/"+InterfaceConstants.Methods.FORCE_UNLOCK+"/{"+InterfaceConstants.Parameters.PROJECT_ID+"}")
    public Project forceUnlock(
            @PathParam(InterfaceConstants.Parameters.PROJECT_ID) String id){
        return new GuardedMethod<Project>(){

            @Override
            protected Project run() throws Exception, WebApplicationException {
                log.warn("UCD {}, forcing unlock for Project ID {}",manager.getUseCaseDescriptor().getId(),id);
                return manager.forceUnlock(id);
            }
        }.execute().getResult();
    }

    @PUT
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/"+InterfaceConstants.Methods.SET_PROJECT_ACCESS_POLICY+"/{"+InterfaceConstants.Parameters.PROJECT_ID+"}")
    public Project setAccessPolicy(
            @PathParam(InterfaceConstants.Parameters.PROJECT_ID) String id, Access toSet){
        return new GuardedMethod<Project>(){

            @Override
            protected Project run() throws Exception, WebApplicationException {
                log.warn("UCD {}, setting Policy {} Project ID {}",manager.getUseCaseDescriptor().getId(),toSet,id);
                return manager.setAccessPolicy(id,toSet);
            }
        }.execute().getResult();
    }

    //********************************** READ

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Iterable<?> list() {
        return new GuardedMethod<Iterable<?>>() {
            protected Iterable<?> run() throws Exception ,WebApplicationException {
                return manager.query(new QueryRequest());
            };
        }.execute().getResult();
    }

    // BY ID
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("{"+InterfaceConstants.Parameters.PROJECT_ID+"}")
    public Project getById(@PathParam(InterfaceConstants.Parameters.PROJECT_ID) String id) {
        return new GuardedMethod<Project>() {
            @Override
            protected Project run() throws Exception, WebApplicationException {
                return manager.getByID(id);
            }
        }.execute().getResult();
    }


    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/"+InterfaceConstants.Methods.SEARCH_PATH)
    public String search(String filter){
        return new GuardedMethod<String>() {
            @Override
            protected String run() throws Exception, WebApplicationException {
                QueryRequest req=new QueryRequest();
                req.setFilter(Document.parse(filter));
                return Serialization.write(manager.query(req));
            }
        }.execute().getResult();
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/"+InterfaceConstants.Methods.QUERY_PATH)
    public Iterable<?> query(String queryString){
        return new GuardedMethod<Iterable<?>>() {
            @Override
            protected Iterable<?> run() throws Exception, WebApplicationException {
                return manager.query(Serialization.parseQuery(queryString));
            }
        }.execute().getResult();
    }

    // Relationships

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("{"+InterfaceConstants.Methods.RELATIONSHIP+"}/{"+InterfaceConstants.Parameters.PROJECT_ID+"}" +
            "/{"+InterfaceConstants.Parameters.RELATIONSHIP_ID+"}")
    public String getRelationshipChain(@PathParam(InterfaceConstants.Parameters.PROJECT_ID) String id,
                                       @PathParam(InterfaceConstants.Parameters.RELATIONSHIP_ID) String relationshipId,
                                       @DefaultValue("false")
                                       @QueryParam(InterfaceConstants.Parameters.DEEP) Boolean deep) {
        return new GuardedMethod<String>(){
            @Override
            protected String run() throws Exception, WebApplicationException {
                // recursive
                log.info("UCD {} : Getting Relationships List for {} [rel : {}, recurse {}]",
                        manager.getUseCaseDescriptor().getId(),id,relationshipId,deep);
                Project current = manager.getByID(id);
                long startTime=System.currentTimeMillis();
                List<RelationshipNavigationObject> toReturn = getLinked(current,relationshipId,deep);
                log.info("Got {} relationship elements in {}ms",toReturn.size(),(System.currentTimeMillis()-startTime));
                return Serialization.write(toReturn);
            }
        }.execute().getResult();
    }


    @PUT
    @Produces(MediaType.APPLICATION_JSON)
    @Path("{"+InterfaceConstants.Methods.RELATIONSHIP+"}/{"+InterfaceConstants.Parameters.PROJECT_ID+"}" +
            "/{"+InterfaceConstants.Parameters.RELATIONSHIP_ID+"}")
    public Project setRelation(@PathParam(InterfaceConstants.Parameters.PROJECT_ID) String id,
                                       @PathParam(InterfaceConstants.Parameters.RELATIONSHIP_ID) String relationshipId,
                                       @QueryParam(InterfaceConstants.Parameters.TARGET_ID) String targetId,
                                       @QueryParam(InterfaceConstants.Parameters.TARGET_UCD) String targetUCD) {
        return new GuardedMethod<Project>() {
            @Override
            protected Project run() throws Exception, WebApplicationException {
                log.info("Set relation from Project ({} : {}) [{}]->  ({} : {})",
                        manager.getUseCaseDescriptor().getId(), id,relationshipId,targetUCD,targetId);
                String toUseTargetUCD=targetUCD;
                if(toUseTargetUCD==null || toUseTargetUCD.isEmpty()) {
                    log.debug("Target UCD is null, forcing same UCD () as source ",manager.getUseCaseDescriptor().getId());
                    toUseTargetUCD = manager.getUseCaseDescriptor().getId();
                }
                return manager.setRelation(id,relationshipId,toUseTargetUCD,targetId);
            }
        }.execute().getResult();
    }


    @DELETE
    @Produces(MediaType.APPLICATION_JSON)
    @Path("{"+InterfaceConstants.Methods.RELATIONSHIP+"}/{"+InterfaceConstants.Parameters.PROJECT_ID+"}" +
            "/{"+InterfaceConstants.Parameters.RELATIONSHIP_ID+"}")
    public Project deleteRelation(@PathParam(InterfaceConstants.Parameters.PROJECT_ID) String id,
                               @PathParam(InterfaceConstants.Parameters.RELATIONSHIP_ID) String relationshipId,
                               @QueryParam(InterfaceConstants.Parameters.TARGET_ID) String targetId,
                               @QueryParam(InterfaceConstants.Parameters.TARGET_UCD) String targetUCD) {
        return new GuardedMethod<Project>() {
            @Override
            protected Project run() throws Exception, WebApplicationException {
                log.info("Deleting relation from Project ({} : {}) [{}]->  ({} : {})",
                        manager.getUseCaseDescriptor().getId(), id,relationshipId,targetUCD,targetId);
                return manager.deleteRelation(id,relationshipId,targetUCD,targetId);
            }
        }.execute().getResult();
    }



    private static List<RelationshipNavigationObject> getLinked(Project current, String relationName, Boolean recurse)throws Exception{
        log.debug("Getting Relationships Lists for {} [rel : {}, recurse {}]",current.getId(),relationName,recurse);
        ArrayList<RelationshipNavigationObject> toReturn = new ArrayList<>();
        List<Relationship> existing = current.getRelationshipsByName(relationName);
        for (Relationship relationship : existing) {
            try{
                log.trace("Navigating from  {} : {} to[rel {} ] {} : {}",relationship.getTargetUCD(),
                        relationship.getTargetID(),relationship.getRelationshipName(),current.getProfileID(),current.getId());
                RelationshipNavigationObject linkedProject = new RelationshipNavigationObject();
                linkedProject.setTarget(new ProfiledMongoManager(relationship.getTargetUCD()).getByID(relationship.getTargetID()));
                if(recurse) linkedProject.setChildren(getLinked(linkedProject.getTarget(),relationName,recurse));
                toReturn.add(linkedProject);
            } catch (Exception e) {
                log.warn("Unable to navigate from  {} : {} to[rel {} ] {} : {}",relationship.getTargetUCD(),
                        relationship.getTargetID(),relationship.getRelationshipName(),current.getProfileID(),current.getId(),e);
            }
        }
        return toReturn;
    }
}
