/*
 * 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.ei.services.domain.service.election;

import org.apache.commons.io.output.CloseShieldOutputStream;
import org.bouncycastle.cms.CMSSignedDataStreamGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.json.JsonObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
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.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.validation.CryptographicOperationException;
import com.scytl.products.ov.commons.beans.validation.ValidationErrorType;
import com.scytl.products.ov.commons.cms.CMSService;
import com.scytl.products.ov.commons.crypto.PrivateKeyForObjectRepository;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.commons.validation.ValidationError;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBoxContentRepository;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBoxInformation;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBoxInformationRepository;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBoxRepository;
import com.scytl.products.ov.ei.services.domain.model.castcode.VoteCastCodeRepository;
import com.scytl.products.ov.ei.services.domain.model.validation.ElectionValidationRequest;
import com.scytl.products.ov.ei.services.domain.model.validation.VoteValidationRepository;
import com.scytl.products.ov.ei.services.domain.model.votingcard.VotingCardWriter;
import com.scytl.products.ov.ei.services.infrastructure.log.ElectionInformationLogConstants;
import com.scytl.products.ov.ei.services.infrastructure.log.ElectionInformationLogEvents;

/**
 * Handles operations on election.
 */
@Stateless
public class ElectionServiceImpl implements ElectionService {
    private static final String JSON_PARAMETER_DATE_FROM = "startDate";

    private static final String JSON_PARAMETER_DATE_TO = "endDate";

    private static final String GRACE_PERIOD = "gracePeriod";

    private static final String KEYSTORE_PK_ALIAS = "privatekey";

    private static final String BALLOT_BOX_CERT_FIELD = "ballotBoxCert";

    @Inject
    private CMSService cmsService;

    @Inject
    private BallotBoxInformationRepository ballotBoxInformationRepository;

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

    @Inject
    private PrivateKeyForObjectRepository privateKeyRepository;

    @EJB
    private VoteCastCodeRepository voteCastCodeRepository;
    
    @EJB
    private VoteValidationRepository voteValidationRepository;

    @EJB
    private BallotBoxContentRepository ballotBoxContentRepository;

    @Inject
    private SecureLoggingWriter secureLoggerWriter;

    @Inject
    private TrackIdInstance trackIdInstance;

    @Inject
    private BallotBoxRepository ballotBoxRepository;

