/*
 * 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.vv.ui.ws.rs.application.operation;

import static java.nio.file.Files.copy;
import static java.nio.file.Files.createTempFile;
import static java.nio.file.Files.delete;
import static java.nio.file.Files.newBufferedReader;
import static java.text.MessageFormat.format;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.StandardCopyOption;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.Iterator;

import javax.ejb.EJB;
import javax.ejb.EJBException;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.opencsv.CSVReader;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.products.oscore.logging.core.provider.TransactionInfoProvider;
import com.scytl.products.ov.commons.beans.domain.model.certificate.CertificateEntity;
import com.scytl.products.ov.commons.beans.exceptions.ApplicationException;
import com.scytl.products.ov.commons.beans.exceptions.ApplicationExceptionMessages;
import com.scytl.products.ov.commons.beans.exceptions.DuplicateEntryException;
import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitException;
import com.scytl.products.ov.commons.infrastructure.remote.service.RemoteCertificateService;
import com.scytl.products.ov.commons.infrastructure.transaction.TransactionController;
import com.scytl.products.ov.commons.infrastructure.transaction.TransactionalAction;
import com.scytl.products.ov.commons.infrastructure.transaction.TransactionalActionException;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.ui.Constants;
import com.scytl.products.ov.commons.ui.ws.rs.ErrorCodes;
import com.scytl.products.ov.commons.util.HttpRequestService;
import com.scytl.products.ov.commons.verify.CSVVerifier;
import com.scytl.products.ov.vv.infrastructure.remote.VvRemoteCertificateService;
import com.scytl.products.ov.vv.service.VerificationDerivedKeysService;

@Path(VerificationCardDerivedKeysResource.RESOURCE_PATH)
public class VerificationCardDerivedKeysResource {

    static final String RESOURCE_PATH = "/derivedkeys";

    public static final String ADMINISTRATION_BOARD_CN_PREFIX = "AdministrationBoard ";

    // The name of the resource handle by this web service.
    static final String RESOURCE_NAME = "verificationcardderivedkeys";

    // The name of the query parameter tenantId
    static final String QUERY_PARAMETER_TENANT_ID = "tenantId";

    // The name of the query parameter electionEventId
    static final String QUERY_PARAMETER_ELECTION_EVENT_ID = "electionEventId";

    // The name of the query parameter verificationCardSetId
    static final String QUERY_PARAMETER_VERIFICATION_CARD_SET_ID = "verificationCardSetId";

    static final String QUERY_PARAMETER_ADMIN_BOARD_ID = "adminBoardId";

    static final String SAVE_VERIFICATION_DERIVED_KEYS_PATH =
        "tenant/{tenantId}/electionevent/{electionEventId}/verificationcardset/{verificationCardSetId}/adminboard/{adminBoardId}";

    private static final int BATCH_SIZE = Integer.parseInt(System.getProperty("synchronization.batch.size", "1000"));

    private static final char CSV_VALUE_SEPARATOR = ';';

    @EJB(beanName = "VoteVerificationTransactionController")
    private TransactionController controller;

    @Inject
    @VvRemoteCertificateService
    private RemoteCertificateService remoteCertificateService;

    @Inject
    private VerificationDerivedKeysService verificationDerivedKeysService;

    @Inject
    private TransactionInfoProvider transactionInfoProvider;

    @Inject
    private HttpRequestService httpRequestService;

    @Inject
    private TrackIdInstance trackIdInstance;

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

    private static void validateParameter(final String value, final String parameter) throws ApplicationException {
        if (value == null || value.isEmpty()) {
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_QUERY_PARAMETER_IS_NULL,
                RESOURCE_NAME, ErrorCodes.MISSING_QUERY_PARAMETER, parameter);
        }
    }

    private static void validateParameters(final String tenantId, final String electionEventId,
            final String verificationCardSetId, final String adminBoardId) throws ApplicationException {
        validateParameter(tenantId, QUERY_PARAMETER_TENANT_ID);
        validateParameter(electionEventId, QUERY_PARAMETER_ELECTION_EVENT_ID);
        validateParameter(verificationCardSetId, QUERY_PARAMETER_VERIFICATION_CARD_SET_ID);
        validateParameter(adminBoardId, QUERY_PARAMETER_ADMIN_BOARD_ID);
    }

    /**
     * Saves verification derived keys signed by the admin board
     * (to be used later in proof verification phase)
     * 
     * @param trackingId
     * @param tenantId
     * @param electionEventId
     * @param verificationCardSetId
     * @param adminBoardId
     * @param data csv content with verificationCardId and derivedkeys in each line + signature
     * @param request
     * @return ok if signature valid and saved, precondition failed if signature is invalid
     * @throws ApplicationException
     * @throws IOException
     */
    @POST
    @Path(SAVE_VERIFICATION_DERIVED_KEYS_PATH)
    @Consumes("text/csv")
    public Response saveVerificationDerivedKeys(@HeaderParam(Constants.PARAMETER_HEADER_X_REQUEST_ID) String trackingId,
            @PathParam(QUERY_PARAMETER_TENANT_ID) final String tenantId,
            @PathParam(QUERY_PARAMETER_ELECTION_EVENT_ID) final String electionEventId,
            @PathParam(QUERY_PARAMETER_VERIFICATION_CARD_SET_ID) final String verificationCardSetId,
            @PathParam(QUERY_PARAMETER_ADMIN_BOARD_ID) final String adminBoardId, @NotNull final InputStream data,
            @Context final HttpServletRequest request) throws ApplicationException, IOException {

        trackIdInstance.setTrackId(trackingId);

        Response.Status status;
        LOG.info(
            "Saving verification card derived keys for verification card set id: {}, electionEventId: {}, and tenantId: {}.",
            verificationCardSetId, electionEventId, tenantId);
        validateParameters(tenantId, electionEventId, verificationCardSetId, adminBoardId);
        generateTransactionId(tenantId, request);
        java.nio.file.Path file = createTemporaryFile(data);
        try {
            if (verifyAndRemoveSignature(adminBoardId, file)) {
                saveVerificationCardDerivedKeys(tenantId, electionEventId, file);
                status = Status.OK;
                LOG.info(
                    "Verification card derived keys for verification card set id: {}, electionEventId: {}, and tenantId: {} saved.",
                    verificationCardSetId, electionEventId, tenantId);
            } else {
                status = Status.PRECONDITION_FAILED;
            }
        } finally {
            deleteTemporaryFile(file);
        }
        return Response.status(status).build();
    }

    private java.nio.file.Path createTemporaryFile(final InputStream data) throws IOException {
        java.nio.file.Path file = createTempFile("VerificationCardDerivedKeys", ".csv");
        try {
            copy(data, file, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            deleteTemporaryFile(file);
            throw e;
        }
        return file;
    }

    private void deleteTemporaryFile(final java.nio.file.Path file) {
        try {
            delete(file);
        } catch (IOException e) {
            LOG.warn(format("Failed to delete temporary file ''{0}''.", file), e);
        }
    }

    private void generateTransactionId(final String tenantId, final HttpServletRequest request) {
        transactionInfoProvider.generate(tenantId, httpRequestService.getIpClientAddress(request),
            httpRequestService.getIpServer(request));
    }

    private PublicKey getAdminBoardPublicKey(final String adminBoardId)
            throws GeneralCryptoLibException, RetrofitException {
        LOG.info("Fetching the administration board certificate");
        String name = ADMINISTRATION_BOARD_CN_PREFIX + adminBoardId;
        CertificateEntity entity = remoteCertificateService.getAdminBoardCertificate(name);
        String content = entity.getCertificateContent();
        Certificate certificate = PemUtils.certificateFromPem(content);
        return certificate.getPublicKey();
    }

    private void saveVerificationCardDerivedKeys(final String tenantId, final String electionEventId,
            final java.nio.file.Path file) throws IOException {
        try (Reader reader = newBufferedReader(file, StandardCharsets.UTF_8);
                CSVReader csvReader = new CSVReader(reader, CSV_VALUE_SEPARATOR)) {
            Iterator<String[]> iterator = csvReader.readAll().iterator();
            TransactionalAction<Void> action =
                context -> saveVerificationCardDerivedKeysBatch(tenantId, electionEventId, iterator, BATCH_SIZE);
            while (iterator.hasNext()) {
                controller.doInNewTransaction(action);
            }
        } catch (TransactionalActionException e) {
            throw new EJBException(e);
        }
    }

    private Void saveVerificationCardDerivedKeysBatch(final String tenantId, final String electionEventId,
            final Iterator<String[]> iterator, final int batchSize) throws TransactionalActionException {
        for (int i = 0; i < batchSize && iterator.hasNext(); i++) {
            String[] verificationCardIdAndDerivedKeys = iterator.next();
            String verificationCardId = verificationCardIdAndDerivedKeys[0];
            String choiceCodesDerivedKeysCommitment = verificationCardIdAndDerivedKeys[1];
            String ballotCastKeyDerivedExponentCommitment = verificationCardIdAndDerivedKeys[2];
			try {
                verificationDerivedKeysService.save(tenantId, electionEventId, verificationCardId,
                    choiceCodesDerivedKeysCommitment, ballotCastKeyDerivedExponentCommitment);
			} catch (DuplicateEntryException e) {
				throw new TransactionalActionException(e);
			}
        }
        return null;
    }

    private boolean verifyAndRemoveSignature(final String adminBoardId, final java.nio.file.Path file)
            throws IOException {
        boolean valid = false;
        CSVVerifier verifier = new CSVVerifier();
        try {
            PublicKey key = getAdminBoardPublicKey(adminBoardId);
            valid = verifier.verify(key, file);
        } catch (GeneralCryptoLibException | RetrofitException e) {
            LOG.error("Verification card derived keys signature could not be verified", e);
        }
        return valid;
    }

}
