package eu.eudat.depositinterface.zenodorepository.interfaces;

import com.fasterxml.jackson.databind.ObjectMapper;
import eu.eudat.depositinterface.models.DMPDepositModel;
import eu.eudat.depositinterface.repository.RepositoryDeposit;
import eu.eudat.depositinterface.repository.RepositoryDepositConfiguration;
import eu.eudat.depositinterface.zenodorepository.config.ConfigLoader;
import eu.eudat.depositinterface.zenodorepository.config.ZenodoConfig;
import eu.eudat.depositinterface.zenodorepository.mapper.DMPToZenodoMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;

@Component
public class ZenodoDeposit implements RepositoryDeposit {
    private static final Logger logger = LoggerFactory.getLogger(ZenodoDeposit.class);
    private static final ObjectMapper objectMapper = new ObjectMapper();

    private ConfigLoader configLoader;

    @Autowired
    public ZenodoDeposit(ConfigLoader configLoader){
        this.configLoader = configLoader;
    }

    @Override
    public String deposit(DMPDepositModel dmpDepositModel, boolean update, String zenodoToken) throws Exception {

        RepositoryDepositConfiguration conf = this.getConfiguration();

        if(zenodoToken == null){
            zenodoToken = conf.getAccessToken();
        }

        String zenodoUrl = conf.getRepositoryUrl();

        // First step, post call to Zenodo, to create the entry.
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.APPLICATION_JSON);
        eu.eudat.depositinterface.zenodorepository.models.ZenodoDeposit deposit = DMPToZenodoMapper.fromDMP(dmpDepositModel, "argos", "ARGOS", "https://argos.openaire.eu/", this.configLoader.getDOIFunders());

