package eu.eudat.logic.managers;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import eu.eudat.data.dao.criteria.DatasetProfileCriteria;
import eu.eudat.data.entities.DatasetProfile;
import eu.eudat.data.entities.UserDatasetProfile;
import eu.eudat.data.entities.UserInfo;
import eu.eudat.data.query.items.item.datasetprofile.DatasetProfileAutocompleteRequest;
import eu.eudat.data.query.items.table.datasetprofile.DatasetProfileTableRequestItem;
import eu.eudat.exceptions.datasetprofile.DatasetProfileNewVersionException;
import eu.eudat.logic.builders.model.models.DataTableDataBuilder;
import eu.eudat.logic.services.ApiContext;
import eu.eudat.logic.services.operations.DatabaseRepository;
import eu.eudat.logic.utilities.builders.XmlBuilder;
import eu.eudat.logic.utilities.documents.helpers.FileEnvelope;
import eu.eudat.logic.utilities.documents.xml.datasetProfileXml.ExportXmlBuilderDatasetProfile;
import eu.eudat.logic.utilities.documents.xml.datasetProfileXml.ImportXmlBuilderDatasetProfile;
import eu.eudat.logic.utilities.helpers.StreamDistinctBy;
import eu.eudat.models.data.components.commons.datafield.AutoCompleteData;
import eu.eudat.models.data.datasetprofile.DatasetProfileAutocompleteItem;
import eu.eudat.models.data.datasetprofile.DatasetProfileListingModel;
import eu.eudat.models.data.entities.xmlmodels.datasetprofiledefinition.Field;
import eu.eudat.models.data.externaldataset.ExternalAutocompleteFieldModel;
import eu.eudat.models.data.helpers.common.DataTableData;
import eu.eudat.models.data.listingmodels.UserInfoListingModel;
import eu.eudat.models.data.mail.SimpleMail;
import eu.eudat.models.data.security.Principal;
import eu.eudat.queryable.QueryableList;
import eu.eudat.types.Authorities;
import eu.eudat.types.MetricNames;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.*;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.activation.MimetypesFileTypeMap;
import javax.transaction.Transactional;
import javax.xml.xpath.*;
import java.io.*;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;


@Component
public class DatasetProfileManager {
    private static final Logger logger = LoggerFactory.getLogger(DatasetProfileManager.class);
    private static final List<String> cache = new ArrayList<>();

    private ApiContext apiContext;
    private DatabaseRepository databaseRepository;
    private Environment environment;

    private final MetricsManager metricsManager;

    @Autowired
    public DatasetProfileManager(ApiContext apiContext, Environment environment, MetricsManager metricsManager) {
        this.apiContext = apiContext;
        this.databaseRepository = apiContext.getOperationsContext().getDatabaseRepository();
        this.environment = environment;
        this.metricsManager = metricsManager;
    }

    @Transactional
    public eu.eudat.models.data.admin.composite.DatasetProfile getDatasetProfile(String id) {
        eu.eudat.data.entities.DatasetProfile profile = apiContext.getOperationsContext().getDatabaseRepository().getDatasetProfileDao().find(UUID.fromString(id));
        eu.eudat.models.data.admin.composite.DatasetProfile datasetprofile = AdminManager.generateDatasetProfileModel(profile);
        datasetprofile.setLabel(profile.getLabel());
        datasetprofile.setStatus(profile.getStatus());
        datasetprofile.setDescription(profile.getDescription());
        datasetprofile.setLanguage(profile.getLanguage());
        datasetprofile.setUsers(new ArrayList<>());
        retrieveUsers(profile, datasetprofile);
        return datasetprofile;
    }

    public List<DatasetProfileAutocompleteItem> getWithCriteria(DatasetProfileAutocompleteRequest datasetProfileAutocompleteRequest) throws IllegalAccessException, InstantiationException {
        QueryableList<DatasetProfile> items = databaseRepository.getDatasetProfileDao().getWithCriteria(datasetProfileAutocompleteRequest.getCriteria());
        QueryableList<DatasetProfile> pagedItems = datasetProfileAutocompleteRequest.applyPaging(items);
        List<DatasetProfileAutocompleteItem> datasetProfiles = pagedItems.select(item -> new DatasetProfileAutocompleteItem().fromDataModel(item));
        return datasetProfiles;
    }

