/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.products.ov.sdm.application.service;

import static java.nio.file.Files.newInputStream;
import static java.nio.file.Files.readAllLines;
import static java.text.MessageFormat.format;
import static java.util.Arrays.fill;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore.PasswordProtection;
import java.security.PrivateKey;
import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.json.JsonObject;
import javax.security.auth.DestroyFailedException;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.scytl.keystore.ScytlKeyStoreServiceAPI;
import com.scytl.cryptolib.elgamal.bean.ElGamalPrivateKey;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.infrastructure.exception.OvCommonsInfrastructureException;
import com.scytl.products.ov.commons.infrastructure.remote.client.NodeIdentifier;
import com.scytl.products.ov.commons.infrastructure.remote.client.RestClientConnectionManager;
import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitConsumer;
import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitException;
import com.scytl.products.ov.commons.path.PathResolver;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.constants.Constants;
import com.scytl.products.ov.datapacks.beans.ConfigurationInput;
import com.scytl.products.ov.readers.ConfigurationInputReader;
import com.scytl.products.ov.sdm.application.exception.ElectionEventServiceException;
import com.scytl.products.ov.sdm.domain.model.electionevent.ElectionEventRepository;
import com.scytl.products.ov.sdm.domain.model.generator.DataGeneratorResponse;
import com.scytl.products.ov.sdm.domain.model.status.Status;
import com.scytl.products.ov.sdm.domain.service.ConfigConstants;
import com.scytl.products.ov.sdm.domain.service.ElectionEventDataGeneratorService;
import com.scytl.products.ov.sdm.infrastructure.JsonConstants;
import com.scytl.products.ov.sdm.infrastructure.clients.ElectionInformationClient;
import com.scytl.products.ov.sdm.infrastructure.service.ConfigurationEntityStatusService;

import okhttp3.ResponseBody;
import retrofit2.Retrofit;

/**
 * This is a service for handling election event entities.
 */
@Service
public class ElectionEventService {

    private static final Logger LOGGER = LoggerFactory.getLogger(ElectionEventService.class);

    @Autowired
    private ElectionEventDataGeneratorService electionEventDataGeneratorService;

    @Autowired
    private ConfigurationEntityStatusService configurationEntityStatusService;

    @Autowired
    private BallotBoxService ballotBoxService;

    @Autowired
    private ElectionEventRepository electionEventRepository;

    @Autowired
    private PathResolver pathResolver;

    @Value("${EI_URL}")
    private String electionInformationBaseURL;

    @Value("${tenantID}")
    private String tenantId;

    @Autowired
    private KeyStoreService keyStoreService;
    
    @Autowired
    private ScytlKeyStoreServiceAPI scytlKeyStoreService;
    
    @Autowired
    private ConfigurationInputReader configurationInputReader;

    private static final String CASTED_VC_NAME_FORMAT = "%s-usedVotingCards-%s";

    /**
     * This method creates an election event based on the given id and if
     * everything ok, it sets its status to ready.
     *
     * @param electionEventId
     *            identifies the election event to be created.
     * @return an object containing the result of the creation.
     */
    public DataGeneratorResponse create(String electionEventId) throws IOException {
        DataGeneratorResponse result = electionEventDataGeneratorService.generate(electionEventId);
        if (result.isSuccessful()) {
            configurationEntityStatusService.update(Status.READY.name(), electionEventId, electionEventRepository);
        }

        return result;
    }

    /**
     * Downloads the election participation file. This file contains the list of
     * voting cards (id) that have casted a vote.
     * 
     * @param electionEventId
     *            the election event identifier
     * @return if the file downloaded successfully or not.
     */
    public Path downloadParticipationFile(final String electionEventId) {

        LOGGER.info("Downloading participation file for election '{}'", electionEventId);
        final Path outputPath = pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR).resolve(electionEventId)
            .resolve(ConfigConstants.CONFIG_DIR_NAME_ONLINE)
            .resolve(ConfigConstants.CONFIG_DIR_NAME_ELECTIONINFORMATION).toAbsolutePath();

