/*
 * 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 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.sdm.application.log.SdmSecureLogEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;

import com.scytl.products.ov.commons.path.PathResolver;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.sdm.domain.model.electionevent.ElectionEventRepository;
import com.scytl.products.ov.sdm.domain.model.status.Status;
import com.scytl.products.ov.sdm.domain.model.status.SynchronizeStatus;
import com.scytl.products.ov.sdm.domain.model.verification.VerificationCardSetDataUploadRepository;
import com.scytl.products.ov.sdm.domain.model.votingcardset.VotingCardSetRepository;
import com.scytl.products.ov.sdm.domain.model.votingcardset.VotingCardSetUploadRepository;
import com.scytl.products.ov.sdm.domain.service.ConfigConstants;
import com.scytl.products.ov.sdm.infrastructure.JsonConstants;
import static java.nio.file.Files.newDirectoryStream;

/**
 * Service which will upload the information related to the voting card set
 */
@Service
public class VotingCardSetUploadService {

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

    private static final String NULL_ELECTION_EVENT_ID = "";

    private static final String CONSTANT_VERIFICATIONCARDSETDATA = "verificationCardSetData";

    private static final String CONSTANT_VOTEVERIFICATIONCONTEXTDATA = "voteVerificationContextData";

    @Autowired
    private VotingCardSetRepository votingCardSetRepository;

    @Autowired
    private VotingCardSetUploadRepository votingCardSetUploadRepository;

    @Autowired
    private VerificationCardSetDataUploadRepository verificationCardSetUploadRepository;

    @Autowired
    private ElectionEventRepository electionEventRepository;

    @Autowired
    private ExtendedAuthenticationUploadService extendedAuthenticationUploadService;

    @Autowired
    private PathResolver pathResolver;

    @Autowired
    private SignatureService signatureService;

    @Autowired
    private SecureLoggingWriter secureLogger;

    /**
     * Uploads the available ballots and ballot texts to the voter portal.
     */

    public void uploadSyncronizableVotingCardSets(final String electionEvent) throws IOException {

        Map<String, Object> votingCardSetsParams = new HashMap<>();
        addSigned(votingCardSetsParams);
        addPendingToSynchronize(votingCardSetsParams);
        addElectionEventIdIfNotNull(electionEvent, votingCardSetsParams);

        String votingCardSetDocuments = votingCardSetRepository.list(votingCardSetsParams);

        JsonArray votingCardSets =
            JsonUtils.getJsonObject(votingCardSetDocuments).getJsonArray(JsonConstants.JSON_ATTRIBUTE_NAME_RESULT);

        for (int i = 0; i < votingCardSets.size(); i++) {

            JsonObject votingCardSetInArray = votingCardSets.getJsonObject(i);
            String electionEventId =
                votingCardSetInArray.getJsonObject(JsonConstants.JSON_ATTRIBUTE_NAME_ELECTION_EVENT)
                    .getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);

            JsonObject eEvent = JsonUtils.getJsonObject(electionEventRepository.find(electionEventId));
            JsonObject adminBoard = eEvent.getJsonObject(JsonConstants.JSON_ATTRIBUTE_NAME_ADMINISTRATION_AUTHORITY);

            String adminBoardId = adminBoard.getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);