        HttpEntity<eu.eudat.depositinterface.zenodorepository.models.ZenodoDeposit> request = new HttpEntity<>(deposit, headers);
        Map createResponse;
        LinkedHashMap<String, String> links;
        String previousDOI = dmpDepositModel.getPreviousDOI();
        String unpublishedUrl = null;
        String publishUrl;
        String finalDoi;
        try {

            if (previousDOI == null) {
                String createUrl = zenodoUrl + "deposit/depositions" + "?access_token=" + zenodoToken;
                createResponse = restTemplate.postForEntity(createUrl, request, Map.class).getBody();
                links = (LinkedHashMap<String, String>) createResponse.get("links");
                finalDoi = (String) createResponse.get("conceptdoi");
            }
            else {
                unpublishedUrl = this.getUnpublishedDOI(zenodoUrl, previousDOI, zenodoToken, dmpDepositModel.getVersion());
                if (unpublishedUrl == null) {
                    //It requires more than one step to create a new version
                    //First, get the deposit related to the concept DOI
                    String listUrl = zenodoUrl + "deposit/depositions" + "?q=conceptdoi:\"" + previousDOI + "\"&access_token=" + zenodoToken;
                    ResponseEntity<Map[]> listResponses = restTemplate.getForEntity(listUrl, Map[].class);
                    createResponse = listResponses.getBody()[0];
                    links = (LinkedHashMap<String, String>) createResponse.get("links");
                    //Second, make the new version (not in the links?)
                    String newVersionUrl = links.get("self") + "/actions/newversion" + "?access_token=" + zenodoToken;
                    createResponse = restTemplate.postForObject(newVersionUrl, null, Map.class);
                    links = (LinkedHashMap<String, String>) createResponse.get("links");
                    //Third, get the new deposit
                    String latestDraftUrl = links.get("latest_draft") + "?access_token=" + zenodoToken;
                    createResponse = restTemplate.getForObject(latestDraftUrl, Map.class);
                    links = (LinkedHashMap<String, String>) createResponse.get("links");
                    finalDoi = (String) createResponse.get("conceptdoi");
                    //At this point it might fail to perform the next requests so enclose them with try catch
                    try {
                        //Forth, update the new deposit's metadata
                        String updateUrl = links.get("self") + "?access_token=" + zenodoToken;
                        restTemplate.put(updateUrl, request);
                        //And finally remove pre-existing files from it
                        String fileListUrl = links.get("self") + "/files" + "?access_token=" + zenodoToken;
                        ResponseEntity<Map[]> fileListResponse = restTemplate.getForEntity(fileListUrl, Map[].class);
                        for (Map file : fileListResponse.getBody()) {
                            String fileDeleteUrl = links.get("self") + "/files/" + file.get("id") + "?access_token=" + zenodoToken;
                            restTemplate.delete(fileDeleteUrl);
                        }
                    } catch (Exception e) {
                        //In case the last two steps fail delete the latest Deposit it in order to create a new one (only one at a time is allowed)
                        restTemplate.delete(latestDraftUrl);
                        throw e;
                    }
                }
                else {
                    String listUrl = zenodoUrl + "deposit/depositions" + "?q=conceptdoi:\"" + previousDOI + "\"&access_token=" + zenodoToken;
                    ResponseEntity<Map[]> listResponses = restTemplate.getForEntity(listUrl, Map[].class);
                    createResponse = listResponses.getBody()[0];
                    links = (LinkedHashMap<String, String>) createResponse.get("links");
                }
            }

            if (!update) {
                if (unpublishedUrl == null) {
                    // Second step, add the file to the entry.
                    File pdfFile = dmpDepositModel.getPdfFile();
                    String fileName = dmpDepositModel.getPdfFileName();
                    FileSystemResource fileSystemResource = new FileSystemResource(pdfFile);
                    HttpEntity<FileSystemResource> addFileMapRequest = new HttpEntity<>(fileSystemResource, null);

                    String addFileUrl = links.get("bucket") + "/" + fileName + "?access_token=" + zenodoToken;
                    restTemplate.put(addFileUrl, addFileMapRequest);

                    ResponseEntity<byte[]> jsonFile = dmpDepositModel.getRdaJson();
                    UUID jsonFileUUID = UUID.randomUUID();
                    File tempJsonFile = new File(jsonFileUUID + ".json"); // temp storage??
                    try (FileOutputStream jsonFos = new FileOutputStream(tempJsonFile)) {
                        jsonFos.write(jsonFile.getBody());
                        jsonFos.flush();
                    }
                    fileSystemResource = new FileSystemResource(tempJsonFile);
                    HttpHeaders jsonHeaders = new HttpHeaders();
                    jsonHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
                    addFileMapRequest = new HttpEntity<>(fileSystemResource, jsonHeaders);
                    String jsonFileName = jsonFile.getHeaders().get("Content-Disposition").get(0).substring(jsonFile.getHeaders().get("Content-Disposition").get(0).lastIndexOf('=')  + 1);
                    addFileUrl = links.get("bucket") + "/" + jsonFileName + "?access_token=" + zenodoToken;
                    restTemplate.put(addFileUrl, addFileMapRequest);
                    Files.deleteIfExists(tempJsonFile.toPath());


                    // Third post call to Zenodo to publish the entry and return the DOI.
                    publishUrl = links.get("publish") + "?access_token=" + zenodoToken;
                }
                else {
                    publishUrl = unpublishedUrl + "?access_token=" + zenodoToken;
                }
                finalDoi = this.publish(publishUrl);
            } else {
                Map<String, Object> editResponce = restTemplate.postForObject(links.get("edit") + "?access_token=" + zenodoToken, "", Map.class);
                restTemplate.put(links.get("self") + "?access_token=" + zenodoToken, request);
                finalDoi = this.publish(links.get("publish") + "?access_token=" + zenodoToken);
            }

            return finalDoi;
        } catch (HttpClientErrorException | HttpServerErrorException ex) {
            Map<String, String> parsedException = objectMapper.readValue(ex.getResponseBodyAsString(), HashMap.class);
            throw new IOException(parsedException.get("message"), ex);
        }

    }

    private String publish(String publishUrl){
        RestTemplate restTemplate = new RestTemplate();
        Map<String, Object> publishResponce = restTemplate.postForObject(publishUrl, "", Map.class);
        return (String) publishResponce.get("conceptdoi");
    }

    private void publishIfNot(String zenodoUrl, String doi, String zenodoToken, DMPDepositModel dmpDepositModel){
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.APPLICATION_JSON);

        eu.eudat.depositinterface.zenodorepository.models.ZenodoDeposit deposit = DMPToZenodoMapper.fromDMP(dmpDepositModel, "argos", "ARGOS", "https://argos.openaire.eu/", this.configLoader.getDOIFunders());
        HttpEntity<eu.eudat.depositinterface.zenodorepository.models.ZenodoDeposit> request = new HttpEntity<>(deposit, headers);

        String listUrl = zenodoUrl + "deposit/depositions" + "?q=conceptdoi:\"" + doi + "\"&access_token=" + zenodoToken;
        ResponseEntity<Map[]> listResponses = restTemplate.getForEntity(listUrl, Map[].class);
        Map createResponse = listResponses.getBody()[0];
        LinkedHashMap<String, String> links = (LinkedHashMap<String, String>) createResponse.get("links");
        Map<String, Object> editResponce = restTemplate.postForObject(links.get("edit") + "?access_token=" + zenodoToken, "", Map.class);

        restTemplate.put(links.get("self") + "?access_token=" + zenodoToken, request);
        this.publish(links.get("publish") + "?access_token=" + zenodoToken);
    }

    @Override
    public RepositoryDepositConfiguration getConfiguration() {
        ZenodoConfig zenodoConfig = this.configLoader.getZenodoConfig();
        return zenodoConfig.toRepoConfig();
    }

    @Override
    public String authenticate(String code){

        RepositoryDepositConfiguration conf = this.getConfiguration();

        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);

        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add("client_id", conf.getRepositoryClientId());
        map.add("client_secret", conf.getRepositoryClientSecret());
        map.add("grant_type", "authorization_code");
        map.add("code", code);
        map.add("redirect_uri", conf.getRedirectUri());
        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);

        try {
            Map<String, Object> values = restTemplate.postForObject(conf.getRepositoryAccessTokenUrl(), request, Map.class);
            //ZenodoResponseToken zenodoResponseToken = new ZenodoResponseToken();
            Map<String, Object> user = (Map<String, Object>) values.get("user");
//            zenodoResponseToken.setUserId((String) user.get("id"));
//            zenodoResponseToken.setEmail((String) user.get("email"));
//            zenodoResponseToken.setExpiresIn((Integer) values.get("expires_in"));
//            zenodoResponseToken.setAccessToken((String) values.get("access_token"));
//            zenodoResponseToken.setRefreshToken((String) values.get("refresh_token"));

            //return zenodoResponseToken;
            return (String) values.get("access_token");
        } catch (HttpClientErrorException ex) {
            logger.error(ex.getResponseBodyAsString(), ex);
        }

        return null;
    }

    private String getUnpublishedDOI(String zenodoUrl, String DOI, String token, Integer version) {
        try {
            RestTemplate restTemplate = new RestTemplate();
            HttpHeaders headers = new HttpHeaders();
            headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
            headers.setContentType(MediaType.APPLICATION_JSON);
            Map createResponse = null;
            LinkedHashMap<String, String> links = null;
            LinkedHashMap<String, String> metadata = null;
            String listUrl = zenodoUrl + "deposit/depositions" + "?q=conceptdoi:\"" + DOI + "\"&access_token=" + token;
            ResponseEntity<Map[]> listResponses = restTemplate.getForEntity(listUrl, Map[].class);
            createResponse = listResponses.getBody()[0];
            metadata = (LinkedHashMap<String, String>) createResponse.get("metadata");
            links = (LinkedHashMap<String, String>) createResponse.get("links");

            if (metadata.get("version").equals(version.toString())) {
                return links.get("publish");
            } else {
                return null;
            }
        }catch (Exception e) {
            logger.warn(e.getMessage(), e);
            return null;
        }
    }
}
