/*
 * 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 java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.annotation.PostConstruct;
import javax.json.JsonArray;
import javax.json.JsonObject;

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.stereotype.Service;

import com.scytl.cryptolib.api.primitives.PrimitivesServiceAPI;
import com.scytl.products.oscore.logging.api.domain.Level;
import com.scytl.products.oscore.logging.api.domain.LogContent;
import com.scytl.products.oscore.logging.api.writer.SecureLoggingWriter;
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.sdm.application.exception.BallotBoxDownloadServiceException;
import com.scytl.products.ov.sdm.application.log.SdmSecureLogEvent;
import com.scytl.products.ov.sdm.domain.model.ballotbox.BallotBoxRepository;
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.infrastructure.JsonConstants;
import com.scytl.products.ov.sdm.infrastructure.clients.ElectionInformationClient;
import com.scytl.products.ov.sdm.infrastructure.clients.OrchestratorClient;
import com.scytl.products.ov.sdm.infrastructure.service.ConfigurationEntityStatusService;

import okhttp3.ResponseBody;
import retrofit2.Retrofit;

/**
 * Service for handling download encrypted ballot boxes.
 */
@Service
public class BallotBoxDownloadService {

    private static final int BUFFER_SIZE = 8192;

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

    @Autowired
    private BallotBoxRepository ballotBoxRepository;

    @Autowired
    private ConfigurationEntityStatusService configurationEntityStatusService;

    @Autowired
    private PathResolver pathResolver;

    @Value("${OR_URL}")
    private String orchestratorUrl;

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

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

    @Autowired
    private KeyStoreService keystoreService;

    private Retrofit orchestratorRestAdapter;

    private Retrofit electionInformationRestAdapter;

    @Autowired
    private SecureLoggingWriter secureLogger;

    @Autowired
    private HashService hashService;

    @Autowired
    private PrimitivesServiceAPI primitivesService;

    @PostConstruct
    void configure() {
        try {
            PrivateKey requestSigningKey = keystoreService.getPrivateKey();

            orchestratorRestAdapter =
                RestClientConnectionManager.getInstance().getRestClientWithInterceptorAndJacksonConverter(
                    orchestratorUrl, requestSigningKey, NodeIdentifier.SECURE_DATA_MANAGER);

            electionInformationRestAdapter =
                RestClientConnectionManager.getInstance().getRestClientWithInterceptorAndJacksonConverter(
                    electionInformationUrl, requestSigningKey, NodeIdentifier.SECURE_DATA_MANAGER);

        } catch (OvCommonsInfrastructureException e) {
            LOGGER.error("Failed to initialize RestAdapter. ", e);
            throw new BallotBoxDownloadServiceException("Failed to initialize RestAdapter.", e);
        }
    }

