package eu.dnetlib.repos;

import eu.dnetlib.api.data.DatasourceManagerServiceException;
import eu.dnetlib.domain.data.Repository;
import eu.dnetlib.domain.data.RepositoryInterface;
import eu.dnetlib.repos.ehcacher.CacheProvider;
import net.sf.ehcache.Element;
import org.apache.log4j.Logger;
import org.springframework.context.annotation.DependsOn;
import org.springframework.scheduling.annotation.Async;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.*;
import java.sql.SQLException;
import java.util.*;

@DependsOn("datasourceManagerServiceLocator")
public class RepoApiCacheImpl implements RepoApi {
    private static Logger logger = Logger.getLogger(RepoApiCacheImpl.class);
    private final String[] cacheKeys = {"opendoar", "re3data", "jour_aggr"};
    protected RepoApi coreRepoApi;
    private CacheProvider cacheProvider;

    @PostConstruct
    public void initCaches() {
        Thread initializingCachesThread = new Thread() {
            public void run() {
                logger.info("initializing caches");
                boolean initialized = false;
                while (!initialized) {
                    for (String cacheKey : cacheKeys) {
                        try {
                            if (!cacheProvider.getCache().isKeyInCache(cacheKey)) {
                                File cacheBackup = new File("/tmp/cache-" + cacheKey + ".bak");
                                if (cacheBackup.exists()) {
                                    logger.info("initializing key: " + cacheKey + " from disk");
                                    FileInputStream fis = new FileInputStream("/tmp/cache-" + cacheKey + ".bak");

                                    ObjectInputStream ois = new ObjectInputStream(fis);
                                    Map<String, Repository> repositories = (Map<String, Repository>) ois.readObject();
                                    ois.close();
                                    cacheProvider.getCache().put(new Element(cacheKey, (Map<String, Repository>) repositories));
                                } else {
                                    logger.info("initializing key: " + cacheKey + " from dms");
                                    cacheProvider.getCache().get(cacheKey).getObjectValue();
                                }
                            }
                        } catch (Exception e) {
                            logger.error("Error while initializing cache for key: " + cacheKey, e);
                            logger.info("initializing key: " + cacheKey + " from dms");
                            cacheProvider.getCache().get(cacheKey).getObjectValue();
                        }
                    }
                    logger.info("caches initialized successfully");
                    initialized = true;
                }
            }
        };
        initializingCachesThread.setDaemon(true);
        initializingCachesThread.start();
    }

    @PreDestroy
    public void persistCaches() {
        logger.info("persisting caches");
        try {
            for (String cacheKey : cacheKeys) {
                FileOutputStream fos = new FileOutputStream("/tmp/cache-" + cacheKey + ".bak");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                if (cacheProvider.getCache().isKeyInCache(cacheKey)) {
                    logger.info("persisting cache: " + cacheKey);
                    oos.writeObject((Map<String, Repository>) cacheProvider.getCache().get(cacheKey).getObjectValue());
                    oos.close();
                }
            }
            logger.info("caches stored in disk");
        } catch (Exception e) {
            logger.error("Error while persisting caches", e);
            for (String cacheKey : cacheKeys) {
                File cacheBackup = new File("/tmp/cache-" + cacheKey + ".bak");
                if (cacheBackup.exists())
                    cacheBackup.delete();
            }
        }
    }

    @Override
    public Repository getRepository(String id) throws Exception {
        return this.getRepository(null, id);
    }

    @Override
    public Repository getRepository(String officialName, String id) throws Exception {
        logger.info("getting repository with name " + officialName + " and id: " + id + " from cache");
        Repository retRepo = null;
        try {
            for (String cacheKey : cacheKeys) {
                if (((Map<String, Repository>) cacheProvider.getCache().get(cacheKey).getObjectValue()).containsKey(id))
                    return ((Map<String, Repository>) cacheProvider.getCache().get(cacheKey).getObjectValue()).get(id);
            }
            retRepo = coreRepoApi.getRepository(id);
        } catch (DatasourceManagerServiceException e) {
            logger.error("Error getting repository with name " + officialName + " and id: " + id + " from cache", e);
        }
        return retRepo;
    }

    @Override
    public void getRepositoryStats(Repository repo) throws Exception {
        logger.info("getting repository stats for : " + repo.getOfficialName() + " from cache");
        this.coreRepoApi.getRepositoryStats(repo);
    }

    @Override
    public String getListLatestUpdate(String mode) throws Exception {
        return this.coreRepoApi.getListLatestUpdate(mode);
    }