    public DatasetProfile clone(String id) {
        DatasetProfile profile = apiContext.getOperationsContext().getDatabaseRepository().getDatasetProfileDao().find(UUID.fromString(id));
        apiContext.getOperationsContext().getDatabaseRepository().detachEntity(profile);
        profile.setId(null);
        return profile;
    }

    public DataTableData<DatasetProfileListingModel> getPaged(DatasetProfileTableRequestItem datasetProfileTableRequestItem, Principal principal) throws Exception {
        QueryableList<DatasetProfile> items = apiContext.getOperationsContext().getDatabaseRepository().getDatasetProfileDao().getWithCriteria(datasetProfileTableRequestItem.getCriteria());
        QueryableList<DatasetProfile> authItems = null;
        if (principal.getAuthz().contains(Authorities.ADMIN)) {
            authItems = items;
        } else if (principal.getAuthz().contains(Authorities.DATASET_PROFILE_MANAGER)) {
            List<Integer> roles = Arrays.asList(0, 1);
            authItems = apiContext.getOperationsContext().getDatabaseRepository().getDatasetProfileDao().getAuthenticated(items, principal.getId(), roles);
        }
        QueryableList<DatasetProfile> pagedItems = PaginationManager.applyPaging(authItems, datasetProfileTableRequestItem);
        List<DatasetProfileListingModel> datasetProfiles = pagedItems.select(item -> new DatasetProfileListingModel().fromDataModel(item));
        return apiContext.getOperationsContext().getBuilderFactory().getBuilder(DataTableDataBuilder.class).data(datasetProfiles).totalCount(items.count()).build();
    }

    public List<DatasetProfileListingModel> getAll(DatasetProfileTableRequestItem tableRequestItem) throws IllegalAccessException, InstantiationException {
        QueryableList<DatasetProfile> items = databaseRepository.getDatasetProfileDao().getWithCriteria(tableRequestItem.getCriteria());
        List<DatasetProfileListingModel> datasetProfiles = items.select(item -> new DatasetProfileListingModel().fromDataModel(item));

        return datasetProfiles;
    }

    public eu.eudat.models.data.entities.xmlmodels.datasetprofiledefinition.Field queryForField(String xml, String fieldId) throws XPathExpressionException {
        eu.eudat.models.data.entities.xmlmodels.datasetprofiledefinition.Field field = new Field();
        Document document = XmlBuilder.fromXml(xml);
        XPathFactory xpathFactory = XPathFactory.newInstance();
        XPath xpath = xpathFactory.newXPath();
        XPathExpression expr =
                xpath.compile("//field[@id='" + fieldId + "']");
        Element name = (Element) expr.evaluate(document, XPathConstants.NODE);
        field.fromXml(name);
        return field;
    }

