/*
 * 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.channel.cc.orchestrator.ui.ws.rs.operation;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.List;

import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.json.Json;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.StreamingOutput;

import org.slf4j.Logger;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncryptionParameters;
import com.scytl.products.oscore.logging.core.provider.TransactionInfoProvider;
import com.scytl.products.ov.channel.cc.orchestrator.choicecodes.domain.model.computedvalues.ChoiceCodesComputationStatus;
import com.scytl.products.ov.channel.cc.orchestrator.choicecodes.domain.services.ChoiceCodesDecryptionContributionsService;
import com.scytl.products.ov.channel.cc.orchestrator.choicecodes.domain.services.ChoiceCodesGenerationContributionsService;
import com.scytl.products.ov.channel.cc.orchestrator.choicecodes.domain.services.ChoiceCodesKeyGenerationService;
import com.scytl.products.ov.channel.cc.orchestrator.choicecodes.domain.services.ChoiceCodesKeysResponse;
import com.scytl.products.ov.channel.cc.orchestrator.choicecodes.domain.services.ChoiceCodesVerificationContributionsService;
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.beans.exceptions.EntryPersistenceException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.key.generation.request.KeyGenerationRequestParameters;
import com.scytl.products.ov.commons.dto.ChoiceCodeGenerationReqPayload;
import com.scytl.products.ov.commons.dto.ChoiceCodesVerificationDecryptResPayload;
import com.scytl.products.ov.commons.dto.ChoiceCodesVerificationResPayload;
import com.scytl.products.ov.commons.dto.PartialCodesInput;
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;

@Path(ChoiceCodesResource.RESOURCE_PATH)
@Stateless(name = "or-ChoiceCodesResource")
public class ChoiceCodesResource {

    /* Base path to resource */
    static final String RESOURCE_PATH = "choicecodes";

    static final String PATH_COMPUTE_VOTING_CONTRIBUTIONS =
        "tenant/{tenantId}/electionevent/{electionEventId}/verificationCardSetId/{verificationCardSetId}/verificationCardId/{verificationCardId}/computeVotingContributions";

    static final String PATH_COMPUTE_GENERATION_CONTRIBUTIONS_REQUEST = "computeGenerationContributions";

    static final String PATH_COMPUTE_GENERATION_CONTRIBUTIONS_RETRIEVAL =
        "tenant/{tenantId}/electionevent/{electionEventId}/verificationCardSetId/{verificationCardSetId}/chunkId/{chunkId}/computeGenerationContributions";

    static final String PATH_COMPUTE_GENERATION_CONTRIBUTIONS_STATUS =
        "tenant/{tenantId}/electionevent/{electionEventId}/verificationCardSetId/{verificationCardSetId}/generationContributions/status";

    static final String PATH_DECRYPT_CONTRIBUTIONS =
        "tenant/{tenantId}/electionevent/{electionEventId}/verificationCardSetId/{verificationCardSetId}/verificationCardId/{verificationCardId}/decryptContributions";

    static final String PATH_KEYS = "tenant/{tenantId}/electionevent/{electionEventId}/keys";

    static final String PATH_PARAMETER_TENANT_ID = "tenantId";

    static final String PATH_PARAMETER_ELECTION_EVENT_ID = "electionEventId";

    static final String PATH_PARAMETER_VERIFICATION_CARD_ID = "verificationCardId";

    static final String PATH_PARAMETER_VERIFICATION_CARD_SET_ID = "verificationCardSetId";

    static final String PATH_PARAMETER_CHUNK_ID = "chunkId";

    static final String QUERY_PARAMETER_CHUNK_COUNT = "chunkCount";

    @Inject
    private Logger LOG;

    @Inject
    private TrackIdInstance trackIdInstance;

    @Inject
    private TransactionInfoProvider transactionInfoProvider;

    @Inject
    private HttpRequestService httpRequestService;

    @Inject
    private ChoiceCodesDecryptionContributionsService choiceCodesDecryptionContributionsService;

    @Inject
    private ChoiceCodesVerificationContributionsService choiceCodesVerificationContributionsService;

    @Inject
    private ChoiceCodesGenerationContributionsService choiceCodesGenerationContributionsService;

    @Inject
    private ChoiceCodesKeyGenerationService choiceCodesKeyGenerationService;

    @POST
    @Path(PATH_COMPUTE_VOTING_CONTRIBUTIONS)
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    public Response getChoiceCodeNodesComputeForVotingContributions(
            @HeaderParam(Constants.PARAMETER_HEADER_X_REQUEST_ID) String trackingId,
            @PathParam(PATH_PARAMETER_TENANT_ID) final String tenantId,
            @PathParam(PATH_PARAMETER_ELECTION_EVENT_ID) final String electionEventId,
            @PathParam(PATH_PARAMETER_VERIFICATION_CARD_SET_ID) final String verificationCardSetId,
            @PathParam(PATH_PARAMETER_VERIFICATION_CARD_ID) final String verificationCardId,
            @NotNull PartialCodesInput partialCodes, @Context final HttpServletRequest request)
            throws ApplicationException, IOException, ResourceNotFoundException {

        trackIdInstance.setTrackId(trackingId);

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

        validateParameters(tenantId, electionEventId, verificationCardId);

        LOG.info(
            "Requesting collection of the choice codes voting phase compute contributions for tenant {} electionEventId {} "
                + "verificationCardSetId {} and verificationCardId {}",
            tenantId, electionEventId, verificationCardSetId, verificationCardId);

        List<ChoiceCodesVerificationResPayload> result = choiceCodesVerificationContributionsService.request(trackingId,
            tenantId, electionEventId, verificationCardSetId, verificationCardId, partialCodes);

        StreamingOutput entity = stream -> toOutputStream(result, stream);

        return Response.ok().entity(entity).build();
    }

    private void toOutputStream(List<ChoiceCodesVerificationResPayload> body, OutputStream outputStream)
            throws IOException {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(body);
        objectOutputStream.flush();
    }

    @POST
    @Path(PATH_COMPUTE_GENERATION_CONTRIBUTIONS_REQUEST)
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response getChoiceCodeNodesComputeForGenerationContributions(
            @HeaderParam(Constants.PARAMETER_HEADER_X_REQUEST_ID) String trackingId,
            @NotNull ChoiceCodeGenerationReqPayload payload, @Context final HttpServletRequest request)
            throws ApplicationException, DuplicateEntryException, IOException, ResourceNotFoundException {

        trackIdInstance.setTrackId(trackingId);

        String tenantId = payload.getTenantId();
        String electionEventId = payload.getElectionEventId();
        String verificationCardSetId = payload.getVerificationCardSetId();
        int chunkId = payload.getChunkId();

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

        validateParameters(tenantId, electionEventId, verificationCardSetId);

        LOG.info(
            "OR:{} - Requesting collection of the choice codes generation phase compute contributions for tenant {}"
                + " electionEventId {} verificationCardSetId {} and chunkId {}",
            trackingId, tenantId, electionEventId, verificationCardSetId, chunkId);

        choiceCodesGenerationContributionsService.request(trackingId, payload);

        return Response.ok().build();
    }

    @GET
    @Path(PATH_COMPUTE_GENERATION_CONTRIBUTIONS_RETRIEVAL)
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    public Response getChoiceCodeNodesComputeForGenerationContributions(
            @HeaderParam(Constants.PARAMETER_HEADER_X_REQUEST_ID) String trackingId,
            @PathParam(PATH_PARAMETER_TENANT_ID) final String tenantId,
            @PathParam(PATH_PARAMETER_ELECTION_EVENT_ID) final String electionEventId,
            @PathParam(PATH_PARAMETER_VERIFICATION_CARD_SET_ID) final String verificationCardSetId,
            @PathParam(PATH_PARAMETER_CHUNK_ID) final int chunkId, @Context final HttpServletRequest request)
            throws ApplicationException, DuplicateEntryException, IOException, ResourceNotFoundException {

        trackIdInstance.setTrackId(trackingId);

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

        validateParameters(tenantId, electionEventId, verificationCardSetId);

        LOG.info(
            "Retrieving collection of the choice codes generation phase compute contributions for tenant {} electionEventId {} "
                + "and verificationCardSetId {}",
            tenantId, electionEventId, verificationCardSetId);

        ChoiceCodesComputationStatus contributionsStatus = choiceCodesGenerationContributionsService
            .getComputedValuesStatus(electionEventId, tenantId, verificationCardSetId, chunkId);

        if (ChoiceCodesComputationStatus.COMPUTED.equals(contributionsStatus)) {

            StreamingOutput entity = stream -> {
                try {
                    choiceCodesGenerationContributionsService.writeToStream(stream, tenantId, electionEventId,
                        verificationCardSetId, chunkId);
                } catch (ResourceNotFoundException e) {
                    throw new WebApplicationException(e, Status.NOT_FOUND);
                }
            };

            return Response.ok().entity(entity).header("Content-Disposition", "attachment;").build();
        } else {
            return Response.status(Status.NOT_FOUND).build();
        }
    }

    @GET
    @Path(PATH_COMPUTE_GENERATION_CONTRIBUTIONS_STATUS)
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response getChoiceCodesGenerationComputationStatus(
            @HeaderParam(Constants.PARAMETER_HEADER_X_REQUEST_ID) String trackingId,
            @PathParam(PATH_PARAMETER_TENANT_ID) final String tenantId,
            @PathParam(PATH_PARAMETER_ELECTION_EVENT_ID) final String electionEventId,
            @PathParam(PATH_PARAMETER_VERIFICATION_CARD_SET_ID) final String verificationCardSetId,
            @QueryParam(QUERY_PARAMETER_CHUNK_COUNT) final int chunkCount, @Context final HttpServletRequest request)
            throws ApplicationException, DuplicateEntryException, IOException, ResourceNotFoundException {

        trackIdInstance.setTrackId(trackingId);

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

        validateParameters(tenantId, electionEventId, verificationCardSetId);

        LOG.info(
            "Checking status of the choice codes generation phase compute contributions for tenant {} electionEventId {} "
                + "and verificationCardSetId {}",
            tenantId, electionEventId, verificationCardSetId);

        try {
            ChoiceCodesComputationStatus contributionsStatus = choiceCodesGenerationContributionsService
                .getCompositeComputedValuesStatus(electionEventId, tenantId, verificationCardSetId, chunkCount);
            String result = Json.createObjectBuilder().add("status", contributionsStatus.name()).build().toString();
            return Response.ok().entity(result).build();

        } catch (ResourceNotFoundException e) {
            LOG.error("Resource not found matching the received parameters", e);
            return Response.status(Status.NOT_FOUND).build();
        }
    }

    @POST
    @Path(PATH_DECRYPT_CONTRIBUTIONS)
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response getChoiceCodeNodesDecryptContributions(
            @HeaderParam(Constants.PARAMETER_HEADER_X_REQUEST_ID) String trackingId,
            @PathParam(PATH_PARAMETER_TENANT_ID) final String tenantId,
            @PathParam(PATH_PARAMETER_ELECTION_EVENT_ID) final String electionEventId,
            @PathParam(PATH_PARAMETER_VERIFICATION_CARD_SET_ID) final String verificationCardSetId,
            @PathParam(PATH_PARAMETER_VERIFICATION_CARD_ID) final String verificationCardId,
            @NotNull final String gamma, @Context final HttpServletRequest request)
            throws ApplicationException, ResourceNotFoundException, EntryPersistenceException, IOException,
            ClassNotFoundException {

        trackIdInstance.setTrackId(trackingId);

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

        validateParameters(tenantId, electionEventId, verificationCardId);

        LOG.info("Requesting collection of the choice codes decrypt contributions for tenant {} electionEventId {} "
            + "and verificationCardId {}", tenantId, electionEventId, verificationCardId);

        List<ChoiceCodesVerificationDecryptResPayload> result =
            choiceCodesDecryptionContributionsService.requestChoiceCodesDecryptionContributionsSync(trackingId,
                tenantId, electionEventId, verificationCardSetId, verificationCardId, gamma);
        return Response.ok().entity(result).build();

    }

    @POST
    @Path(PATH_KEYS)
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response generateKeys(@HeaderParam(Constants.PARAMETER_HEADER_X_REQUEST_ID) String trackingId,
            @PathParam(PATH_PARAMETER_TENANT_ID) final String tenantId,
            @PathParam(PATH_PARAMETER_ELECTION_EVENT_ID) final String electionEventId,
            @NotNull final KeyGenerationRequestParameters keyGenerationRequestParameters,
            @Context final HttpServletRequest request)
            throws ApplicationException, ResourceNotFoundException, EntryPersistenceException,
            GeneralCryptoLibException, IOException {

        trackIdInstance.setTrackId(trackingId);

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

        validateTenantIDAndElectionEventID(tenantId, electionEventId);

        List<String> verificationCardSetIds = keyGenerationRequestParameters.getResourceIds();
        ZonedDateTime keysDateFrom = ZonedDateTime.parse(keyGenerationRequestParameters.getKeysDateFrom());
        ZonedDateTime keysDateTo = ZonedDateTime.parse(keyGenerationRequestParameters.getKeysDateTo());
        ElGamalEncryptionParameters elGamalEncryptionParameters =
            recoverEncryptionParameters(keyGenerationRequestParameters.getElGamalEncryptionParameters());

        LOG.info("Requesting Choice Codes key generation for tenant {} electionEventId {}", tenantId, electionEventId);

        ChoiceCodesKeysResponse choiceCodesKeyResponse =
            choiceCodesKeyGenerationService.requestChoiceCodesKeyGenerationSync(trackingId, tenantId, electionEventId,
                verificationCardSetIds, keysDateFrom, keysDateTo, elGamalEncryptionParameters);

        return Response.ok().entity(choiceCodesKeyResponse).build();
    }

    private ElGamalEncryptionParameters recoverEncryptionParameters(String encodedEncryptionParameters)
            throws GeneralCryptoLibException {

        String encryptionParamsAsJson =
            new String(Base64.getDecoder().decode(encodedEncryptionParameters), StandardCharsets.UTF_8);
        return ElGamalEncryptionParameters.fromJson(encryptionParamsAsJson);
    }

    private void validateParameters(final String tenantId, final String electionEventId,
            final String verificationCardSetId) throws ApplicationException {

        validateTenantIDAndElectionEventID(tenantId, electionEventId);

        if (verificationCardSetId == null || verificationCardSetId.isEmpty()) {
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_QUERY_PARAMETER_IS_NULL,
                RESOURCE_PATH, ErrorCodes.MISSING_QUERY_PARAMETER, PATH_PARAMETER_VERIFICATION_CARD_ID);
        }
    }

    private void validateTenantIDAndElectionEventID(final String tenantId, final String electionEventId)
            throws ApplicationException {
        if (tenantId == null || tenantId.isEmpty())
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_QUERY_PARAMETER_IS_NULL,
                RESOURCE_PATH, ErrorCodes.MISSING_QUERY_PARAMETER, PATH_PARAMETER_TENANT_ID);

        if (electionEventId == null || electionEventId.isEmpty()) {
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_QUERY_PARAMETER_IS_NULL,
                RESOURCE_PATH, ErrorCodes.MISSING_QUERY_PARAMETER, PATH_PARAMETER_ELECTION_EVENT_ID);
        }
    }
}