        try {
            Files.createDirectories(outputPath);

            PrivateKey requestSigningKey = keyStoreService.getPrivateKey();

            Retrofit restAdapter = null;
            try {
                restAdapter = RestClientConnectionManager.getInstance().getRestClientWithInterceptorAndJacksonConverter(
                    electionInformationBaseURL, requestSigningKey, NodeIdentifier.SECURE_DATA_MANAGER);


            } catch (OvCommonsInfrastructureException e) {
				LOGGER.error(
						String.format("Failed to download participation file for election '%s'. ", electionEventId));
				throw new ElectionEventServiceException("Error trying to get a RestClientConnectionManager instance.",
						e);
            }

            ElectionInformationClient eiClient = restAdapter.create(ElectionInformationClient.class);

            retrofit2.Response<ResponseBody> executeCall = RetrofitConsumer.executeCall(eiClient.getCastedVotingCards(tenantId, electionEventId));

            Path outputFilePath = null;

            String fileName = getFileNameFromHeaders(executeCall, electionEventId);
            outputFilePath = Paths.get(outputPath.toAbsolutePath().toString(), fileName);
            File outputFile = new File(outputFilePath.toAbsolutePath().toString());
            try (InputStream is = executeCall.body().byteStream()) {
                FileUtils.copyInputStreamToFile(is, outputFile);
            }
            LOGGER.info("Participation file downloaded successfully to '{}'", outputFilePath);


            return outputFilePath;

        } catch (IOException | RetrofitException e) {
            LOGGER.error(String.format("Failed to download participation file for election '%s': %s", electionEventId,
                ExceptionUtils.getRootCauseMessage(e)));
            throw new ElectionEventServiceException("Error trying to download participation file for election.", e);
        }
    }

    /**
     * Tries to retrieve the filename from response CONTENT_DISPOSITION header.
     * If the filename is unable to be located, then a default filename is
     * generated based on Election Event Id passed as parameter.
     * 
     * @param response
     * @param electionEventId
     * @return
     */
    private String getFileNameFromHeaders(final retrofit2.Response<ResponseBody> response, final String electionEventId) {
        String result = "";
        // We know that the following header contains the filename: Ex.-
        // "attachment; filename=test.csv"
        final String lookingFor = HttpHeaders.CONTENT_DISPOSITION.toLowerCase();
        for (String header : response.headers().names()) {
            if (lookingFor.equalsIgnoreCase(header)) {
                try {
                    String[] headerValues = response.headers().get(header).split(";");
                    String filename = headerValues[1];
                    String[] filenameValues = filename.split("=");
                    result = filenameValues[1];
                } catch (java.lang.ArrayIndexOutOfBoundsException e) {
                    LOGGER.warn("Unable to locate the filename in the response. A default one will be used.", e);
                }
                break;
            }
        }

        if (result.isEmpty()) {
            String timestamp = Long.toString(Instant.now(Clock.systemUTC()).getEpochSecond());
            String filenamePrefix = String.format(CASTED_VC_NAME_FORMAT, electionEventId, timestamp);
            String filename = filenamePrefix + ".zip";
            result = filename;
        }

        return result;
    }

    /**
     * This method returns election event alias based on the given id
     */
    public String getElectionEventAlias(String electionEventId) {

        return electionEventRepository.getElectionEventAlias(electionEventId);
    }

    /**
     * Get all available election events
     * 
     * @return the election events list
     */
    public List<String> getAllElectionEventIds() {

        return electionEventRepository.listIds();
    }

    /**
     * Gets the Election Event Json Object
     * 
     * @param electionEventId
     *            the election event id
     * @return the election event json object
     * @throws ResourceNotFoundException
     *             the resource not found exception
     */
    public JsonObject getElectionEventJsonObject(String electionEventId) throws ResourceNotFoundException {

        String electionEvent = electionEventRepository.find(electionEventId);

        if (StringUtils.isEmpty(electionEvent) || JsonConstants.JSON_EMPTY_OBJECT.equals(electionEvent)) {
            throw new ResourceNotFoundException("Election Event not found");
        }

        return JsonUtils.getJsonObject(electionEvent);
    }

    /**
     * Gets the Election Event Json Object by Ballot Box id
     * 
     * @param ballotBoxId
     *            the ballot box id
     * @return the election event json object
     * @throws ResourceNotFoundException
     *             the resourse not found exception
     */
    public JsonObject getElectionEventByBallotBox(String ballotBoxId) throws ResourceNotFoundException {
        JsonObject ballotBox = ballotBoxService.getBallotBoxJsonObject(ballotBoxId);
        return getElectionEventJsonObject(ballotBox.getString(JsonConstants.JSON_ATTRIBUTE_NAME_ELECTION_EVENT));
    }
    
    /**
     * Returns the prime number encryption private key of a given election event.
     * 
     * @param electionEventId the election event identifier
     * @return the key
     * @throws ResourceNotFoundException the election or the key has not been found
     */
    public ElGamalPrivateKey getPrimeEncryptionPrivateKey(String electionEventId) throws ResourceNotFoundException {
        ElGamalPrivateKey key;
        char[] password = getPrimeEncryptionPassword(electionEventId);
        try {
            CryptoAPIScytlKeyStore store = getPrimeEncryptionKeyStore(electionEventId, password);
            String alias = getPrimeEncryptionAlias();
            key = store.getElGamalPrivateKeyEntry(alias, password);
        } catch (GeneralCryptoLibException e) {
            throw new IllegalStateException("Invalid prime encryption keystore configuration.", e);
        } finally {
            fill(password, '\u0000');
        }
        if (key == null) {
            throw new ResourceNotFoundException(format(
                "Prime encryption key not found for election event {0}.",
                electionEventId));
        }
        return key;
    }
    
    /**
     * Returns the prime number encryption public key of a given election event.
     * 
     * @param electionEventId the election event identifier
     * @return the key
     * @throws ResourceNotFoundException the election or the key has not been found
     */
    public ElGamalPublicKey getPrimeEncryptionPublicKey(String electionEventId) throws ResourceNotFoundException {
        ElGamalPrivateKey privateKey = getPrimeEncryptionPrivateKey(electionEventId);
        ZpSubgroup group = privateKey.getGroup();
        ZpGroupElement generator = group.getGenerator();
        List<ZpGroupElement> elements = new ArrayList<>();
        try {
            for (Exponent exponent : privateKey.getKeys()) {
                elements.add(generator.exponentiate(exponent));
            }
            return new ElGamalPublicKey(elements, group);
        } catch (GeneralCryptoLibException e) {
            throw new IllegalStateException("Failed to create public key from the private one.", e);
        }
    }
    
    private char[] getPrimeEncryptionPassword(String electionEventId) throws ResourceNotFoundException {
        Path file = pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR)
                .resolve(electionEventId)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_OFFLINE)
                .resolve(Constants.PW_TXT);
        List<String> lines;
        try {
            lines = readAllLines(file);
        } catch (IOException e) {
            throw new ResourceNotFoundException(format(
                "Prime encryption password not found for election event ''{0}''.",
                electionEventId), e);
        }
        String prefix = "primeEncryption" + Constants.COMMA;
        for (String line : lines) {
            if (line.startsWith(prefix)) {
                return line.substring(prefix.length()).toCharArray();
            }
        }
        throw new ResourceNotFoundException(format(
            "Prime encryption password not found for election event ''{0}''.",
            electionEventId));
    }

    private CryptoAPIScytlKeyStore getPrimeEncryptionKeyStore(
            String electionEventId, char[] password) throws ResourceNotFoundException {
        CryptoAPIScytlKeyStore store;
        Path file = pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR)
                .resolve(electionEventId)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_OFFLINE)
                .resolve("primeEncryption" + Constants.SKS);
        try (InputStream stream = newInputStream(file)) {
            PasswordProtection protection = new PasswordProtection(password);
            try {
                store = scytlKeyStoreService.loadKeyStore(stream, protection);
            } finally {
                protection.destroy();
            }
        } catch (NoSuchFileException e) {
            throw new ResourceNotFoundException(format(
                "Prime encryption key store not found for election event ''{0}''.",
                electionEventId), e);
        } catch (IOException | GeneralCryptoLibException | DestroyFailedException e) {
            throw new IllegalStateException("Failed to get the prime encryption key store.", e);
        }
        return store;
    }

    private String getPrimeEncryptionAlias() {
        ConfigurationInput input;
        Path file = pathResolver.resolve(ConfigConstants.SDM_DIR_NAME, 
            ConfigConstants.SDM_CONFIG_DIR_NAME, 
            com.scytl.products.ov.constants.Constants.KEYS_CONFIG_FILENAME);
        try (InputStream stream = newInputStream(file)){
            input = configurationInputReader.fromStreamToJava(stream);
        } catch (IOException e) {
            throw new IllegalStateException(format("Failed to read config file''{0}''.", file), e);
        }
        Map<String, String> aliasMap = input.getPrimeEncryption().getAlias();
        return aliasMap.getOrDefault("privateKey", "elgamalprivatekey");
    }
}