    @Override
    public String getNextScheduledExecution(String mode) throws Exception {
        return this.coreRepoApi.getRepository("openaire____::" + mode).getInterfaces().get(0).getExtraFields().get("last_collection_date");
    }

    @Override
    public String storeRepository(Repository repo, String datatype, List<RepositoryInterface> interfacesToRegister) throws Exception {
        String retMessage = null;
        logger.info("Storing " + datatype + " repository with id: " + repo.getId());
        try {
            retMessage = this.coreRepoApi.storeRepository(repo, datatype, interfacesToRegister);
            forceUpdateCache(datatype, repo.getId());
        } catch (Exception e) {
            logger.error("Error storing repo " + repo.getOfficialName() + " in dms", e);
            throw e;
        }
        return retMessage;
    }

    public List<Repository> getRepositories(String collectedFrom) throws Exception {
        List<Repository> repoList = new ArrayList<Repository>();
        try {
            logger.info("getting repos from cache with collected from value: " + collectedFrom);

            Map<String, Repository> repoMap = (Map<String, Repository>) cacheProvider.getCache().get(collectedFrom).getObjectValue();
            repoList = new ArrayList<Repository>(repoMap.values());

        } catch (Exception e) {
            logger.error("Error getting repos from cache with collected from value: " + collectedFrom, e);
            throw e;
        }
        return repoList;
    }

    public Map<String, Repository> getRepositoriesAsMap(String collectedFrom) throws Exception {
        List<Repository> repoList = new ArrayList<Repository>();
        Map<String, Repository> repoMap = new HashMap<String, Repository>();
        try {
            logger.info("getting repos from cache with collected from value: " + collectedFrom);
            repoMap = (Map<String, Repository>) cacheProvider.getCache().get(collectedFrom).getObjectValue();

        } catch (Exception e) {
            logger.error("Error getting repos from cache with collected from value: " + collectedFrom, e);
            throw e;
        }
        return repoMap;
    }

    @Override
    public List<Map<String, String>> getRegisteredReposByOthers(String user_mail) {
        List<Map<String, String>> res = new ArrayList<Map<String, String>>();

        try {
            logger.info("getting repos by others for user : " + user_mail);

            res = this.getRepositoriesOfUserAsMap(user_mail, true);
        } catch (Exception e) {
            logger.error("Error getting repos by others for user : " + user_mail, e);

        }
        return res;
    }

    @Override
    public List<Map<String, String>> getRepositoriesOfUser(String user_mail) throws Exception {
        logger.info("getting repos for user : " + user_mail);
        return this.getRepositoriesOfUserAsMap(user_mail);
    }

    public List<Map<String, String>> getRepositoriesOfUserAsMap(String user_mail) throws Exception {
        logger.info("getting repos for user : " + user_mail);
        return this.getRepositoriesOfUserAsMap(user_mail, false);
    }

    private List<Map<String, String>> getRepositoriesOfUserAsMap(String user_mail, Boolean isAdmin) throws Exception {

        logger.info("getting repos as map for user: " + user_mail);
        List<Map<String, String>> res = new ArrayList<Map<String, String>>();
        try {
            for (Repository repo : this.getRepositoriesOfUser(user_mail, isAdmin)) {
                res.add(RepoTools.convertRepoToMap(repo));
            }
        } catch (Exception e) {
            logger.error("getting repos as map for user: " + user_mail, e);
        }
        return res;
    }

    @SuppressWarnings("unchecked")
    public TreeMap<String, List<Map<String, String>>> getRepositoriesByCountry(String collectedFrom) throws Exception {
        List<Repository> repoList = new ArrayList<Repository>();
        TreeMap<String, List<Map<String, String>>> res = new TreeMap<String, List<Map<String, String>>>();

        try {
            logger.info("getting repos by country from cache with key: " + collectedFrom);
            Map<String, Repository> repoMap = (Map<String, Repository>) cacheProvider.getCache().get(collectedFrom).getObjectValue();
            repoList = new ArrayList<Repository>(repoMap.values());
            res = RepoTools.splitReposByCountry(repoList);

        } catch (Exception e) {
            logger.error("Error getting repositories from cache with key: " + collectedFrom, e);
            throw e;
        }
        return res;
    }

