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

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Base64;
import java.util.Collection;
import java.util.List;

import javax.annotation.Resource;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import javax.inject.Inject;

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

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.domain.model.EntityId;
import com.scytl.products.ov.commons.beans.exceptions.EntryPersistenceException;
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.crypto.PrivateKeyForObjectRepository;
import com.scytl.products.ov.commons.signature.SignatureFactory;
import com.scytl.products.ov.commons.signature.SignatureOutputStream;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.validation.ValidationError;
import com.scytl.products.ov.ei.services.domain.common.csv.ExportedBallotBoxItemWriter;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBox;
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.ballotbox.BallotBoxStatus;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.ExportedBallotBoxItem;
import com.scytl.products.ov.ei.services.infrastructure.log.ElectionInformationLogConstants;
import com.scytl.products.ov.ei.services.infrastructure.log.ElectionInformationLogEvents;

/**
 * A service for handling ballot boxes.
 */
@Stateless
public class BallotBoxServiceImpl implements BallotBoxService {

    private static final int PAGE_SIZE = Integer.parseInt(System.getProperty("BALLOT_BOX_PAGE_SIZE", "7"));

    // The keystore alias
    private static final String KEYSTORE_ALIAS = "privatekey";

    private static final byte[] LINE_SEPARATOR = "\n".getBytes(StandardCharsets.UTF_8);

    @Inject
    private SignatureFactory signatureFactory;

    @Inject
    private BallotBoxRepository ballotBoxRepository;

    @Inject
    private BallotBoxInformationService ballotBoxInformationService;

    @Inject
    private BallotBoxInformationRepository ballotBoxInformationRepository;
    
    @Inject
    private TrackIdInstance trackId;
    
    @Inject
    private SecureLoggingWriter secureLogger;

    private static final Logger LOG = LoggerFactory.getLogger(BallotBoxServiceImpl.class);

    @Inject
    private PrivateKeyForObjectRepository privateKeyRepository;
    
    @Resource
    private SessionContext context;

    /**
     * @see com.scytl.products.ov.ei.services.domain.service.ballotbox.BallotBoxService#checkIfBallotBoxesAreEmpty(java.lang.String,
     *      java.lang.String, java.lang.String)
     */
    @Override
    public com.scytl.products.ov.commons.validation.ValidationResult checkIfBallotBoxesAreEmpty(final String tenantId,
            final String electionEventId, final String ballotBoxId) {
        LOG.info("Validating if all ballot box {} for tenant {} and election event {} is empty.", ballotBoxId,
            tenantId, electionEventId);

        final List<BallotBox> listBallotBox =
            ballotBoxRepository.findByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId, ballotBoxId);
        final com.scytl.products.ov.commons.validation.ValidationResult validationResult =
            new com.scytl.products.ov.commons.validation.ValidationResult();
        validationResult.setResult(listBallotBox.isEmpty());
        final ValidationError validationError = new ValidationError();
        if (validationResult.isResult()) {
            validationError.setValidationErrorType(ValidationErrorType.SUCCESS);
        }
        validationResult.setValidationError(validationError);

