package org.gcube.application.geoportal.service.engine.providers.ucd;

import lombok.extern.slf4j.Slf4j;
import org.gcube.application.cms.caches.AbstractScopedMap;
import org.gcube.application.cms.caches.Engine;
import org.gcube.application.cms.caches.ObjectManager;
import org.gcube.application.cms.implementations.ImplementationProvider;
import org.gcube.application.cms.serialization.Serialization;
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.useCaseDescriptor.UseCaseDescriptor;
import org.gcube.application.geoportal.common.utils.ContextUtils;
import org.gcube.application.geoportal.service.engine.mongo.UCDManagerI;
import org.gcube.application.geoportal.service.engine.mongo.UCDMongoManager;
import org.gcube.application.geoportal.service.model.internal.faults.RegistrationException;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static java.lang.Thread.sleep;

/**
 * Provider of UCDManagerI (this)
 *
 * - nb on get should check / trigger cache refresh TTL
 * - uses UCDMongoManager for storing
 * - profile live map engine is discovered
 *
 *
 *
 */
@Slf4j
public class UCDManager extends AbstractScopedMap<UCDManagerI> implements UCDManagerI {

    private Map<String,UseCaseDescriptor> memCache = new HashMap<>();

    private final ReadWriteLock cacheLock = new ReentrantReadWriteLock();


    public UCDManager() {
        super("UCD MANAGER");
        setTTL(Duration.of(2, ChronoUnit.MINUTES));
    }


    @Override
    public Iterable<UseCaseDescriptor> query(QueryRequest queryRequest) throws ConfigurationException {
        cacheLock.readLock().lock();
        try{
            return getMongoManager().query(queryRequest);
        }finally {
            cacheLock.readLock().unlock();
        }
    }

    @Override
    public void deleteById(String id, boolean force) throws RegistrationException, ConfigurationException {
        // NB Check for existing ID
        UseCaseDescriptor found = getByUCID(id,true);
        cacheLock.writeLock().lock();
        try{
            if(found!=null) {
                // TODO validate DELETE REQUEST
                // TODO DELETE UCD
                // forceUpdateCache();
                throw new WebApplicationException("TO IMPLEMENT ", Response.Status.INTERNAL_SERVER_ERROR);
            } else
                throw new WebApplicationException("No Matching UCD with ID "+id, Response.Status.NOT_FOUND);
        }finally{
            cacheLock.writeLock().unlock();
        }
    }

    @Override
    public UseCaseDescriptor put(UseCaseDescriptor desc) throws ConfigurationException, RegistrationException {
        log.info("Update UCD {} ",desc.getId());
        log.debug("Mongo id is {} ",desc.getMongoId());
        // NB Check for existing ID
        UseCaseDescriptor found = getByUCID(desc.getId(),true);
            if(found!=null) {
                // TODO validate UPDATE
                // TODO STORE UCD
                // forceUpdateCache();
                throw new WebApplicationException("Update Feature is yet TO IMPLEMENT ", Response.Status.INTERNAL_SERVER_ERROR);
            } else {
                // create new
                registerNew(desc);
                forceUpdateCache();
                int attempt=0;
                do{
                    found =getByUCID(desc.getId(),true);
                    log.info("Waiting for backend to update.. ");
                    try{sleep(4000);}catch (Throwable t){}
                    attempt++;
                } while(found == null && attempt<=4);

                return found;
            }
    }

    @Override
    protected UCDManagerI retrieveObject(String context) throws ConfigurationException {
        // Called when TTL ends
        forceUpdateCache();
        return this;
    }

    @Override
    public UseCaseDescriptor getById(String id) throws ConfigurationException, RegistrationException {
        // GET from mongo cache
//        UCDMongoManager mongo=getMongoManager();
//        UseCaseDescriptor toReturn=mongo.getById(id);
//        log.debug("UCD ID : {} from mongo is {} ",id,toReturn);
//        if(toReturn == null) {
//            // IF void try from ProfileEngine
//            toReturn =getLiveMap().get(id);
//           if(toReturn != null ){
//                log.debug("Force update of live map {} from live map ",id);
//                toReturn = mongo.put(toReturn);
//            }
//        }
//        return toReturn;

        return getByUCID(id,true);
    }


    private UseCaseDescriptor getByUCID(String ucid,boolean refreshOnMissing) throws ConfigurationException {
        // Wait / check for cache loaded
        log.debug("Trying to getById from memcache {} [refresh on missing : {}]",ucid,refreshOnMissing);
        cacheLock.readLock().lock();
        boolean releaseLock = true;
        UseCaseDescriptor toReturn =null;
        try{
            if(memCache.containsKey(ucid)) toReturn = memCache.get(ucid);
            else {
                if(refreshOnMissing){
                    cacheLock.readLock().unlock();
                    releaseLock=false;
                    forceUpdateCache();
                    // pass false in order to trigger cache update only on first NOT FOUND event
                    toReturn = getByUCID(ucid,false);
                }
            }
        }finally {
            if(releaseLock) cacheLock.readLock().unlock();
        }
        return toReturn;
    }






    private void registerNew(UseCaseDescriptor ucd) throws ConfigurationException {
        cacheLock.writeLock().lock();
        try{
            Engine<ProfileMap> engine=ImplementationProvider.get().getEngineByManagedClass(ProfileMap.class);
        if(engine instanceof ObjectManager){
            ((ObjectManager<UseCaseDescriptor>)engine).insert(ucd);
        } else throw new ConfigurationException("Profile Map Engine is not Object Manager. Actual implementation is "+engine.getClass());
        }finally {
            cacheLock.writeLock().unlock();
        }
    }

    private UCDMongoManager getMongoManager() throws ConfigurationException {
        return new UCDMongoManager();
    }


    private void forceUpdateCache() throws ConfigurationException {
        log.info("UPDATING PROFILE CACHE..");
        cacheLock.writeLock().lock();

        final UCDMongoManager manager = getMongoManager();
        manager.deleteAll();
        memCache.clear();
        Engine<ProfileMap> liveMapProvider =ImplementationProvider.get().getEngineByManagedClass(ProfileMap.class);
        log.trace("LiveMap Provider class is {} ",liveMapProvider.getClass());
        ProfileMap liveMap= liveMapProvider.getObject();
        log.debug("LiveMap size is {} ",liveMap.size());
        for (Map.Entry<String, UseCaseDescriptor> entry : liveMap.entrySet()) {
            // Copying object so we don't alter live Map
            UseCaseDescriptor useCaseDescriptor = Serialization.convert(entry.getValue(),UseCaseDescriptor.class);
            try {
                log.debug("Updateing cache with {}, mongo id is {}", useCaseDescriptor.getId(),useCaseDescriptor.getMongoId());
                if(useCaseDescriptor.getMongoId()!=null){
                    log.warn("Retrieved UCD {} from Live Map has a Mongo id [{}]. Removing it..",useCaseDescriptor.getId(),useCaseDescriptor.getMongoId());
                    useCaseDescriptor.setMongoId(null);
                }
                // insert/update into DB
                manager.insert(useCaseDescriptor);
                memCache.put(useCaseDescriptor.getId(), useCaseDescriptor);
            } catch (RegistrationException e) {
               log.warn("Unable to cache UCD {}",entry.getKey(),e);
            }
        }
        log.info("Cached {} UCDs in {} ",memCache.size(),ContextUtils.getCurrentScope());

        cacheLock.writeLock().unlock();
    }
}
