/*
 * 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.webapp.mvc;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.IOUtils;
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 com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.certificates.utils.LdapHelper;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
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.Ballot;
import com.scytl.products.ov.commons.beans.Contest;
import com.scytl.products.ov.constants.Constants;
import com.scytl.products.ov.tallying.command.TallyingHolder;
import com.scytl.products.ov.tallying.commons.MappingStrategy;
import com.scytl.products.ov.tallying.commons.beans.AuditableVote;
import com.scytl.products.ov.tallying.commons.beans.FailedVote;
import com.scytl.products.ov.tallying.commons.exception.TallyingException;
import com.scytl.products.ov.tallying.commons.io.reader.AuditableVotesReader;
import com.scytl.products.ov.tallying.commons.io.reader.BallotsReader;
import com.scytl.products.ov.tallying.commons.io.reader.FailedVotesReader;
import com.scytl.products.ov.tallying.commons.log.TallyingLogConstants;
import com.scytl.products.ov.tallying.commons.log.TallyingLogEvents;
import com.scytl.products.ov.tallying.path.PathResolver;
import com.scytl.products.ov.tallying.webapp.mvc.dto.input.TallyingWebappInput;

public class TallyingWebappAdapter {

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

    private final BallotsReader ballotsReader;

    private final AuditableVotesReader auditableVotesReader;

    private final FailedVotesReader failedVotesReader;

    private final MappingStrategy mappingStrategy;

    @Value("${failedVotes.filename}")
    private String failedVotesFilename;

    @Value("${auditableVotes.filename}")
    private String auditableVotesFilename;

    @Value("${invalidVotes.filename}")
    private String invalidVotesFilename;

    @Value("${decryptedBallots.filename}")
    private String decryptedBallotsFilename;

    @Value("${working.directory}")
    private String configFilesBaseDir;

    @Value("${output.folder}")
    private String outputFolder;

    @Value("${decryptedBallots.signature.cert.directory}")
    private String signatureCertDir;

    @Autowired
    private SecureLoggingWriter SECURE_LOG;

    @Autowired
    private PathResolver pathResolver;

    public TallyingWebappAdapter(BallotsReader ballotsReader, AuditableVotesReader auditableVotesReader,
            FailedVotesReader failedVotesReader, MappingStrategy mappingStrategy) {
        this.ballotsReader = ballotsReader;
        this.auditableVotesReader = auditableVotesReader;
        this.failedVotesReader = failedVotesReader;
        this.mappingStrategy = mappingStrategy;
    }

    public TallyingHolder adapt(final TallyingWebappInput input) throws TallyingException {

        LOG.info("Adapting input...");

        Ballot ballot = input.getBallot();

        Path workingDirectory = pathResolver.resolve(configFilesBaseDir).resolve(ballot.getElectionEvent().getId())
            .resolve(Constants.ONLINE_DIRECTORY).resolve(Constants.ELECTION_INFORMATION_DIRECTORY)
            .resolve(Constants.BALLOTS_DIRECTORY).resolve(ballot.getId()).resolve(Constants.BALLOT_BOXES_DIRECTORY)
            .resolve(input.getBallotBoxID());

        String failedVotesPath = workingDirectory.resolve(failedVotesFilename).toString();
        File failedVotesFile = validateInputFile(ballot, failedVotesPath, failedVotesFilename,
            TallyingLogEvents.FAILED_VOTES_FILE_NOT_FOUND, input.getBallotBoxID());

        String auditableVotesPath = workingDirectory.resolve(auditableVotesFilename).toString();
        File auditableVotesFile = validateInputFile(ballot, auditableVotesPath, auditableVotesFilename,
            TallyingLogEvents.AUDITABLE_VOTES_FILE_NOT_FOUND, input.getBallotBoxID());

        String decryptedBallotsPath = workingDirectory.resolve(decryptedBallotsFilename).toString();
        File decryptedOutputFile = validateInputFile(ballot, decryptedBallotsPath, decryptedBallotsFilename,
            TallyingLogEvents.DECRYPTION_VOTES_FILE_NOT_FOUND, input.getBallotBoxID());

        Path outputDir = workingDirectory.resolve(outputFolder);

        List<List<String>> decryptedValues = ballotsReader.read(decryptedOutputFile.toPath(),
            ballot.getElectionEvent().getId(), input.getBallotBoxID());

        List<AuditableVote> auditableValues = auditableVotesReader.read(auditableVotesFile.toPath(),
            ballot.getElectionEvent().getId(), input.getBallotBoxID());

        List<FailedVote> failedValues = failedVotesReader.read(failedVotesFile.toPath(),
            ballot.getElectionEvent().getId(), input.getBallotBoxID());

        Map<String, Contest> optionContestMappings = mappingStrategy.mapBallot(ballot);

        X509Certificate signatureCertificate = recoverSignatureCertificate(input);

        PrivateKey signaturePrivateKey;

        try {
            signaturePrivateKey = PemUtils.privateKeyFromPem(input.getPrivateKeyPEM());
        } catch (GeneralCryptoLibException e) {
            throw new TallyingException("Extracting signature private key from PEM failed", e);
        }

        TallyingHolder holder = new TallyingHolder();
        holder.setElectionEventId(ballot.getElectionEvent().getId());
        holder.setFailedVotesPath(failedVotesFile.toPath());
        holder.setAuditableVotesPath(auditableVotesFile.toPath());
        holder.setDecryptedBallotsPath(decryptedOutputFile.toPath());
        holder.setDecryptedValues(decryptedValues);
        holder.setAuditableValues(auditableValues);
        holder.setFailedValues(failedValues);
        holder.setInvalidVotesFileName(invalidVotesFilename);
        holder.setOptionContestMappings(optionContestMappings);
        holder.setOutputDir(outputDir);
        holder.setSignatureCert(signatureCertificate);
        holder.setSignatureKey(signaturePrivateKey);
        holder.setTenantId(input.getTenantID());
        holder.setBallotBoxId(input.getBallotBoxID());
        holder.setWriteInSeparator(input.getWriteInSeparator());
        holder.setContests(ballot.getContests());

        LOG.info("Input adopted for BallotBoxId " + input.getBallotBoxID());

        return holder;
    }

    private X509Certificate recoverSignatureCertificate(final TallyingWebappInput input) throws TallyingException {

        String certificateId = input.getCertId();

        Path signatureCertificatePath =
            pathResolver.resolve(configFilesBaseDir).resolve(signatureCertDir).resolve(certificateId + ".pem");

        LOG.info("SignatureCertificatePath: " + signatureCertificatePath);
        String content;

        try (FileInputStream fisTargetFile = new FileInputStream(signatureCertificatePath.toFile())) {
            content = IOUtils.toString(fisTargetFile, StandardCharsets.UTF_8);
            X509Certificate signatureCertificate = (X509Certificate) PemUtils.certificateFromPem(content);

            LdapHelper ldapHelper = new LdapHelper();
            String certCN =
                ldapHelper.getAttributeFromDistinguishedName(signatureCertificate.getSubjectDN().toString(), "CN");

            SECURE_LOG.log(Level.INFO,
                new LogContent.LogContentBuilder()
                    .logEvent(TallyingLogEvents.SIGNATURE_CERTIFICATE_RECOVERED_SUCCESSFULLY)
                    .objectId(input.getBallotBoxID()).user("adminID")
                    .electionEvent(input.getBallot().getElectionEvent().getId())
                    .additionalInfo(TallyingLogConstants.CERT_COMMON_NAME, certCN)
                    .additionalInfo(TallyingLogConstants.CERT_SERIAL_NUMBER,
                        String.valueOf(signatureCertificate.getSerialNumber()))
                    .createLogInfo());
            return signatureCertificate;
        } catch (IOException | GeneralCryptoLibException e) {
            LogContent.LogContentBuilder logContent = new LogContent.LogContentBuilder()
                .logEvent(TallyingLogEvents.SIGNATURE_CERTIFICATE_RECOVERING_FAILED).objectId(input.getBallotBoxID())
                .user("adminID").electionEvent(input.getBallot().getElectionEvent().getId());

            SECURE_LOG.log(Level.ERROR, logContent.additionalInfo(TallyingLogConstants.CERT_ID, input.getCertId())
                .additionalInfo(TallyingLogConstants.ERR_DESC, ExceptionUtils.getRootCauseMessage(e)).createLogInfo());
            throw new TallyingException(TallyingLogEvents.SIGNATURE_CERTIFICATE_RECOVERING_FAILED.getInfo(), e);
        }
    }

    private File validateInputFile(Ballot ballot, String path, String fileName, TallyingLogEvents fileNotFoundLogEvent,
            String ballotBoxId) throws TallyingException {

        File file = new File(path);
        if (!file.exists()) {
            SECURE_LOG.log(Level.ERROR, new LogContent.LogContentBuilder().logEvent(fileNotFoundLogEvent)
                .objectId(ballotBoxId).user("adminID").electionEvent(ballot.getElectionEvent().getId())
                .additionalInfo(TallyingLogConstants.FILE_NAME, fileName)
                .additionalInfo(TallyingLogConstants.ERR_DESC, "File:  " + path + " does not exists.").createLogInfo());
            throw new TallyingException("File:  " + path + " does not exist.");
        }
        return file;
    }
}