    /**
     * Download a ballot box based on election event id and its id.
     *
     * @param electionEventId
     *            the election event id.
     * @param ballotBoxId
     *            the ballot box id.
     * @throws BallotBoxDownloadException
     *             if the ballot box to download does not exist in DB or the
     *             download/write of the ballot box fails.
     */
    public void download(final String electionEventId, final String ballotBoxId) throws BallotBoxDownloadException {
        JsonObject ballotBox = getBallotBoxJson(ballotBoxId, electionEventId);
        checkBallotBoxStatusForDownload(ballotBox, ballotBoxId, electionEventId);

        ResponseBody responseBody = downloadBallotBox(electionEventId, ballotBoxId);
        String ballotId = ballotBox.getJsonObject(JsonConstants.JSON_ATTRIBUTE_NAME_BALLOT)
            .getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);
        writeBallotBoxStreamToFile(electionEventId, ballotId, ballotBoxId, responseBody);
    }

    /**
     * Downloads the outputs generated when mixing the requested ballot box on
     * the online control component nodes.
     *
     * @param electionEventId
     *            the election event id.
     * @param ballotBoxId
     *            the ballot box id.
     * @throws BallotBoxDownloadException
     *             if the ballot box to download does not exist in DB or the
     *             download/write of the ballot box fails.
     */
    public void downloadPayloads(final String electionEventId, final String ballotBoxId)
            throws BallotBoxDownloadException {
        LOGGER.info("Requesting payloads for ballot box {}...", ballotBoxId);

        JsonObject ballotBox = getBallotBoxJson(ballotBoxId, electionEventId);
        checkBallotBoxStatusForDownload(ballotBox, ballotBoxId, electionEventId);

        String ballotId = ballotBox.getJsonObject(JsonConstants.JSON_ATTRIBUTE_NAME_BALLOT)
            .getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);

        // Check that the full ballot box has been mixed.
        // Could the node outputs be downloaded regardless?
        checkBallotBoxStatusForDownload(ballotBox, ballotBoxId, electionEventId);

        try (InputStream is = getMixingPayloads(electionEventId, ballotBoxId).byteStream()) {
            persistNodeOutputsLocally(electionEventId, ballotId, ballotBoxId, is);
        } catch (IOException e) {
            throw new BallotBoxDownloadException(
                "The node outputs for ballot box " + ballotBoxId + " could not be persisted locally", e);
        }
    }

    /**
     * Downloads the ballot box and returns it in a ResponseBody stream to be
     * processed.
     *
     * @param electionEventId
     *            the election event identifier.
     * @param ballotBoxId
     *            the ballot box identifier
     * @throws BallotBoxDownloadException
     *             if the download call fails.
     */
    private ResponseBody downloadBallotBox(String electionEventId, String ballotBoxId)
            throws BallotBoxDownloadException {
        ResponseBody response;
        try {
            response = RetrofitConsumer.processResponse(
                getElectionInformationClient().getRawBallotBox(tenantId, electionEventId, ballotBoxId));
        } catch (final RetrofitException e) {
            String errormessage =
                String.format("Failed to download encrypted BallotBox [electionEvent=%s, ballotBox=%s]: %s",
                    electionEventId, ballotBoxId, e.getMessage());
            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().objectId(ballotBoxId).electionEvent(electionEventId)
                    .logEvent(SdmSecureLogEvent.BALLOT_BOX_DOWNLOADING_FAILED)
                    .additionalInfo("file_name",
                        ConfigConstants.CONFIG_FILE_NAME_ELECTION_INFORMATION_DOWNLOADED_BALLOT_BOX)
                    .additionalInfo("err_desc", "Error downloading ballotBox. HTTP status: " + e.getHttpCode())
                    .createLogInfo());
            throw new BallotBoxDownloadException(errormessage, e);
        }
        return response;
    }

    /**
     * Downloads the outputs of the online mixing nodes.
     *
     * @param electionEventId
     *            the election event identifier.
     * @param ballotBoxId
     *            the ballot box identifier
     * @throws BallotBoxDownloadException
     *             if the download call fails.
     */
    private ResponseBody getMixingPayloads(String electionEventId, String ballotBoxId)
            throws BallotBoxDownloadException {
        ResponseBody response;
        try {
            response = RetrofitConsumer
                .processResponse(getOrchestratorClient().getMixDecNodeOutputs(tenantId, electionEventId, ballotBoxId));
        } catch (final RetrofitException e) {
            String errormessage =
                String.format("Failed to download the mixing payloads [electionEvent=%s, ballotBox=%s]: %s",
                    electionEventId, ballotBoxId, e.getMessage());
            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().objectId(ballotBoxId).electionEvent(electionEventId)
                    .logEvent(SdmSecureLogEvent.BALLOT_BOX_DOWNLOADING_FAILED)
                    .additionalInfo("file_name",
                        ConfigConstants.CONFIG_FILE_NAME_ELECTION_INFORMATION_DOWNLOADED_BALLOT_BOX)
                    .additionalInfo("err_desc", "Error downloading ballotBox. HTTP status: " + e.getHttpCode())
                    .createLogInfo());
            throw new BallotBoxDownloadException(errormessage, e);
        }

        return response;
    }

    private String hashBallotBox(Path ballotBoxTempFile) {

        String ballotBoxHash = "";
        try {
            ballotBoxHash = hashService.getB64FileHash(ballotBoxTempFile);
        } catch (HashServiceException e) {
        	LOGGER.error("Error performing hash of ballot box file {} ", ballotBoxTempFile.toString(), e);
        }
        return ballotBoxHash;
    }

    /**
     * Extracts the ballot box bytes from the stream and write them to file.
     *
     * @param responseBody
     *            the stream containing the ballot box raw bytes
     */
    private void writeBallotBoxStreamToFile(String electionEventId, String ballotId, String ballotBoxId,
            ResponseBody responseBody) throws BallotBoxDownloadException {
        Path ballotBoxFile;
        try (InputStream stream = responseBody.byteStream()) {
            ballotBoxFile = getDownloadedBallotBoxPath(electionEventId, ballotId, ballotBoxId);
            Files.copy(stream, ballotBoxFile, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            String errorMessage =
                String.format("Failed to write downloaded BallotBox [electionEvent=%s, ballotBox=%s] to file.",
                    electionEventId, ballotBoxId);
            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().objectId(ballotBoxId).electionEvent(electionEventId)
                    .logEvent(SdmSecureLogEvent.BALLOT_BOX_DOWNLOADING_FAILED)
                    .additionalInfo("file_name",
                        ConfigConstants.CONFIG_FILE_NAME_ELECTION_INFORMATION_DOWNLOADED_BALLOT_BOX)
                    .additionalInfo("err_desc", errorMessage + ": " + e.getMessage()).createLogInfo());
            throw new BallotBoxDownloadException(errorMessage, e);
        }

        String ballotBoxHash = hashBallotBox(ballotBoxFile);
        secureLogger.log(Level.INFO,
            new LogContent.LogContentBuilder().objectId(ballotBoxId).electionEvent(electionEventId)
                .logEvent(SdmSecureLogEvent.BALLOT_BOX_DOWNLOADED_SUCCESSFULLY)
                .additionalInfo("file_name",
                    ConfigConstants.CONFIG_FILE_NAME_ELECTION_INFORMATION_DOWNLOADED_BALLOT_BOX)
                .additionalInfo("file_h", ballotBoxHash).createLogInfo());
    }

    /**
     * Stores downloaded mixing payloads locally.
     *
     * @param payloadZipStream
     *            an input stream of a zip file containing the payloads in JSON
     *            format.
     */
    private void persistNodeOutputsLocally(String electionEventId, String ballotId, String ballotBoxId,
            InputStream payloadZipStream) throws BallotBoxDownloadException {

    	// The input stream is a zip file with the payload JSONs in it.
        try (ZipInputStream zis = new ZipInputStream(payloadZipStream)) {
        	
        	Path ballotBoxPath = getBallotBoxPath(electionEventId, ballotId, ballotBoxId);
        	MessageDigest messageDigest = primitivesService.getRawMessageDigest();
        	
        	storingPayload(zis, ballotBoxPath, messageDigest);

        	secureLogger.log(Level.INFO,
					new LogContent.LogContentBuilder().objectId(ballotBoxId).electionEvent(electionEventId)
							.logEvent(SdmSecureLogEvent.BALLOT_BOX_DOWNLOADED_SUCCESSFULLY)
							.additionalInfo("file_name",
									ConfigConstants.CONFIG_FILE_NAME_ELECTION_INFORMATION_DOWNLOADED_BALLOT_BOX)
							.additionalInfo("file_h", new String(messageDigest.digest())).createLogInfo());
        
        } catch (IOException e) {
			String errorMessage = String.format("Node output %s could not be persisted locally", electionEventId);
			secureLogger.log(Level.ERROR,
					new LogContent.LogContentBuilder().objectId(ballotBoxId).electionEvent(electionEventId)
							.logEvent(SdmSecureLogEvent.BALLOT_BOX_DOWNLOADING_FAILED)
							.additionalInfo("file_name",
									ConfigConstants.CONFIG_FILE_NAME_ELECTION_INFORMATION_DOWNLOADED_BALLOT_BOX)
							.additionalInfo("err_desc", errorMessage + ": " + e.getMessage()).createLogInfo());
			
			throw new BallotBoxDownloadException(errorMessage, e);
        }

    }

    public void updateBallotBoxStatus(String ballotBoxId) {
        configurationEntityStatusService.update(Status.BB_DOWNLOADED.name(), ballotBoxId, ballotBoxRepository);
    }

    /**
     * Returns a client for mixed decrypted ballot box in orchestrator context.
     * Used for testing purposes with spy.
     *
     * @return the rest client.
     */
    OrchestratorClient getOrchestratorClient() {
        return orchestratorRestAdapter.create(OrchestratorClient.class);

    }

    /**
     * Returns a client for the raw ballot box in election information context.
     * Used for testing purposes with spy.
     *
     * @return the rest client.
     */
    ElectionInformationClient getElectionInformationClient() {
        return electionInformationRestAdapter.create(ElectionInformationClient.class);

    }

    /**
     * Check if the ballot box has the proper status to be downloaded.
     *
     * @param ballotBox
     *            the ballotBox json object obtained from DB.
     * @param ballotBoxId
     *            the ballotBox identifier.
     * @param electionEventId
     *            the election event identifier.
     * @throws BallotBoxDownloadException
     *             if the ballot box status is not appropiate for downloading.
     */
    private void checkBallotBoxStatusForDownload(JsonObject ballotBox, String ballotBoxId, String electionEventId)
            throws BallotBoxDownloadException {
        LOGGER.info("Checking whether ballot box {} can be downloaded...", ballotBoxId);
        Status status = Status.valueOf(ballotBox.getString(JsonConstants.JSON_ATTRIBUTE_NAME_STATUS));
        boolean isSynchronized = Boolean.parseBoolean(ballotBox.getString(JsonConstants.JSON_ATTRIBUTE_NAME_SYNCHRONIZED));
        boolean test = Boolean.parseBoolean(ballotBox.getString(JsonConstants.JSON_ATTRIBUTE_NAME_TEST));

        boolean result = Status.MIXED.equals(status) || (Status.SIGNED.equals(status) && isSynchronized && test);

        if (!result) {
            String errorMessage = "";
            if (!Status.MIXED.equals(status)) {
                if (!Status.SIGNED.equals(status)) {
                    errorMessage = "Ballot box can not be downloaded because its status is " + status;
                } else {
                    errorMessage = "Ballot box can not be downloaded because is not synchronized or it is not test";
                }
            }

            LOGGER.warn(errorMessage);

            secureLogger.log(Level.WARN,
                new LogContent.LogContentBuilder().objectId(ballotBoxId).electionEvent(electionEventId)
                    .logEvent(SdmSecureLogEvent.BALLOT_BOX_NOT_DOWNLOADED).additionalInfo("err_desc", errorMessage)
                    .createLogInfo());
            throw new BallotBoxDownloadException(errorMessage);
        }

        LOGGER.info("Ballot box {} can be downloaded.", ballotBoxId);
    }

    /**
     * Retrieves a ballot box and returns its JSON representation
     * 
     * @param ballotBoxId
     *            the ballot box identifier
     * @param electionEventId
     *            the election event the ballot box belongs to
     * @return a JSON representation of the ballot box
     * @throws BallotBoxDownloadException
     */
    private JsonObject getBallotBoxJson(String ballotBoxId, String electionEventId) throws BallotBoxDownloadException {
        LOGGER.info("Retrieving the JSON representation of ballot box {}...", ballotBoxId);

        Map<String, Object> criteria = new HashMap<>();
        criteria.put(JsonConstants.JSON_ATTRIBUTE_NAME_ID, ballotBoxId);
        criteria.put(JsonConstants.JSON_STRUCTURE_NAME_ELECTION_EVENT_ID, electionEventId);
        String json = ballotBoxRepository.list(criteria);
        JsonArray ballotBoxes = JsonUtils.getJsonObject(json).getJsonArray(JsonConstants.JSON_ATTRIBUTE_NAME_RESULT);

        if (ballotBoxes.isEmpty()) {
            LOGGER.info("No ballot boxes found", ballotBoxId);
            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().objectId(ballotBoxId).electionEvent(electionEventId)
                    .logEvent(SdmSecureLogEvent.BALLOT_BOX_NOT_DOWNLOADED)
                    .additionalInfo("err_desc", "Ballot boxes are empty").createLogInfo());
            throw new BallotBoxDownloadException("Ballot boxes are empty");
        }

        return ballotBoxes.getJsonObject(0);
    }

    private Path getDownloadedBallotBoxPath(final String electionEventId, final String ballotId,
            final String ballotBoxId) {
        return pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR, electionEventId,
            ConfigConstants.CONFIG_DIR_NAME_ONLINE, ConfigConstants.CONFIG_DIR_NAME_ELECTIONINFORMATION,
            ConfigConstants.CONFIG_DIR_NAME_BALLOTS, ballotId, ConfigConstants.CONFIG_DIR_NAME_BALLOTBOXES, ballotBoxId,
            ConfigConstants.CONFIG_FILE_NAME_ELECTION_INFORMATION_DOWNLOADED_BALLOT_BOX);
    }

    /**
     * Gets the location in the local file system where a particular ballot
     * box's data is kept.
     * 
     * @param electionEventId
     *            the identifier of the election event the ballot box belongs to
     * @param ballotId
     *            the identifier of the ballot definition for the ballot box
     * @param ballotBoxId
     *            the ballot box identifier
     * @return the path where the ballot box's data is kept
     */
    private Path getBallotBoxPath(String electionEventId, String ballotId, String ballotBoxId) {
        return pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR, electionEventId,
            ConfigConstants.CONFIG_DIR_NAME_ONLINE, ConfigConstants.CONFIG_DIR_NAME_ELECTIONINFORMATION,
            ConfigConstants.CONFIG_DIR_NAME_BALLOTS, ballotId, ConfigConstants.CONFIG_DIR_NAME_BALLOTBOXES,
            ballotBoxId);
    }
    
    /**
     * Write the zip entries to the corresponding ballot box directory.
     * 
     * @param zis Zip input Stream
     * @throws IOException 
     */
	private void storingPayload(ZipInputStream zis, Path ballotBoxPath, MessageDigest messageDigest)
			throws IOException {
    	
		byte[] buffer = new byte[BUFFER_SIZE];	
		messageDigest.reset();

		ZipEntry zipEntry;
		while (null != (zipEntry = zis.getNextEntry())) {
			String payloadFileName = zipEntry.getName();
			LOGGER.info("Storing payload {}...", payloadFileName);

			Path nodeOutputFilePath = ballotBoxPath.resolve(payloadFileName);
			try (OutputStream os = Files.newOutputStream(nodeOutputFilePath)) {
				int len;
				while ((len = zis.read(buffer)) > 0) {
					// Write the payload file.
					os.write(buffer, 0, len);
					// Contribute to the hash of the downloaded contents.
					messageDigest.update(buffer);
				}
			}

			LOGGER.info("Payload stored in {}", nodeOutputFilePath.toString());
		}
	}
}
