package org.gcube.application.geoportal.client;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.gcube.application.geoportal.client.utils.Serialization;
import org.gcube.application.geoportal.common.faults.InvalidRequestException;
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.RelationshipNavigationObject;
import org.gcube.application.geoportal.common.model.rest.*;
import org.gcube.application.geoportal.common.rest.InterfaceConstants;
import org.gcube.application.geoportal.common.rest.Projects;
import org.gcube.common.clients.Call;
import org.gcube.common.clients.delegates.ProxyDelegate;

import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.Iterator;

@RequiredArgsConstructor
@Slf4j
public class DefaultDocumentsClient<T extends Project> implements Projects<T> {

    @NonNull
    protected final ProxyDelegate<WebTarget> delegate;
    @NonNull
    protected final String profileID;
    @NonNull
    protected final Class<T> managedClass;


    public Class<T> getManagedClass() {
        return managedClass;
    }

    @Override
    public T createNew(Document toCreate) throws RemoteException {
        try {
            log.debug("Creating Profiled Document (class {}, useCaseDescriptor {}) with content {} ",
                    getManagedClass(),profileID, toCreate);
            Call<WebTarget, T> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).request(MediaType.APPLICATION_JSON).
                        post(Entity.entity(toCreate, MediaType.APPLICATION_JSON)),getManagedClass());
            };
            T toReturn = delegate.make(call);
            log.info("Registered {} profiled {} ",toReturn.getId(),profileID);
            return toReturn;
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public void deleteById(String id) throws RemoteException {
        deleteById(id,false);
    }

    @Override
    public void deleteById(String id, Boolean force) throws RemoteException {
        try {
            log.debug("Deleting ID {}  useCaseDescriptor {}  force {} ", id, profileID, force);
            Call<WebTarget, T> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).path(id).
                        queryParam(InterfaceConstants.Parameters.FORCE,force).
                        request(MediaType.APPLICATION_JSON).delete(),null);
            };
            delegate.make(call);
            log.info("Deleted ID {}  useCaseDescriptor {}  force {} ", id, profileID, force);
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public T getById(String id) throws RemoteException {
        try {
            log.info("Loading Document ID {} (class {}, useCaseDescriptor {})",
                    id, getManagedClass(),profileID);
            Call<WebTarget, T> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).path(id).
                        request(MediaType.APPLICATION_JSON).get(), getManagedClass());
            };
            return  delegate.make(call);
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public Configuration getConfiguration() throws RemoteException {
        try {
            log.info("Loading Configuration for useCaseDescriptor {}", profileID);
            Call<WebTarget, Configuration> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).path(InterfaceConstants.Methods.CONFIGURATION_PATH).
                        request(MediaType.APPLICATION_JSON).get(), Configuration.class);
            };
            return delegate.make(call);
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public Iterator<T> query(QueryRequest request) throws RemoteException {
        return queryForClass(request,getManagedClass());
    }

    @Override
    public <C> Iterator<C> queryForClass(QueryRequest request,Class<C> clazz) throws RemoteException {
        String jsonString=queryForJSON(request);
        log.debug("Deserializing query Result as {} ",clazz);
        try {
            return Serialization.readCollection(jsonString,clazz);
        } catch (IOException e) {
            log.error("Unable to deserialize result as "+clazz,e);
            log.debug("Query request was {} ",request);
            log.debug("Query result was {} ",jsonString);
            throw new RemoteException("Invalid format for submitted query");
        }
    }

    @Override
    public String queryForJSON(QueryRequest request) throws RemoteException {
        try {
            log.debug("Querying useCaseDescriptor {}  for {}",profileID,request);
            Call<WebTarget, String> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).path(InterfaceConstants.Methods.QUERY_PATH).
                        request(MediaType.APPLICATION_JSON).post(Entity.entity(request,MediaType.APPLICATION_JSON)), String.class);
            };
            return delegate.make(call);
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public T performStep(String id, StepExecutionRequest request) throws RemoteException{
        try {
            log.debug("Executing step on {} (class {}, useCaseDescriptor {}) with request {} ",
                    id, getManagedClass(),profileID, request);
            Call<WebTarget, T> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).path(InterfaceConstants.Methods.STEP)
                        .path(id).request(MediaType.APPLICATION_JSON).
                        post(Entity.entity(request, MediaType.APPLICATION_JSON)),getManagedClass());
            };
            T toReturn = delegate.make(call);
            log.info("Executed STEP {} on {} [useCaseDescriptor {}, class {}] ",request.getStepID(),
                    id,profileID,getManagedClass());
            return toReturn;
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public T registerFileSet(String id, RegisterFileSetRequest req) throws RemoteException, InvalidRequestException {
        try {
            log.debug("Registering FileSet on {} (class {}, useCaseDescriptor {}) with request {} ",
                    id, getManagedClass(), profileID, req);
            req.validate();
            Call<WebTarget, T> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).path(InterfaceConstants.Methods.REGISTER_FILES_PATH)
                        .path(id).request(MediaType.APPLICATION_JSON).
                                post(Entity.entity(req, MediaType.APPLICATION_JSON)), getManagedClass());
            };
            T toReturn = delegate.make(call);
            log.info("Registered FileSet on {} [useCaseDescriptor {}, class {}]  with {}",
                    id, profileID, getManagedClass(), req);
            return toReturn;
        }catch (InvalidRequestException e){
            log.error("Invalid Request ",e);
            throw e;
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public T deleteFileSet(String id, String path, Boolean force) throws RemoteException {
        try {
            log.debug("Deleting Fileset for ID {}  [useCaseDescriptor {}  , class {}] at {} (force {} )",
                    id, profileID,getManagedClass(),path, force);
            Call<WebTarget, T> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).path(InterfaceConstants.Methods.DELETE_FILES_PATH).
                        path(id).queryParam(InterfaceConstants.Parameters.FORCE,force).
                        request(MediaType.APPLICATION_JSON).
                        post(Entity.entity(path, MediaType.APPLICATION_JSON)),getManagedClass());
            };
            T toReturn=delegate.make(call);
            log.info("Deleted ID {}  useCaseDescriptor {}  force {} ", id, profileID, force);
            return toReturn;
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public T forceUnlock(String id) throws RemoteException {
        try {
            log.warn("Force Unlock of {} [useCaseDescriptor {} , class {}]",
                    id, profileID,getManagedClass());
            Call<WebTarget, T> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).path(InterfaceConstants.Methods.FORCE_UNLOCK).path(id).
                        request(MediaType.APPLICATION_JSON).
                        put(Entity.json("")),getManagedClass());
            };
            T toReturn=delegate.make(call);
            log.info("Unlocked ID {} useCaseDescriptor {}", id, profileID);
            return toReturn;
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public T setAccessPolicy(String id, Access toSet) throws RemoteException {
        try {
            log.info("Setting Access of {} [useCaseDescriptor {} , class {}] as {}",
                    id, profileID,getManagedClass(),toSet);
            Call<WebTarget, T> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).path(InterfaceConstants.Methods.SET_PROJECT_ACCESS_POLICY).path(id).
                        request(MediaType.APPLICATION_JSON).
                        put(Entity.json(toSet)),getManagedClass());
            };
            T toReturn=delegate.make(call);
            log.debug("Updated Access of ID {} useCaseDescriptor {}", id, profileID);
            return toReturn;
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public T updateDocument(String id, Document updatedDocument) throws RemoteException {
        try {
            log.debug("Updating {} [useCaseDescriptor {} , class {}] with ",
                    id, profileID,getManagedClass(),updatedDocument);
            Call<WebTarget, T> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).path(id).
                        request(MediaType.APPLICATION_JSON).
                        put(Entity.entity(updatedDocument, MediaType.APPLICATION_JSON)),getManagedClass());
            };
            T toReturn=delegate.make(call);
            log.info("Updated ID {} useCaseDescriptor {}", id, profileID);
            return toReturn;
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public Project setRelation(CreateRelationshipRequest request) throws RemoteException {
        try {
            log.debug("Setting relationship {}:{} --{}--> {}:{}",
                    profileID, request.getProjectId(), request.getRelationshipId(),
                    request.getTargetUCD(),request.getTargetId());
            Call<WebTarget, T> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).
                                path(InterfaceConstants.Methods.RELATIONSHIP).
                                path(request.getRelationshipId()).
                                queryParam(InterfaceConstants.Parameters.TARGET_ID,request.getTargetId()).
                                queryParam(InterfaceConstants.Parameters.TARGET_UCD,request.getTargetUCD()).
                                request(MediaType.APPLICATION_JSON).
                                put(Entity.entity(null, MediaType.APPLICATION_JSON)),getManagedClass());
            };
            T toReturn=delegate.make(call);
            log.info("Set relationship {}:{} --{}--> {}:{}",
                    profileID, request.getProjectId(), request.getRelationshipId(),
                    request.getTargetUCD(),request.getTargetId());
            return toReturn;
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public Project deleteRelation(DeleteRelationshipRequest request) throws RemoteException {
        try {
            log.debug("Deleting relationship {}:{} --{}--> {}:{}",
                    profileID, request.getProjectId(), request.getRelationshipId(),
                    request.getTargetUCD(),request.getTargetId());
            Call<WebTarget, T> call = endpoint -> {
                return ResponseCommons.check(endpoint.path(profileID).
                        path(InterfaceConstants.Methods.RELATIONSHIP).
                        path(request.getRelationshipId()).
                        queryParam(InterfaceConstants.Parameters.TARGET_ID,request.getTargetId()).
                        queryParam(InterfaceConstants.Parameters.TARGET_UCD,request.getTargetUCD()).
                        request(MediaType.APPLICATION_JSON).
                        delete(),getManagedClass());
            };
            T toReturn=delegate.make(call);
            log.info("Deleted relationship {}:{} --{}--> {}:{}",
                    profileID, request.getProjectId(), request.getRelationshipId(),
                    request.getTargetUCD(),request.getTargetId());
            return toReturn;
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }

    @Override
    public Iterator<RelationshipNavigationObject> getRelationshipChain(String id, String relationId) throws RemoteException {
        return getRelationshipChain(id,relationId,null);
    }

    @Override
    public Iterator<RelationshipNavigationObject> getRelationshipChain(String id, String relationId, Boolean deep) throws RemoteException {
        try {
            log.debug("Get relationship chain ID {} for {} [useCaseDescriptor {} , class {}]",
                    relationId, id, profileID,getManagedClass());
            Call<WebTarget, Iterator<RelationshipNavigationObject>> call = endpoint -> {

                WebTarget target = endpoint.path(profileID).
                        path(InterfaceConstants.Methods.RELATIONSHIP).
                        path(id).
                        path(relationId);

                if(deep!=null) target = target.queryParam(InterfaceConstants.Parameters.DEEP,deep);

                String jsonChain = ResponseCommons.check(target.request(MediaType.APPLICATION_JSON).
                        get(),String.class);

                return Serialization.readCollection(jsonChain,RelationshipNavigationObject.class);
            };
            return delegate.make(call);
        }catch(RemoteException e){
            log.error("Unexpected error ",e);
            throw e;
        }catch(Exception e){
            log.error("Unexpected error ",e);
            throw new RemoteException("Unexpected Error", e);
        }
    }
}