    public static List<ExternalAutocompleteFieldModel> getAutocomplete(AutoCompleteData data, String like) {
        List<ExternalAutocompleteFieldModel> result = new LinkedList<>();
        SimpleClientHttpRequestFactory simpleFactory = new SimpleClientHttpRequestFactory();
        
        RestTemplate restTemplate = new RestTemplate(simpleFactory);
        HttpHeaders headers = new HttpHeaders();
        DocumentContext jsonContext = null;
        HttpEntity<String> entity;
        ResponseEntity response;
        List<Map<String, String>> jsonItems;
        int i = 0;
        for (AutoCompleteData.AutoCompleteSingleData singleData: data.getAutoCompleteSingleDataList()) {
            switch (AutoCompleteData.AutocompleteType.fromValue(singleData.getAutocompleteType())) {
                case UNCACHED:

                    String url = singleData.getUrl();
                    String mediaType = "";
                    if (url.contains("openaire") || url.contains("zenodo")) {
                    	mediaType = "application/json; charset=utf-8";
                    	if (url.contains("zenodo")) {
                    		url = url.replace("?", "/?");
                    	}
       
                        url = url.replace("{like}", like.equals("") ? "*" : like);
                        url = url.replace("%20", " ");
                        url = url.replace("%22", "\"");
                        url = url.replace("&amp;", "&");
                    } else {
                        mediaType = "application/vnd.api+json; charset=utf-8";
                        url += "?search=" + like;
                    }

                    if (!url.contains("zenodo")) {
                    	headers.setAccept(Collections.singletonList(MediaType.valueOf(mediaType)));
                    }
                    headers.setContentType(MediaType.APPLICATION_JSON);
                    entity = new HttpEntity<>("parameters", headers);

                   
                    response = restTemplate.exchange(url, HttpMethod.GET, entity, Object.class);
                    jsonContext = JsonPath.parse(response.getBody());
                    jsonItems = jsonContext.read(singleData.getOptionsRoot() + "['" + singleData.getAutoCompleteOptions().getLabel() + "','" + singleData.getAutoCompleteOptions().getValue() + "','" + singleData.getAutoCompleteOptions().getSource() + "','" + "uri" + "']");
                    jsonItems.forEach(item -> result.add(new ExternalAutocompleteFieldModel(parseItem(item.get(singleData.getAutoCompleteOptions().getValue())), parseItem(item.get(singleData.getAutoCompleteOptions().getLabel())), item.get(singleData.getAutoCompleteOptions().getSource())  != null ? parseItem(item.get(singleData.getAutoCompleteOptions().getSource())) : singleData.getAutoCompleteOptions().getSource(), parseItem(item.get("uri")))));
                    break;
                case CACHED:
                    headers.setAccept(Collections.singletonList(MediaType.valueOf("text/plain; charset=utf-8")));
                    headers.setContentType(MediaType.TEXT_PLAIN);
                    entity = new HttpEntity<>("parameters", headers);

                    if (cache.size() <= i) {
                        response = restTemplate.exchange(singleData.getUrl(), HttpMethod.GET, entity, String.class);
                        cache.add((String) response.getBody());
                    }
                    jsonContext = JsonPath.parse(cache.get(i));
                    jsonItems = jsonContext.read(singleData.getOptionsRoot() + "['" + singleData.getAutoCompleteOptions().getLabel() + "','" + singleData.getAutoCompleteOptions().getValue() + "','" + singleData.getAutoCompleteOptions().getSource() + "','" + "uri" + "']");
                    jsonItems.stream().filter(item -> item.get(singleData.getAutoCompleteOptions().getLabel()).toLowerCase().contains(like.toLowerCase()))
                             .forEach(item -> result.add(new ExternalAutocompleteFieldModel(item.get(singleData.getAutoCompleteOptions().getValue()), item.get(singleData.getAutoCompleteOptions().getLabel()), item.get(singleData.getAutoCompleteOptions().getSource())  != null ? item.get(singleData.getAutoCompleteOptions().getSource()) : singleData.getAutoCompleteOptions().getSource(), item.get("uri"))));
                    i++;
                    break;
            }

        }

        return result.stream().sorted(Comparator.comparing(ExternalAutocompleteFieldModel::getLabel)).collect(Collectors.toList());

        //return result;
    }

    private static String parseItem(Object item) {
        if (item instanceof String) {
            return (String) item;
        }
        if (item instanceof List) {
            List listedItems = (List) item;
            return parseItem(listedItems.get(0));
        }
        if (item instanceof Map) {
            return String.valueOf(((Map)item).get("$"));
        }
        return item != null ? item.toString() : null;
    }

    public ResponseEntity<byte[]> getDocument(eu.eudat.models.data.user.composite.DatasetProfile datasetProfile, String label) throws IllegalAccessException, IOException, InstantiationException {
        FileEnvelope envelope = getXmlDocument(datasetProfile, label);
        InputStream resource = new FileInputStream(envelope.getFile());
        logger.info("Mime Type of " + envelope.getFilename() + " is " +
                new MimetypesFileTypeMap().getContentType(envelope.getFile()));
        HttpHeaders responseHeaders = new HttpHeaders();
        responseHeaders.setContentLength(envelope.getFile().length());
        responseHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        String fileName = envelope.getFilename().replace(" ", "_").replace(",", "_");
        responseHeaders.set("Content-Disposition", "attachment;filename=" + fileName + ".xml");
        responseHeaders.set("Access-Control-Expose-Headers", "Content-Disposition");
        responseHeaders.get("Access-Control-Expose-Headers").add("Content-Type");

        byte[] content = org.apache.poi.util.IOUtils.toByteArray(resource);
        resource.close();
        Files.deleteIfExists(envelope.getFile().toPath());

        return new ResponseEntity<>(content,
                responseHeaders,
                HttpStatus.OK);
    }

