/*
 * 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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ejb.EJB;
import javax.ejb.Stateless;
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.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.security.PublicKey;
import java.security.cert.Certificate;

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.VoteVerificationContextData;
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.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.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.util.HttpRequestService;
import com.scytl.products.ov.commons.verify.JSONVerifier;
import com.scytl.products.ov.vv.domain.common.SignedObject;
import com.scytl.products.ov.vv.domain.model.content.VerificationContentEntity;
import com.scytl.products.ov.vv.domain.model.content.VerificationContentRepository;
import com.scytl.products.ov.vv.domain.model.verificationset.VerificationCardSetData;
import com.scytl.products.ov.vv.domain.model.verificationset.VerificationSetEntity;
import com.scytl.products.ov.vv.domain.model.verificationset.VerificationSetRepository;
import com.scytl.products.ov.vv.infrastructure.remote.VvRemoteCertificateService;

/**
 * Web service for handling ballot data resource.
 */
@Path(VerificationCardSetDataResource.RESOURCE_PATH)
@Stateless
public class VerificationCardSetDataResource {

    public static final String ADMINISTRATION_BOARD_CN_PREFIX = "AdministrationBoard ";
    
    static final String RESOURCE_PATH = "/verificationcardsetdata";
    
    static final String SAVE_VERIFICATION_CARD_SET_DATA_PATH = "/tenant/{tenantId}/electionevent/{electionEventId}/verificationcardset/{verificationCardSetId}/adminboard/{adminBoardId}";

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

    // 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";

    // An instance of the verification card set repository
    @EJB
    private VerificationSetRepository verificationSetRepository;

    // An instance of the verification content repository
    @EJB
    private VerificationContentRepository verificationContentRepository;

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

    @Inject
    private TransactionInfoProvider transactionInfoProvider;

    @Inject
    private HttpRequestService httpRequestService;

    @Inject
    @VvRemoteCertificateService
    private RemoteCertificateService remoteCertificateService;

    // The track id instance
    @Inject
    private TrackIdInstance trackIdInstance;