            String votingCardSetId = votingCardSetInArray.getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);
            String verificationCardSetId =
                votingCardSetInArray.getString(JsonConstants.JSON_ATTRIBUTE_NAME_VERIFICATIONCARDSETID);

            Path voterMaterialPath = pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR)
                .resolve(electionEventId).resolve(ConfigConstants.CONFIG_DIR_NAME_ONLINE)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_VOTERMATERIAL).resolve(votingCardSetId);

            Path voteVerificationPath = pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR)
                .resolve(electionEventId).resolve(ConfigConstants.CONFIG_DIR_NAME_ONLINE)
                .resolve(ConfigConstants.CONFIG_DIR_NAME_VOTERVERIFICATION).resolve(verificationCardSetId);

            Path extendedAuthenticationPath = pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR)
                .resolve(electionEventId).resolve(ConfigConstants.CONFIG_DIR_NAME_ONLINE)
                .resolve(ConfigConstants.CONFIG_FILE_EXTENDED_AUTHENTICATION_DATA).resolve(votingCardSetId);

            JsonObjectBuilder builder = Json.createObjectBuilder();
            try {
                LOGGER.info("Uploading voter material configuration");
                uploadVoterMaterialContents(voterMaterialPath, electionEventId, votingCardSetId, adminBoardId);
                LOGGER.info("Uploading vote verification configuration");
                uploadVoteVerificationContents(voteVerificationPath, electionEventId, verificationCardSetId,
                    adminBoardId);
                LOGGER.info("Uploading extended authentication");
                extendedAuthenticationUploadService.uploadExtendedAuthenticationFiles(extendedAuthenticationPath,
                    electionEventId, adminBoardId);

                builder.add(JsonConstants.JSON_ATTRIBUTE_NAME_ID, votingCardSetId);
                builder.add(JsonConstants.JSON_ATTRIBUTE_NAME_SYNCHRONIZED,
                    SynchronizeStatus.SYNCHRONIZED.getIsSynchronized().toString());
                builder.add(JsonConstants.JSON_ATTRIBUTE_NAME_DETAILS, SynchronizeStatus.SYNCHRONIZED.getStatus());
                LOGGER.info("The voting card and verification card sets where successfully uploaded");
            } catch (IOException e) {
                LOGGER.error("An error occurred while uploading the signed voting card set and verification card set",
                    e);
                builder.add(JsonConstants.JSON_ATTRIBUTE_NAME_ID, votingCardSetId);
                builder.add(JsonConstants.JSON_ATTRIBUTE_NAME_DETAILS, SynchronizeStatus.FAILED.getStatus());
                secureLogger.log(Level.ERROR,
                    new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.SYNCHRONIZING_WITH_VP_FAILED)
                        .electionEvent(electionEventId)
                        .additionalInfo("err_desc",
                            "An error occurred while uploading the signed voting card set and verification card set: "
                                + e.getMessage())
                        .createLogInfo());

            }
            LOGGER.info("Changing the state of the voting card set");
            votingCardSetRepository.update(builder.build().toString());
        }
    }

    private void addElectionEventIdIfNotNull(final String electionEvent,
            final Map<String, Object> votingCardSetsParams) {
        if (!NULL_ELECTION_EVENT_ID.equals(electionEvent)) {
            votingCardSetsParams.put(JsonConstants.JSON_STRUCTURE_NAME_ELECTION_EVENT_ID, electionEvent);
        }
    }

    private void addPendingToSynchronize(final Map<String, Object> votingCardSetsParams) {
        votingCardSetsParams.put(JsonConstants.JSON_ATTRIBUTE_NAME_SYNCHRONIZED,
            SynchronizeStatus.PENDING.getIsSynchronized().toString());
    }

    private void addSigned(final Map<String, Object> votingCardSetsParams) {
        votingCardSetsParams.put(JsonConstants.JSON_ATTRIBUTE_NAME_STATUS, Status.SIGNED.name());
    }

    private void uploadVoterMaterialContents(final Path voterMaterialPath, final String electionEventId,
            final String votingCardSetId, final String adminBoardId) throws IOException {
        try (DirectoryStream<Path> files = newDirectoryStream(voterMaterialPath, ConfigConstants.CSV_GLOB)) {
            for (Path file : files) {
                String name = file.getFileName().toString();
                if (name.startsWith(ConfigConstants.CONFIG_FILE_NAME_VOTER_INFORMATION)) {
                    LOGGER.info("Uploading voter information file " + name + " and its signature");
                    uploadVoterInformationFromCSV(electionEventId, votingCardSetId, adminBoardId, file);
                } else if (name.startsWith(ConfigConstants.CONFIG_FILE_NAME_CREDENTIAL_DATA)) {
                    LOGGER.info("Uploading credential data file " + name + " and its signature");
                    uploadCredentialDataFromCSV(electionEventId, votingCardSetId, adminBoardId, file);
                }
            }
        }
    }

    private void uploadVoteVerificationContents(final Path voteVerificationPath, final String electionEventId,
            final String verificationCardSetId, final String adminBoardId) throws IOException {
        try (DirectoryStream<Path> files = newDirectoryStream(voteVerificationPath, ConfigConstants.CSV_GLOB)) {
            for (Path file : files) {
                String name = file.getFileName().toString();
                if (name.startsWith(ConfigConstants.CONFIG_FILE_NAME_CODES_MAPPING)) {
                    LOGGER.info("Uploading codes mapping file " + name + " and its signature");
                    uploadCodesMappingFromCSV(electionEventId, verificationCardSetId, adminBoardId, file);
                } else if (name.startsWith(ConfigConstants.CONFIG_FILE_VERIFICATION_CARD_DATA)) {
                    LOGGER.info("Uploading verification card data file " + name + " and its signature");
                    uploadVerificationCardDataFromCSV(electionEventId, verificationCardSetId, adminBoardId, file);
                } else if (name.startsWith(ConfigConstants.CONFIG_FILE_NAME_DERIVED_KEYS)) {
                    LOGGER.info("Uploading verification card derived keys " + name + " and its signature");
                    uploadVerificationCardDerivedKeysFromCSV(electionEventId, verificationCardSetId, adminBoardId, file);
                }
            }
        }
        uploadVerificationCardSetDataFromJSON(electionEventId, verificationCardSetId, adminBoardId,
            voteVerificationPath);
    }
    
    private void uploadVerificationCardDerivedKeysFromCSV(final String electionEventId, final String verificationCardSetId,
            final String adminBoardId, final Path filePath) throws IOException {

        String fileName = filePath.getFileName().toString();

        try (InputStream stream = signatureService.newCSVAndSignatureInputStream(filePath)) {
            verificationCardSetUploadRepository.uploadVerificationCardDerivedKeys(electionEventId, verificationCardSetId,
                adminBoardId, stream);

            Path signatureFile = filePath.resolveSibling(filePath.getFileName() + ConfigConstants.SIGN);
            byte[] signature = Files.readAllBytes(signatureFile);
            String b64FileSignature = new String(signature, StandardCharsets.UTF_8);

            secureLogger.log(Level.INFO, new LogContent.LogContentBuilder()
                .logEvent(SdmSecureLogEvent.VERIFICATION_CARD_DATA_UPLOADED_SUCCESSFULLY).electionEvent(electionEventId)
                .additionalInfo("file_name", fileName).additionalInfo("file_s", b64FileSignature).createLogInfo());
        } catch (IOException e) {
        	LOGGER.error("Error uploading verification card derived keys file and its signature.", e);
            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().electionEvent(electionEventId)
                    .logEvent(SdmSecureLogEvent.VERIFICATION_CARD_DATA_UPLOADING_FAILED)
                    .additionalInfo("file_name", fileName)
                    .additionalInfo("err_desc", "Error uploading verification card derived keys file and its signature")
                    .createLogInfo());
        }
    }

    private void uploadVoterInformationFromCSV(final String electionEventId, final String votingCardSetId,
            final String adminBoardId, final Path filePath) throws IOException {

        String fileName = filePath.getFileName().toString();

        try (InputStream stream = signatureService.newCSVAndSignatureInputStream(filePath)) {

            votingCardSetUploadRepository.uploadVoterInformation(electionEventId, votingCardSetId, adminBoardId,
                stream);
            Path signatureFile = filePath.resolveSibling(filePath.getFileName() + ConfigConstants.SIGN);
            byte[] signature = Files.readAllBytes(signatureFile);
            String b64FileSignature = new String(signature, StandardCharsets.UTF_8);

            secureLogger.log(Level.INFO,
                new LogContent.LogContentBuilder().electionEvent(electionEventId)
                    .logEvent(SdmSecureLogEvent.VOTER_INFORMATION_UPLOADED_SUCCESSFULLY)
                    .additionalInfo("file_name", fileName).additionalInfo("file_s", b64FileSignature).createLogInfo());

        } catch (IOException e) {
        	LOGGER.error("Error uploading voter information file and its signature.", e);
            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().electionEvent(electionEventId)
                    .logEvent(SdmSecureLogEvent.VOTER_INFORMATION_FILE_UPLOADING_FAILED)
                    .additionalInfo("file_name", fileName)
                    .additionalInfo("err_desc", "Error uploading voter information file and its signature").createLogInfo());
        }
    }

    private void uploadCredentialDataFromCSV(final String electionEventId, final String votingCardSetId,
            final String adminBoardId, final Path filePath) throws IOException {

        String fileName = filePath.getFileName().toString();

        try (InputStream stream = signatureService.newCSVAndSignatureInputStream(filePath)) {
            votingCardSetUploadRepository.uploadCredentialData(electionEventId, votingCardSetId, adminBoardId, stream);

            Path signatureFile = filePath.resolveSibling(filePath.getFileName() + ConfigConstants.SIGN);
            byte[] signature = Files.readAllBytes(signatureFile);
            String b64FileSignature = new String(signature, StandardCharsets.UTF_8);

            secureLogger.log(Level.INFO,
                new LogContent.LogContentBuilder().electionEvent(electionEventId)
                    .logEvent(SdmSecureLogEvent.CREDENTIAL_DATA_UPLOADED_SUCCESSFULLY)
                    .additionalInfo("file_name", fileName).additionalInfo("file_s", b64FileSignature).createLogInfo());

        } catch (IOException e) {
        	LOGGER.error("Error uploading credential data file and its signature.", e);
            secureLogger.log(Level.ERROR, new LogContent.LogContentBuilder().electionEvent(electionEventId)
                .logEvent(SdmSecureLogEvent.CREDENTIAL_DATA_UPLOADING_FAILED).additionalInfo("file_name", fileName)
                .additionalInfo("err_desc", "Error uploading credential data file and its signature").createLogInfo());
        }
    }

    private void uploadCodesMappingFromCSV(final String electionEventId, final String verificationCardSetId,
            final String adminBoardId, final Path filePath) throws IOException {

        String fileName = filePath.getFileName().toString();

        try (InputStream stream = signatureService.newCSVAndSignatureInputStream(filePath)) {
            verificationCardSetUploadRepository.uploadCodesMapping(electionEventId, verificationCardSetId, adminBoardId,
                stream);

            Path signatureFile = filePath.resolveSibling(filePath.getFileName() + ConfigConstants.SIGN);
            byte[] signature = Files.readAllBytes(signatureFile);
            String b64FileSignature = new String(signature, StandardCharsets.UTF_8);

            secureLogger.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.CODES_MAPPING_UPLOADED_SUCCESSFULLY)
                    .electionEvent(electionEventId).additionalInfo("file_name", fileName)
                    .additionalInfo("file_s", b64FileSignature).createLogInfo());

        } catch (IOException e) {
        	LOGGER.error("Error uploading codes mapping file and its signature.", e);
            secureLogger.log(Level.ERROR, new LogContent.LogContentBuilder().electionEvent(electionEventId)
                .logEvent(SdmSecureLogEvent.CODES_MAPPING_UPLOADING_FAILED).additionalInfo("file_name", fileName)
                .electionEvent(electionEventId)
                .additionalInfo("err_desc", "Error uploading codes mapping file and its signature").createLogInfo());
        }
    }

    private void uploadVerificationCardDataFromCSV(final String electionEventId, final String verificationCardSetId,
            final String adminBoardId, final Path filePath) throws IOException {

        String fileName = filePath.getFileName().toString();

        try (InputStream stream = signatureService.newCSVAndSignatureInputStream(filePath)) {
            verificationCardSetUploadRepository.uploadVerificationCardData(electionEventId, verificationCardSetId,
                adminBoardId, stream);

            Path signatureFile = filePath.resolveSibling(filePath.getFileName() + ConfigConstants.SIGN);
            byte[] signature = Files.readAllBytes(signatureFile);
            String b64FileSignature = new String(signature, StandardCharsets.UTF_8);

            secureLogger.log(Level.INFO, new LogContent.LogContentBuilder()
                .logEvent(SdmSecureLogEvent.VERIFICATION_CARD_DATA_UPLOADED_SUCCESSFULLY).electionEvent(electionEventId)
                .additionalInfo("file_name", fileName).additionalInfo("file_s", b64FileSignature).createLogInfo());
        } catch (IOException e) {
        	LOGGER.error("Error uploading verification card data file and its signature.", e);
            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().electionEvent(electionEventId)
                    .logEvent(SdmSecureLogEvent.VERIFICATION_CARD_DATA_UPLOADING_FAILED)
                    .additionalInfo("file_name", fileName)
                    .additionalInfo("err_desc", "Error uploading verification card data file and its signature")
                    .createLogInfo());
        }
    }

    private void uploadVerificationCardSetDataFromJSON(final String electionEventId, final String verificationCardSetId,
            final String adminBoardId, final Path filePath) throws IOException {
        Path verificationCardSetPath = filePath.resolve(ConfigConstants.CONFIG_FILE_NAME_SIGNED_VERIFICATIONSET_DATA);
        Path verificationContextPath =
            filePath.resolve(ConfigConstants.CONFIG_FILE_NAME_SIGNED_VERIFICATION_CONTEXT_DATA);
        JsonObject verificationCardSetData =
            JsonUtils.getJsonObject(new String(Files.readAllBytes(verificationCardSetPath), StandardCharsets.UTF_8));
        JsonObject verificationContextData =
            JsonUtils.getJsonObject(new String(Files.readAllBytes(verificationContextPath), StandardCharsets.UTF_8));
        JsonObject jsonInput =
            Json.createObjectBuilder().add(CONSTANT_VERIFICATIONCARDSETDATA, verificationCardSetData.toString())
                .add(CONSTANT_VOTEVERIFICATIONCONTEXTDATA, verificationContextData.toString()).build();

        LOGGER.info("Uploading verification card set and context configuration");
        verificationCardSetUploadRepository.uploadVerificationCardSetData(electionEventId, verificationCardSetId,
            adminBoardId, jsonInput);
    }
}