    /**
     * Validates if the election is open
     *
     * @param request
     *            a request object including the ids and whether the grace
     *            period has to be applied or not
     * @return
     */
    @Override
    public ValidationError validateIfElectionIsOpen(final ElectionValidationRequest request) {
        ValidationError result = new ValidationError();
        BallotBoxInformation info = null;
        try {
            info = ballotBoxInformationRepository.findByTenantIdElectionEventIdBallotBoxId(request.getTenantId(),
                request.getElectionEventId(), request.getBallotBoxId());
        } catch (ResourceNotFoundException e) {
            LOG.error("Failed to validate is election is open.", e);
            return result;
        }

        result.setValidationErrorType(ValidationErrorType.SUCCESS);
        JsonObject json = JsonUtils.getJsonObject(info.getJson());
        String dateFromString = json.getString(JSON_PARAMETER_DATE_FROM);
        String dateToString = json.getString(JSON_PARAMETER_DATE_TO);

        ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
        ZonedDateTime dateTo =
            ZonedDateTime.parse(dateToString, DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneOffset.UTC));
        ZonedDateTime dateFrom =
            ZonedDateTime.parse(dateFromString, DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneOffset.UTC));
        DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC);
        int gracePeriodInSeconds = Integer.parseInt(json.getString(GRACE_PERIOD));
        boolean isElectionOverDate =
            request.isValidatedWithGracePeriod() ? isElectionOverDateWithGracePeriod(now, dateTo, gracePeriodInSeconds)
                    : isElectionOverDate(now, dateTo);
        if (isElectionOverDate) {
            result.setValidationErrorType(ValidationErrorType.ELECTION_OVER_DATE);
            String dateError = request.isValidatedWithGracePeriod()
                    ? dateTo.plusSeconds(gracePeriodInSeconds).format(formatter) : dateTo.format(formatter);
            String[] errorArgs = {dateError };
            result.setErrorArgs(errorArgs);

            secureLoggerWriter.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(ElectionInformationLogEvents.ELECTION_EVENT_ALREADY_CLOSED)
                    .objectId(request.getBallotBoxId()).electionEvent(request.getElectionEventId())
                    .additionalInfo(ElectionInformationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .createLogInfo());
        } else if (hasNotElectionStarted(now, dateFrom)) {
            result.setValidationErrorType(ValidationErrorType.ELECTION_NOT_STARTED);
            String[] errorArgs = {dateFrom.format(formatter) };
            result.setErrorArgs(errorArgs);

            secureLoggerWriter.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(ElectionInformationLogEvents.ELECTION_EVENT_NOT_STARTED)
                    .objectId(request.getBallotBoxId()).electionEvent(request.getElectionEventId())
                    .additionalInfo(ElectionInformationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .createLogInfo());
        }
        return result;
    }

    @Override
    public void writeCastVotes(final String tenantId, final String electionEventId, final String filenamePrefix,
            final OutputStream stream) throws IOException {
        try (ZipOutputStream zip = new ZipOutputStream(new CloseShieldOutputStream(stream), StandardCharsets.UTF_8)) {
            zip.putNextEntry(new ZipEntry(filenamePrefix + ".csv"));
            ByteArrayOutputStream signature = new ByteArrayOutputStream();
            try {
                CMSSignedDataStreamGenerator generator = newCMSSignedDataStreamGenerator(tenantId, electionEventId);
                try ( VotingCardWriter writer =
                    new VotingCardWriter(generator.open(signature, false, new CloseShieldOutputStream(zip)))) {
                    voteCastCodeRepository.findAndWriteCastVotingCards(tenantId, electionEventId, writer);
                }
            } finally {
                signature.close();
            }
            zip.putNextEntry(new ZipEntry(filenamePrefix + ".p7"));
            zip.write(signature.toByteArray());
        }
    }

    @Override
    public void writeVerifiedVotes(final String tenantId, final String electionEventId, final String filenamePrefix,
            final OutputStream stream) throws IOException {
        try (ZipOutputStream zip = new ZipOutputStream(new CloseShieldOutputStream(stream), StandardCharsets.UTF_8)) {
            zip.putNextEntry(new ZipEntry(filenamePrefix + ".csv"));
            ByteArrayOutputStream signature = new ByteArrayOutputStream();
            try {
                CMSSignedDataStreamGenerator generator = newCMSSignedDataStreamGenerator(tenantId, electionEventId);
                try ( VotingCardWriter writer =
                        new VotingCardWriter(generator.open(signature, false, new CloseShieldOutputStream(zip)))) {
                    ballotBoxRepository.findAndWriteUsedVotingCards(tenantId, electionEventId, writer);
                 }
            } finally {
                signature.close();
            }
            zip.putNextEntry(new ZipEntry(filenamePrefix + ".p7"));
            zip.write(signature.toByteArray());
        }
    }

    private CMSSignedDataStreamGenerator newCMSSignedDataStreamGenerator(final String tenantId,
            final String electionEventId) throws IOException {
        CMSSignedDataStreamGenerator generator;
        try {
            String ballotBoxId = ballotBoxContentRepository.findFirstBallotBoxForElection(tenantId, electionEventId);
            PrivateKey key = privateKeyRepository.findByTenantEEIDObjectIdAlias(tenantId, electionEventId, ballotBoxId,
                KEYSTORE_PK_ALIAS);
            BallotBoxInformation info = ballotBoxInformationRepository
                .findByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId, ballotBoxId);
            JsonObject object = JsonUtils.getJsonObject(info.getJson());
            String pem = object.getString(BALLOT_BOX_CERT_FIELD);
            X509Certificate certificate;
            certificate = (X509Certificate) PemUtils.certificateFromPem(pem);
            generator = cmsService.newCMSSignedDataStreamGenerator(key, certificate);
        } catch (ResourceNotFoundException | CryptographicOperationException | GeneralCryptoLibException
                | GeneralSecurityException e) {
            throw new IOException("Failed to create PKCS#7 signature generator.", e);
        }
        return generator;
    }

    private boolean isElectionOverDate(final ZonedDateTime now, final ZonedDateTime dateTo) {
        return now.isAfter(dateTo);
    }

    private boolean isElectionOverDateWithGracePeriod(final ZonedDateTime now, final ZonedDateTime dateTo,
            final int gracePeriodInSeconds) {
        return now.isAfter(dateTo.plusSeconds(gracePeriodInSeconds));
    }

    private boolean hasNotElectionStarted(final ZonedDateTime now, final ZonedDateTime dateFrom) {
        return now.isBefore(dateFrom);
    }
}