    @Override
    public Map<String, List<Repository>> getRepositoriesPerCountry(String collectedFrom) throws Exception {
        List<Repository> repoList;
        Map<String, List<Repository>> res = new TreeMap<String, List<Repository>>();

        try {
            logger.info("getting repos by country from cache with key: " + collectedFrom);
            Map<String, Repository> repoMap = (Map<String, Repository>) cacheProvider.getCache().get(collectedFrom).getObjectValue();
            repoList = new ArrayList<Repository>(repoMap.values());
            res = RepoTools.splitReposPerCountry(repoList);

        } catch (Exception e) {
            logger.error("Error getting repositories from cache with key: " + collectedFrom, e);
            throw e;
        }
        return res;
    }

    @Override
    public List<Repository> getRepositoriesOfCountry(String collectedFrom, String country) throws Exception {
        try {
            logger.info("getting repos for country " + country + " from cache with key: " + collectedFrom);
            return this.getRepositoriesPerCountry(collectedFrom).get(country);
        } catch (Exception e) {
            logger.error("Error getting repos for country " + country + " from cache with key: " + collectedFrom, e);
            throw e;
        }
    }

    @Override
    public Map<String, String> getRepoCompatibility(String officialName, String datasourceId) throws Exception {
        Map<String, String> compMap = null;

        try {
            logger.debug("getting repository " + officialName + " compatibility from cache");

            compMap = this.getRepoCompatibility(officialName, datasourceId);
        } catch (Exception e) {
            logger.error("Error getting repo " + officialName + " compatibility from cache", e);
            throw e;
        }

        return compMap;
    }

