/*
 * 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.tallying.commons.io.writer;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
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.beans.Contest;
import com.scytl.products.ov.tallying.command.TallyingHolder;
import com.scytl.products.ov.tallying.commons.HashService;
import com.scytl.products.ov.tallying.commons.beans.ContestVote;
import com.scytl.products.ov.tallying.commons.beans.OptionData;
import com.scytl.products.ov.tallying.commons.exception.TallyingException;
import com.scytl.products.ov.tallying.commons.log.TallyingLogConstants;
import com.scytl.products.ov.tallying.commons.log.TallyingLogEvents;

/**
 * A class that writes the results of the tallying of successful votes to a csv
 * file.
 */
public class TalliedSuccessfulVotesWriter {

    private static final Logger LOG = LoggerFactory.getLogger("std");

    private static final String RESULT_FILE_EXTENSION = ".csv";

    private static final String VALUE_SEPARATOR = ";";

    private final SecureLoggingWriter loggingWriter;

    private final HashService hashService;

    private final SignResultWriter signResultWriter;

    public TalliedSuccessfulVotesWriter(final SignResultWriter signResultWriter, final HashService hashService,
            final SecureLoggingWriter loggingWriter) {
        this.signResultWriter = signResultWriter;
        this.hashService = hashService;
        this.loggingWriter = loggingWriter;
    }

    public void writeToFiles(final List<ContestVote> contestVotes, final TallyingHolder holder)
            throws TallyingException {

        LOG.info("Persisting tallying results in directory " + holder.getOutputDir());

        if (!Files.exists(holder.getOutputDir())) {
            try {
                Files.createDirectory(holder.getOutputDir());
            } catch (IOException e) {
                logErrorDuringTallyingFileCreating(holder, e, String.valueOf(holder.getOutputDir()));
                throw new TallyingException(TallyingLogEvents.TALLYING_FILE_CREATION_FAILED.getInfo(), e);
            }
        }

        Map<String, List<String>> resultValues = prepareResult(contestVotes);

        for (Contest contest : holder.getContests()) {

            String tallyFileAbsolutePath;
            String fileHash;
            try {
                tallyFileAbsolutePath =
                    writeToFile(holder.getOutputDir(), contest.getId(), resultValues.get(contest.getId()));
                fileHash = hashService.getHash(tallyFileAbsolutePath);
                loggingWriter.log(Level.INFO,
                    new LogContent.LogContentBuilder().logEvent(TallyingLogEvents.TALLYING_FILE_CREATED_SUCCESSFULLY)
                        .objectId(holder.getBallotBoxId()).user("adminID").electionEvent(holder.getElectionEventId())
                        .additionalInfo(TallyingLogConstants.TALLY_SHEET_ID, contest.getId())
                        .additionalInfo(TallyingLogConstants.TALLY_SHEET_HASH, fileHash).createLogInfo());

            } catch (IOException e) {
                logErrorDuringTallyingFileCreating(holder, e, contest.getId());
                throw new TallyingException(TallyingLogEvents.TALLYING_FILE_CREATION_FAILED.getInfo(), e);
            }

            try {
                signResultWriter.signOutput(Paths.get(tallyFileAbsolutePath), holder);
                loggingWriter.log(Level.INFO,
                    new LogContent.LogContentBuilder().logEvent(TallyingLogEvents.TALLYING_FILE_SIGNED_SUCCESSFULLY)
                        .objectId(holder.getBallotBoxId()).user("adminID").electionEvent(holder.getElectionEventId())
                        .additionalInfo(TallyingLogConstants.TALLY_SHEET_ID, contest.getId())
                        .additionalInfo(TallyingLogConstants.TALLY_SHEET_HASH, fileHash).createLogInfo());
            } catch (IOException | GeneralCryptoLibException e) {
                loggingWriter.log(Level.ERROR,
                    new LogContent.LogContentBuilder().logEvent(TallyingLogEvents.TALLYING_FILE_SIGNING_FAILED)
                        .objectId(holder.getBallotBoxId()).user("adminID").electionEvent(holder.getElectionEventId())
                        .additionalInfo(TallyingLogConstants.TALLY_SHEET_ID, contest.getId())
                        .additionalInfo(TallyingLogConstants.TALLY_SHEET_HASH, fileHash)
                        .additionalInfo(TallyingLogConstants.ERR_DESC, ExceptionUtils.getRootCauseMessage(e))
                        .createLogInfo());

                String errorMessage = "Error occurred during signing tallying file " + tallyFileAbsolutePath;
                LOG.error(errorMessage, e);
                throw new TallyingException(errorMessage, e);
            }
        }

        LOG.info("Tallying results saved for BallotBoxId " + holder.getBallotBoxId());
    }

    private String writeToFile(Path outputFileDir, String contestId, List<String> representations) throws IOException {

        File contextOutputDir = Paths.get(outputFileDir.toUri()).resolve(contestId).toFile();

        if (!contextOutputDir.exists() && !contextOutputDir.mkdir()) {
            String msg = "Directory creating failed: " + contextOutputDir;
            LOG.error(msg);
            throw new IOException(msg);
        }

        String fileAbsolutePath =
            contextOutputDir.getAbsolutePath() + File.separator + contestId + RESULT_FILE_EXTENSION;
        LOG.info("Persisting tallying results in file " + fileAbsolutePath);

        try (FileWriter fileWriter = new FileWriter(fileAbsolutePath);
				BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
				PrintWriter writer = new PrintWriter(bufferedWriter)) {
        	        
            if (representations != null) {
                representations.forEach(writer::println);
            }
        }

        LOG.info("Result was written to " + fileAbsolutePath);

        return fileAbsolutePath;
    }

    private Map<String, List<String>> prepareResult(List<ContestVote> contestVotes) {

        Map<String, List<String>> resultValues = new HashMap<>();
        LOG.info("Preparing tallying output...");

        for (ContestVote contestVote : contestVotes) {
            List<String> representationsList = resultValues.get(contestVote.getContestId());
            if (representationsList == null) {
                representationsList = new ArrayList<>();
                resultValues.put(contestVote.getContestId(), representationsList);
            }
            String writeIns = String.join(VALUE_SEPARATOR,
                contestVote.getRepresentations().stream().filter(option -> Objects.nonNull(option.getWriteInValue()))
                    .map(option -> option.getRepresentation() + "#" + option.getWriteInValue())
                    .collect(Collectors.toList()));
            String representation = contestVote.getCount() + VALUE_SEPARATOR + String.join(VALUE_SEPARATOR, contestVote
                .getRepresentations().stream().map(OptionData::getRepresentation).collect(Collectors.toList()));
            if (!writeIns.isEmpty()) {
                representation = representation + VALUE_SEPARATOR + writeIns;
            }
            representationsList.add(representation);
        }

        LOG.info("Tallying results ready.");

        return resultValues;
    }

    private void logErrorDuringTallyingFileCreating(TallyingHolder holder, IOException e, String tallyFileName) {
        loggingWriter.log(Level.ERROR,
            new LogContent.LogContentBuilder().logEvent(TallyingLogEvents.TALLYING_FILE_CREATION_FAILED)
                .objectId(holder.getBallotBoxId()).user("adminID").electionEvent(holder.getElectionEventId())
                .additionalInfo(TallyingLogConstants.TALLY_SHEET_ID, tallyFileName)
                .additionalInfo(TallyingLogConstants.ERR_DESC, ExceptionUtils.getRootCauseMessage(e)).createLogInfo());
    }

}