    public FileEnvelope getXmlDocument(eu.eudat.models.data.user.composite.DatasetProfile datatasetProfile, String label) throws InstantiationException, IllegalAccessException, IOException {
        ExportXmlBuilderDatasetProfile xmlBuilder = new ExportXmlBuilderDatasetProfile();
        File file = xmlBuilder.build(datatasetProfile, environment);
        FileEnvelope fileEnvelope = new FileEnvelope();
        fileEnvelope.setFile(file);
        fileEnvelope.setFilename(label);
        return fileEnvelope;
    }

    public eu.eudat.logic.utilities.documents.xml.datasetProfileXml.datasetProfileModel.DatasetProfile createDatasetProfileFromXml(MultipartFile multiPartFile) {
        ImportXmlBuilderDatasetProfile xmlBuilder = new ImportXmlBuilderDatasetProfile();
        try {
            File localFile = convert(multiPartFile);
            eu.eudat.logic.utilities.documents.xml.datasetProfileXml.datasetProfileModel.DatasetProfile profile = xmlBuilder.build(localFile);
            Files.deleteIfExists(localFile.toPath());
            metricsManager.increaseValue(MetricNames.DATASET_TEMPLATE, 1, MetricNames.DRAFT);
            return profile;
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        }
        return null;
    }

    private File convert(MultipartFile file) throws IOException {
        File convFile = new File(this.environment.getProperty("temp.temp") + file.getOriginalFilename());
        convFile.createNewFile();
        FileOutputStream fos = new FileOutputStream(convFile);
        fos.write(file.getBytes());
        fos.close();
        return convFile;
    }

    public eu.eudat.data.entities.DatasetProfile createNewVersionDatasetProfile(String id, eu.eudat.models.data.admin.composite.DatasetProfile profile) throws Exception {
        // Getting the DatasetProfile which we will create its new version.
        eu.eudat.data.entities.DatasetProfile oldDatasetProfile = apiContext.getOperationsContext().getDatabaseRepository().getDatasetProfileDao().find(UUID.fromString(id));

        // Getting the DatasetProfile with the latest Version.
        DatasetProfileCriteria criteria = new DatasetProfileCriteria();
        LinkedList<UUID> list = new LinkedList<>();
        list.push(oldDatasetProfile.getGroupId());
        criteria.setGroupIds(list);
        criteria.setAllVersions(false);
        QueryableList<DatasetProfile> datasetProfileQueryableList = apiContext.getOperationsContext().getDatabaseRepository().getDatasetProfileDao().getWithCriteria(criteria);
        eu.eudat.data.entities.DatasetProfile latestVersionDatasetProfile = datasetProfileQueryableList.getSingle();

        if (latestVersionDatasetProfile.getVersion().equals(oldDatasetProfile.getVersion())){
            eu.eudat.models.data.admin.composite.DatasetProfile sortedProfile = profile.toShort();
            eu.eudat.data.entities.DatasetProfile modelDefinition = AdminManager.generateViewStyleDefinition(sortedProfile, apiContext);
//            modelDefinition.setLabel(oldDatasetProfile.getLabel());
            modelDefinition.setVersion((short) (oldDatasetProfile.getVersion() + 1));
            modelDefinition.setGroupId(oldDatasetProfile.getGroupId());
//            modelDefinition.setLanguage(oldDatasetProfile.getLanguage());
            apiContext.getOperationsContext().getDatabaseRepository().getDatasetProfileDao().createOrUpdate(modelDefinition);
            eu.eudat.data.entities.DatasetProfile datasetProfile =  apiContext.getOperationsContext().getDatabaseRepository().getDatasetProfileDao().createOrUpdate(modelDefinition);
            this.storeDatasetProfileUsers(datasetProfile, profile);
            return modelDefinition;
        } else {
            throw new DatasetProfileNewVersionException("Version to update not the latest.");
        }
    }