        LOG.info("Ballot box {} is empty: {}", ballotBoxId, validationResult.isResult());
        return validationResult;
    }

    @Override
    public void writeEncryptedBallotBox(final OutputStream stream, final String tenantId, final String electionEventId,
            final String ballotBoxId, final boolean test) throws IOException {
        LOG.info("Retrieving encrypted ballot box box {} for tenant {} and election event {}.", ballotBoxId,
            tenantId, electionEventId);
        Signature signature = getSignature(tenantId, electionEventId, ballotBoxId);
        writeEncryptedBallotBoxItems(new SignatureOutputStream(stream, signature), tenantId, electionEventId,
            ballotBoxId);
        writeSignature(stream, signature);
    }

    private Signature getSignature(final String tenantId, final String electionEventId, final String ballotBoxId)
            throws IOException {
        Signature signature = signatureFactory.newSignature();
        try {
            PrivateKey privateKey = privateKeyRepository.findByTenantEEIDObjectIdAlias(tenantId, electionEventId,
                ballotBoxId, KEYSTORE_ALIAS);
            signature.initSign(privateKey);
        } catch (InvalidKeyException | ResourceNotFoundException | CryptographicOperationException e) {
            throw new IOException("Failed to get signature.", e);
        }
        return signature;
    }

    private void writeEncryptedBallotBoxItems(final OutputStream stream, final String tenantId,
            final String electionEventId, final String ballotBoxId) throws IOException {
        try (ExportedBallotBoxItemWriter writer =
            new ExportedBallotBoxItemWriter(new CloseShieldOutputStream(stream))) {
            int first = 1;
            int last = PAGE_SIZE;
            List<ExportedBallotBoxItem> page =
                ballotBoxRepository.getEncryptedVotesByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId,
                    ballotBoxId, first, last);
            while (!page.isEmpty()) {
                for (final ExportedBallotBoxItem item : page) {
                    writer.write(item);
                }
                first += PAGE_SIZE;
                last += PAGE_SIZE;
                page = ballotBoxRepository.getEncryptedVotesByTenantIdElectionEventIdBallotBoxId(tenantId,
                    electionEventId, ballotBoxId, first, last);
            }
        }
    }

    private void writeSignature(final OutputStream stream, final Signature signature) throws IOException {
        byte[] base64Signature;
        try {
            base64Signature = Base64.getEncoder().encode(signature.sign());
        } catch (SignatureException e) {
            throw new IOException("Failed to sign encrypted ballot box.", e);
        }
        stream.write(LINE_SEPARATOR);
        stream.write(base64Signature);
    }

    /**
     * Check if a ballot box is a test ballot box
     * 
     * @param tenantId
     *            - tenant identifier
     * @param electionEventId
     *            - election event identifier
     * @param ballotBoxId
     *            - ballot box identifier
     * @return if the ballot box is a test ballot box or not
     * @throws ResourceNotFoundException
     */
    @Override
    public boolean checkIfTest(String tenantId, String electionEventId, String ballotBoxId)
            throws ResourceNotFoundException {
        return ballotBoxInformationService.isBallotBoxForTest(tenantId, electionEventId, ballotBoxId);
    }
    
    @Override
    public void blockBallotBoxes(String tenantId, String electionEventId,
            Collection<EntityId> ballotBoxIds)
            throws ResourceNotFoundException, EntryPersistenceException {
        for (EntityId id : ballotBoxIds) {
            try {
                ballotBoxInformationService.changeBallotBoxStatus(tenantId,
                    electionEventId, id.getId(), BallotBoxStatus.BLOCKED);
            } catch (ResourceNotFoundException e) {
                context.setRollbackOnly();
                logBallotBoxToBlockNotFound(electionEventId, id, e);
                throw e;
            } catch (EntryPersistenceException e) {
                context.setRollbackOnly();
                logErrorBlockingBallotBox(electionEventId, id, e);
                throw e;
            }
        }
        logBlockBallotBoxOK(electionEventId, ballotBoxIds);
    }
    
    @Override
    public void unblockBallotBoxes(String tenantId, String electionEventId,
            Collection<EntityId> ballotBoxIds)
            throws ResourceNotFoundException, EntryPersistenceException {
        for (EntityId id : ballotBoxIds) {
            try {
                ballotBoxInformationService.changeBallotBoxStatus(tenantId,
                    electionEventId, id.getId(), BallotBoxStatus.NONE);
            } catch (ResourceNotFoundException e) {
                context.setRollbackOnly();
                logBallotBoxToUnblockNotFound(electionEventId, id, e);
                throw e;
            } catch (EntryPersistenceException e) {
                context.setRollbackOnly();
                logErrorUnblockingBallotBox(electionEventId, id, e);
                throw e;
            }
        }
        logUnblockBallotBoxOK(electionEventId, ballotBoxIds);
    }

    /**
     * Check if ballot box is blocked
     *
     * @param tenantId
     *            - tenant identifier
     * @param electionEventId
     *            - election event identifier
     * @param ballotBoxId
     *            - ballot box identifier
     * @throws ResourceNotFoundException
     */
    @Override
    public boolean isBlockedBallotBox(String tenantId, String electionEventId, String ballotBoxId)
            throws ResourceNotFoundException {
        BallotBoxInformation ballotBox = ballotBoxInformationRepository
            .findByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId, ballotBoxId);
        return BallotBoxStatus.BLOCKED.equals(ballotBox.getStatus());
    }

    /**
     * Validates if ballot box is not blocked
     *
     * @param tenantId
     *            - tenant identifier
     * @param electionEventId
     *            - election event identifier
     * @param ballotBoxId
     *            - ballot box identifier
     * @return
     */
    @Override
    public ValidationError validateBallotBoxNotBlocked(String tenantId, String electionEventId, String ballotBoxId)
            throws ResourceNotFoundException {
        final ValidationError result = new ValidationError();
        final BallotBoxInformation ballotBox;
        ballotBox = ballotBoxInformationRepository.findByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId,
            ballotBoxId);

        if (BallotBoxStatus.BLOCKED.equals(ballotBox.getStatus())) {
            result.setValidationErrorType(ValidationErrorType.BLOCKED_BALLOT_BOX);

        } else {
            result.setValidationErrorType(ValidationErrorType.SUCCESS);
        }
        return result;
    }
    
	private void logBallotBoxToBlockNotFound(String electionEventId, EntityId ballotBoxId,
			ResourceNotFoundException e) {
		LOG.warn("Attempted to block a non existing ballot box with id {}", ballotBoxId.getId());
		secureLogger.log(Level.WARN,
				new LogContent.LogContentBuilder().logEvent(ElectionInformationLogEvents.BALLOT_BOX_TO_BLOCK_NOT_FOUND)
						.objectId(ballotBoxId.getId()).electionEvent(electionEventId)
						.additionalInfo(ElectionInformationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
						.additionalInfo(ElectionInformationLogConstants.INFO_ERR_DESC, e.getMessage())
						.additionalInfo(ElectionInformationLogConstants.INFO_BALLOT_BOX_ID, ballotBoxId.getId())
						.createLogInfo());
    }
    
	private void logBallotBoxToUnblockNotFound(String electionEventId, EntityId ballotBoxId,
			ResourceNotFoundException e) {
		LOG.warn("Attempted to unblock a non existing ballot box with id {}", ballotBoxId.getId());
		secureLogger.log(Level.WARN,
				new LogContent.LogContentBuilder()
						.logEvent(ElectionInformationLogEvents.BALLOT_BOX_TO_UNBLOCK_NOT_FOUND)
						.objectId(ballotBoxId.getId()).electionEvent(electionEventId)
						.additionalInfo(ElectionInformationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
						.additionalInfo(ElectionInformationLogConstants.INFO_ERR_DESC, e.getMessage())
						.additionalInfo(ElectionInformationLogConstants.INFO_BALLOT_BOX_ID, ballotBoxId.getId())
						.createLogInfo());
	}

	private void logErrorBlockingBallotBox(String electionEventId, EntityId ballotBoxId, EntryPersistenceException e) {
		LOG.error("Error blocking ballot box with id {}", ballotBoxId.getId(), e);
		secureLogger.log(Level.ERROR,
				new LogContent.LogContentBuilder().logEvent(ElectionInformationLogEvents.ERROR_BLOCKING_BALLOT_BOX)
						.objectId(ballotBoxId.getId()).electionEvent(electionEventId)
						.additionalInfo(ElectionInformationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
						.additionalInfo(ElectionInformationLogConstants.INFO_ERR_DESC, e.getMessage())
						.additionalInfo(ElectionInformationLogConstants.INFO_BALLOT_BOX_ID, ballotBoxId.getId())
						.createLogInfo());
	}

	private void logErrorUnblockingBallotBox(String electionEventId, EntityId ballotBoxId,
			EntryPersistenceException e) {
		LOG.error("Error unblocking ballot box with id {}", ballotBoxId.getId(), e);
        secureLogger.log(Level.ERROR,
            new LogContent.LogContentBuilder().logEvent(ElectionInformationLogEvents.ERROR_UNBLOCKING_BALLOT_BOX)
                .objectId(ballotBoxId.getId()).electionEvent(electionEventId)
                .additionalInfo(ElectionInformationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
                .additionalInfo(ElectionInformationLogConstants.INFO_ERR_DESC, e.getMessage())
                .additionalInfo(ElectionInformationLogConstants.INFO_BALLOT_BOX_ID, ballotBoxId.getId()).createLogInfo());
    }

	private void logBlockBallotBoxOK(String electionEventId, Collection<EntityId> ballotBoxIds) {
		for (EntityId id : ballotBoxIds) {
			secureLogger.log(Level.INFO,
					new LogContent.LogContentBuilder().logEvent(ElectionInformationLogEvents.BLOCK_BALLOT_BOX_OK)
							.objectId(id.getId()).electionEvent(electionEventId)
							.additionalInfo(ElectionInformationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
							.additionalInfo(ElectionInformationLogConstants.INFO_BALLOT_BOX_ID, id.getId())
							.createLogInfo());
		}
	}
    
	private void logUnblockBallotBoxOK(String electionEventId, Collection<EntityId> ballotBoxIds) {
		for (EntityId id : ballotBoxIds) {
			secureLogger.log(Level.INFO,
					new LogContent.LogContentBuilder().logEvent(ElectionInformationLogEvents.UNBLOCK_BALLOT_BOX_OK)
							.objectId(id.getId()).electionEvent(electionEventId)
							.additionalInfo(ElectionInformationLogConstants.INFO_TRACK_ID, trackId.getTrackId())
							.additionalInfo(ElectionInformationLogConstants.INFO_BALLOT_BOX_ID, id.getId())
							.createLogInfo());
		}
	}
}