    /**
     * Save data for a verification card set given the tenant, the election event id and the verification card set
     * identifier.
     *
     * @param tenantId              - the tenant identifier.
     * @param electionEventId       - the election event identifier.
     * @param verificationCardSetId - the verification card set identifier.
     * @param request               - the http servlet request.
     * @return Returns status 200 on success.
     * @throws ApplicationException if the input parameters are not valid.
     */
    @POST
    @Path(SAVE_VERIFICATION_CARD_SET_DATA_PATH)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response saveVerificationCardSetData(@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 VerificationCardSetData verificationCardSetData,
                                   @Context final HttpServletRequest request)
        throws ApplicationException, IOException {

        trackIdInstance.setTrackId(trackingId);

        // transaction id generation
        transactionInfoProvider.generate(tenantId, httpRequestService.getIpClientAddress(request),
            httpRequestService.getIpServer(request));

        LOG.info("Saving verification card set data for verificationCardSetId: {}, electionEventId: {}, and tenantId: {}.",
            verificationCardSetId, electionEventId, tenantId);

        // validate parameters
        validateParameters(tenantId, electionEventId, verificationCardSetId);

        LOG.info("Fetching the administration board certificate");
        final String adminBoardCommonName = ADMINISTRATION_BOARD_CN_PREFIX + adminBoardId;

        Certificate adminBoardCert;
        try {
	        final CertificateEntity adminBoardCertificateEntity =
	            remoteCertificateService.getAdminBoardCertificate(adminBoardCommonName);
	        final String adminBoardCertPEM = adminBoardCertificateEntity.getCertificateContent();
            adminBoardCert = PemUtils.certificateFromPem(adminBoardCertPEM);
        } catch (final GeneralCryptoLibException | RetrofitException e) {
            LOG.error("An error occurred while fetching the administration board certificate", e);
            return Response.status(Response.Status.PRECONDITION_FAILED).build();
        }
        final PublicKey adminBoardPublicKey = adminBoardCert.getPublicKey();

        // Build verification card set data
        final VerificationSetEntity verificationcardSet = new VerificationSetEntity();
        verificationcardSet.setElectionEventId(electionEventId);
        verificationcardSet.setTenantId(tenantId);
        verificationcardSet.setVerificationCardSetId(verificationCardSetId);

        final String signedVerificationCardSetData = verificationCardSetData.getVerificationCardSetData();
        final SignedObject signedVerificationCardSetDataObject =
            ObjectMappers.fromJson(signedVerificationCardSetData, SignedObject.class);
        final String signatureVerificationCardSetData = signedVerificationCardSetDataObject.getSignature();

        final JSONVerifier verifier = new JSONVerifier();
        com.scytl.products.ov.commons.beans.VerificationCardSetData verifiedVerificationCardSetData;

        try {
            LOG.info("Verifying verification card set data signature");
            verifiedVerificationCardSetData = verifier.verify(adminBoardPublicKey, signatureVerificationCardSetData,
                com.scytl.products.ov.commons.beans.VerificationCardSetData.class);
            LOG.info("Verification card set data signature was successfully verified");
        } catch (final Exception e) {
            LOG.error("Verification card set data signature could not be verified", e);
            return Response.status(Response.Status.PRECONDITION_FAILED).build();
        }
        final String verificationCardSetDataJSON = ObjectMappers.toJson(verifiedVerificationCardSetData);
        verificationcardSet.setJSON(verificationCardSetDataJSON);
        verificationcardSet.setSignature(signatureVerificationCardSetData);

        // Save verification card set data
        try {
            verificationSetRepository.save(verificationcardSet);

            // Build verification content data
            final VerificationContentEntity verificationContent = new VerificationContentEntity();
            verificationContent.setElectionEventId(electionEventId);
            verificationContent.setTenantId(tenantId);
            verificationContent.setVerificationCardSetId(verificationCardSetId);

            final String signedVoteVerificationContextData = verificationCardSetData.getVoteVerificationContextData();
            final SignedObject signedVoteVerificationContextDataObject =
                ObjectMappers.fromJson(signedVoteVerificationContextData, SignedObject.class);
            final String signatureVoteVerificationContextData = signedVoteVerificationContextDataObject.getSignature();
            VoteVerificationContextData voteVerificationContextData;
            try {
                LOG.info("Verifying vote verification context data signature");
                voteVerificationContextData = verifier.verify(adminBoardPublicKey, signatureVoteVerificationContextData,
                    VoteVerificationContextData.class);
                LOG.info("Vote verification context data signature was successfully verified");
            } catch (final Exception e) {
                LOG.error("Vote verification context data signature could not be verified", e);
                return Response.status(Response.Status.PRECONDITION_FAILED).build();
            }
            final String voteVerificationContextDataJSON = ObjectMappers.toJson(voteVerificationContextData);
            verificationContent.setJson(voteVerificationContextDataJSON);

            // Save verification content
            verificationContentRepository.save(verificationContent);

            LOG.info("Verification card set data with verificationCardSetId: {}, electionEventId: {}, and tenantId: {} saved.",
                verificationCardSetId, electionEventId, tenantId);
        } catch (final DuplicateEntryException ex) {
            LOG.warn("Duplicate entry tried to be inserted for vertification card set: {}, electionEventId: {}, and tenantId: {}.",
                verificationCardSetId, electionEventId, tenantId, ex);
        }

        // return on success
        return Response.ok().build();
    }

    // Validate parameters.
    private void validateParameters(final String tenantId, final String electionEventId, final String verificationCardSetId)
        throws ApplicationException {
        if (tenantId == null || tenantId.isEmpty())
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_QUERY_PARAMETER_IS_NULL,
                RESOURCE_NAME, ErrorCodes.MISSING_QUERY_PARAMETER, QUERY_PARAMETER_TENANT_ID);

        if (electionEventId == null || electionEventId.isEmpty())
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_QUERY_PARAMETER_IS_NULL,
                RESOURCE_NAME, ErrorCodes.MISSING_QUERY_PARAMETER, QUERY_PARAMETER_ELECTION_EVENT_ID);

        if (verificationCardSetId == null || verificationCardSetId.isEmpty())
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_QUERY_PARAMETER_IS_NULL,
                RESOURCE_NAME, ErrorCodes.MISSING_QUERY_PARAMETER, QUERY_PARAMETER_VERIFICATION_CARD_SET_ID);
    }
}