    public void storeDatasetProfileUsers(DatasetProfile entity, eu.eudat.models.data.admin.composite.DatasetProfile model) {
        if (model.getUsers() != null && !model.getUsers().isEmpty()) {
            if (entity.getUsers() == null) {
                entity.setUsers(new HashSet<>());
            }
            model.getUsers().stream().filter(userInfoListingModel -> entity.getUsers().stream()
                    .filter(userDatasetProfile -> userDatasetProfile.getUser().getId().equals(userInfoListingModel.getId())).count() == 0)
                    .forEach(userInfoListingModel -> {
                UserDatasetProfile userDatasetProfile1 = new UserDatasetProfile();
                userDatasetProfile1.setDatasetProfile(entity);
                UserInfo userInfo1 = apiContext.getOperationsContext().getDatabaseRepository().getUserInfoDao().find(userInfoListingModel.getId());
                userDatasetProfile1.setUser(userInfo1);
                userDatasetProfile1.setRole(1);
                apiContext.getOperationsContext().getDatabaseRepository().getUserDatasetProfileDao().createOrUpdate(userDatasetProfile1);
                sendJoinMail(userDatasetProfile1);
            });
            entity.getUsers().stream().filter(userDatasetProfile -> model.getUsers().stream()
                    .filter(userInfoListingModel -> userDatasetProfile.getUser().getId().equals(userInfoListingModel.getId())).count() > 0
                    && userDatasetProfile.getRole() == 2).forEach(userDatasetProfile -> {
                userDatasetProfile.setRole(1);
                apiContext.getOperationsContext().getDatabaseRepository().getUserDatasetProfileDao().createOrUpdate(userDatasetProfile);
                sendJoinMail(userDatasetProfile);
            });
        }
        if (entity.getUsers() != null && !entity.getUsers().isEmpty()) {
            entity.getUsers().stream().filter(userDatasetProfile -> model.getUsers().stream()
                    .filter(userInfoListingModel -> userDatasetProfile.getUser().getId().equals(userInfoListingModel.getId())).count() == 0)
                    .forEach(userDatasetProfile -> {
                userDatasetProfile.setRole(2);
                apiContext.getOperationsContext().getDatabaseRepository().getUserDatasetProfileDao().createOrUpdate(userDatasetProfile);
            });
        }
    }

    @Transactional
    public void retrieveUsers(DatasetProfile entity, eu.eudat.models.data.admin.composite.DatasetProfile model) {
        if (entity.getUsers() != null && !entity.getUsers().isEmpty()) {
            model.setUsers(entity.getUsers().stream().filter(userDatasetProfile -> userDatasetProfile.getRole() < 2).map(userDatasetProfile -> {
                UserInfoListingModel userInfoListingModel = new UserInfoListingModel();
                userInfoListingModel.setId(userDatasetProfile.getUser().getId());
                userInfoListingModel.setName(userDatasetProfile.getUser().getName());
                userInfoListingModel.setEmail(userDatasetProfile.getUser().getEmail());
                userInfoListingModel.setRole(userDatasetProfile.getRole());
                return userInfoListingModel;
            }).collect(Collectors.toList()));
        }
    }

    private void sendJoinMail(UserDatasetProfile userDatasetProfile) {
            SimpleMail mail = new SimpleMail();
            mail.setSubject(environment.getProperty("admin.mail.subject").replace( "{templateName}", userDatasetProfile.getDatasetProfile().getLabel()));
            String content = apiContext.getUtilitiesService().getMailService().getMailTemplateContent(environment.getProperty("email.dataset.template"));
            content = content.replace("{recipient}", userDatasetProfile.getUser().getName());
            content = content.replace("{templateName}", userDatasetProfile.getDatasetProfile().getLabel());
            content = content.replace("{host}", this.environment.getProperty("dmp.domain"));
            content = content.replace("{templateID}", userDatasetProfile.getDatasetProfile().getId().toString());
            mail.setContent(content);
            mail.setTo(userDatasetProfile.getUser().getEmail());
            try {
                apiContext.getUtilitiesService().getMailService().sendSimpleMail(mail);
            } catch (Exception ex) {
                logger.error(ex.getMessage(), ex);
            }

    }
}