    @Override
    public List<Repository> getReposByIds(List<String> datasourceIds) throws Exception {
        List<Repository> retRepos = new ArrayList<Repository>();
        try {
            logger.info("getting repos by ids from cache");
            for (String cacheKey : cacheKeys) {
                Map<String, Repository> repoMap = (Map<String, Repository>) cacheProvider.getCache().get(cacheKey).getObjectValue();
                List<Repository> repoList = new ArrayList<Repository>(repoMap.values());
                for (Repository rep : repoList) {
                    if (datasourceIds.contains(rep.getId())) {
                        retRepos.add(rep);
                    }
                }
            }
        } catch (Exception e) {
            logger.error("error getting repos by ids from cache", e);
        }
        return retRepos;
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Repository> getRepositoriesOfUser(String user_mail, Boolean repoAdmin) throws SQLException {
        List<Repository> retRepos = new ArrayList<Repository>();
        try {
            if (!repoAdmin) {
                logger.info("getting repositories registered by user: " + user_mail + " from cache");
                for (String cacheKey : cacheKeys) {
                    Map<String, Repository> repoMap = (Map<String, Repository>) cacheProvider.getCache().get(cacheKey).getObjectValue();
                    List<Repository> repoList = new ArrayList<Repository>(repoMap.values());
                    for (Repository rep : repoList) {
                        if (rep.getRegisteredBy().equalsIgnoreCase(user_mail) || rep.getContactEmail().equalsIgnoreCase(user_mail)) {
                            retRepos.add(rep);
                        }
                    }
                }
            } else {
                logger.debug("getting journal /agreg  registered by others  from cache");
                Map<String, Repository> repoMap = (Map<String, Repository>) cacheProvider.getCache().get("jour_aggr").getObjectValue();
                List<Repository> repoList = new ArrayList<Repository>(repoMap.values());
                for (Repository rep : repoList) {
                    if (!rep.getRegisteredBy().equalsIgnoreCase(user_mail) && !rep.getContactEmail().equalsIgnoreCase(user_mail)) {
                        retRepos.add(rep);
                    }
                }
            }

        } catch (Exception e) {
            logger.error("error getting repositories registered by user: " + user_mail + " from cache", e);
        }
        return retRepos;
    }

    @Override
    public boolean updateRepositoryInterfaceCompliance(String officialName,
                                                       String datasourceId, String interfaceId, String desiredCompliance, String set, String baseUrl, String oldId)
            throws Exception {
        boolean ret;
        try {
            ret = this.coreRepoApi.updateRepositoryInterfaceCompliance(officialName, datasourceId, interfaceId, desiredCompliance, set, baseUrl, oldId);
            forceUpdateCache(null, datasourceId);
        } catch (Exception e) {
            logger.error("error connecting to dms to set a repo interface as openaire compliant " + officialName, e);
            throw e;
        }
        return ret;
    }

    @Override
    public boolean insertPubFileInterface(String dsId, RepositoryInterface iFace)
            throws Exception {
        boolean ret = false;
        try {
            logger.info("storing pdf interface for repository: " + dsId);
            ret = this.coreRepoApi.insertPubFileInterface(dsId, iFace);
            forceUpdateCache(null, dsId);
        } catch (Exception e) {
            logger.error("error storing pdf interface for repository: " + dsId, e);
            throw e;
        }
        return ret;
    }

    @Override
    public boolean updatePubFileInterface(String dsId, RepositoryInterface iFace)
            throws Exception {
        boolean ret;
        try {
            logger.info("updating pdf interface for repository: " + dsId);
            ret = this.coreRepoApi.updatePubFileInterface(dsId, iFace);
            forceUpdateCache(null, dsId);
        } catch (Exception e) {
            logger.error("error updating pdf interface for repository: " + dsId, e);
            throw e;
        }
        return ret;
    }

    @Override
    public List<String> getUrlsOfRepos(String user_mail, Boolean repoAdmin) throws Exception {
        List<String> urls = new ArrayList<String>();
        try {
            logger.info("getting url from repositories registered by user: " + user_mail + " from cache");
            for (Repository repo : this.getRepositoriesOfUser(user_mail, repoAdmin)) {
                for (RepositoryInterface iFace : repo.getInterfaces()) {
                    if (iFace.getContentDescription().equalsIgnoreCase("metadata")) {
                        if (iFace.getBaseUrl() != null && !iFace.getBaseUrl().isEmpty() && !urls.contains(iFace.getBaseUrl()) && iFace.getAccessProtocol().equalsIgnoreCase("oai"))
                            urls.add(iFace.getBaseUrl());
                    }
                }
            }
        } catch (Exception e) {
            logger.error("error getting url from repositories registered by user: " + user_mail + " from cache", e);
            throw e;
        }
        return urls;
    }

    @Override
    public String editRepository(Repository repo, String officialNameOld,
                                 String idOld, String datatype) throws Exception {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean repoIsCompliant(String officialName) throws Exception {
        throw new UnsupportedOperationException();
    }

    @Override
    public String updateRepositoryInformation(Repository repo) throws Exception {
        String retMessage = "";
        try {
            logger.info("updating repository information");
            this.coreRepoApi.updateRepositoryInformation(repo);
            forceUpdateCache(repo.getDatasourceType(), repo.getId());
        } catch (Exception e) {
            logger.error("error updating repository information", e);
            throw e;
        }
        return retMessage;
    }

    @Override
    public String deleteRepositoryInterfaces(String dsId, List<RepositoryInterface> interfaces) throws Exception {
        String retMessage = "";
        try {
            logger.info("deleting repository interfaces");
            retMessage = this.coreRepoApi.deleteRepositoryInterfaces(dsId, interfaces);
        } catch (Exception e) {
            logger.error("error deleting repository interfaces", e);
            throw e;
        }
        return retMessage;
    }

    @Override
    public void deleteRepositoryInterfacesWithoutChecks(String dsId, List<RepositoryInterface> interfaces, String datatype) throws Exception {
        try {
            logger.info("deleting repository interfaces without checks");
            this.coreRepoApi.deleteRepositoryInterfacesWithoutChecks(dsId, interfaces, datatype);
            forceUpdateCache(datatype, dsId);
        } catch (Exception e) {
            logger.error("error deleting repository interfaces", e);
            throw e;
        }
    }

    @Override
    public String updateRepositoryInterfaces(String dsId,
                                             List<RepositoryInterface> interfaces,
                                             List<RepositoryInterface> interfacesOld, String datatype, List<RepositoryInterface> interfacesToRegister) throws Exception {
        String importResult = "";
        try {
            logger.info("updating repository interfaces for datasource: " + dsId);
            importResult = coreRepoApi.updateRepositoryInterfaces(dsId, interfaces, interfacesOld, datatype, interfacesToRegister);
            forceUpdateCache(datatype, dsId);
            logger.debug("import result final: " + importResult);
        } catch (Exception e) {
            logger.error("error updating repository interfaces for datasource: " + dsId, e);
            throw e;
        }
        return importResult;

    }

    @Override
    public RepositoryInterface updateRepositoryInterfaceWithoutChecks(String dsId, RepositoryInterface iFace, String datatype) throws Exception {
        RepositoryInterface retIface;
        try {
            logger.info("updating repository interface for datasource: " + dsId);
            retIface = this.coreRepoApi.updateRepositoryInterfaceWithoutChecks(dsId, iFace, datatype);
            forceUpdateCache(datatype, dsId);
        } catch (Exception e) {
            logger.error("error updating repository interface for datasource: " + dsId, e);
            throw e;
        }
        return retIface;
    }

    @Override
    public String insertRepositoryInterfaces(String dsId,
                                             List<RepositoryInterface> interfaces, List<RepositoryInterface> interfacesOld, String datatype, List<RepositoryInterface> interfacesToRegister)
            throws Exception {
        String importResult = "";
        try {
            logger.info("inserting repository interfaces for datasource: " + dsId);
            importResult = this.coreRepoApi.insertRepositoryInterfaces(dsId, interfaces, interfacesOld, datatype, interfacesToRegister);
            forceUpdateCache(datatype, dsId);
        } catch (Exception e) {
            logger.error("error inserting repository interfaces for datasource: " + dsId, e);
            throw e;
        }
        return importResult;
    }

    @Override
    public RepositoryInterface insertRepositoryInterfaceWithoutChecks(String dsId,
                                                                      RepositoryInterface iFace, String datatype)
            throws Exception {
        RepositoryInterface retIface;
        try {
            retIface = this.coreRepoApi.updateRepositoryInterfaceWithoutChecks(dsId, iFace, datatype);
            forceUpdateCache(datatype, dsId);
        } catch (Exception e) {
            logger.error("error inserting repository interface for datasource: " + dsId, e);
            throw e;
        }
        return retIface;
    }

    @Override
    public boolean unregisterRepository(Repository repo) throws Exception {
        logger.info("unregistering repository " + repo.getOfficialName());
        boolean ret;
        try {
            ret = this.unregisterRepository(repo);
            this.forceUpdateCache(repo.getDatasourceType(), repo.getId());
        } catch (Exception e) {
            logger.error("Error unregistering repository " + repo.getOfficialName(), e);
            return false;
        }
        return ret;

    }

    private void printInterface(RepositoryInterface iFace) {
        logger.debug("baseUrl: " + iFace.getBaseUrl());
        logger.debug("format: " + iFace.getAccessFormat());
        logger.debug("typology: " + iFace.getTypology());
        logger.debug("set: " + iFace.getAccessSet());
        logger.debug("des_comp_level: " + iFace.getDesiredCompatibilityLevel());
        logger.debug("cur_comp_level: " + iFace.getCompliance());
        logger.debug("protocol: " + iFace.getAccessProtocol());
        logger.debug("api_id: " + iFace.getId());
        logger.debug("removable: " + iFace.isRemovable());
        logger.debug("deleteApi: " + iFace.isDeleteApi());
    }

    private void forceUpdateCache(final String datatype, final String repoId) {
        Thread updatingThread = new Thread() {
            public void run() {
                logger.info("Updating cache for key: " + datatype + " after adding/editing repository");
                try {
                    String key = "";
                    if (datatype == null) {
                        for (String cacheKey : cacheKeys) {
                            if (((Map<String, Repository>) cacheProvider.getCache().get(cacheKey).getObjectValue()).containsKey(repoId)) {
                                key = cacheKey;
                                break;
                            }
                        }
                    } else {
                        key = datatype;
                        if (datatype.equalsIgnoreCase("journal") || datatype.equalsIgnoreCase("aggregator")) {
                            key = "jour_aggr";
                        }
                    }

                    Map<String, Repository> oldMap = (Map<String, Repository>) cacheProvider.getCache().get(key).getObjectValue();
                    oldMap.put(repoId, coreRepoApi.getRepository(repoId));
                    cacheProvider.getCache().replace(new Element(key, (Map<String, Repository>) oldMap), new Element(key, (Map<String, Repository>) oldMap));
                    logger.info("cache updated");
                    //((SelfPopulatingCache) cacheProvider.getCache()).refresh(key);
                } catch (Exception e) {
                    logger.error("Error while updating cache. Retry in 250000min", e);
                    try {
                        Thread.sleep(250000);
                    } catch (Exception e1) {
                        logger.error("Error while updating cache", e1);
                    }
                }
            }
        };
        updatingThread.setDaemon(true);
        updatingThread.start();
    }

    public CacheProvider getCacheProvider() {
        return cacheProvider;
    }

    public void setCacheProvider(CacheProvider cacheProvider) {
        this.cacheProvider = cacheProvider;
    }

    public RepoApi getCoreRepoApi() {
        return coreRepoApi;
    }

    public void setCoreRepoApi(RepoApi coreRepoApi) {
        this.coreRepoApi = coreRepoApi;
    }
}