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

import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.json.JsonObject;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
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.oscore.logging.core.provider.TransactionInfoProvider;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignatureException;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadVerificationException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.sdm.application.log.SdmSecureLogEvent;
import com.scytl.products.ov.sdm.application.service.IdleStatusService;
import com.scytl.products.ov.sdm.application.service.VotingCardSetComputationService;
import com.scytl.products.ov.sdm.application.service.VotingCardSetPrecomputationService;
import com.scytl.products.ov.sdm.application.service.VotingCardSetService;
import com.scytl.products.ov.sdm.domain.model.config.VotingCardGenerationJobStatus;
import com.scytl.products.ov.sdm.domain.model.generator.DataGeneratorResponse;
import com.scytl.products.ov.sdm.domain.model.status.InvalidStatusTransitionException;
import com.scytl.products.ov.sdm.domain.model.votingcardset.VotingCardSetRepository;
import com.scytl.products.ov.sdm.domain.model.votingcardset.VotingCardSetUpdateInputData;
import com.scytl.products.ov.sdm.domain.service.ProgressManagerService;
import com.scytl.products.ov.sdm.infrastructure.JsonConstants;
import com.scytl.products.ov.sdm.infrastructure.cc.PayloadStorageException;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;

/**
 * The voting card set end-point.
 */
@RestController
@RequestMapping("/sdm-ws-rest/votingcardsets")
@Api(value = "Voting card set REST API", description = "")
public class VotingCardSetResource {

    private static final Logger LOGGER = LoggerFactory.getLogger(VotingCardSetResource.class);

    private static final String VOTING_CARD_SET_URL_PATH =
        "/electionevent/{electionEventId}/votingcardset/{votingCardSetId}";

    // The voting card set repository
    @Autowired
    private VotingCardSetRepository votingCardSetRepository;

    // The configuration of contests / elections
    @Value("${elections.config.filename}")
    private String configFile;

    @Autowired
    private VotingCardSetService votingCardSetService;

    @Autowired
    private VotingCardSetPrecomputationService votingCardSetPrecomputationService;

    @Autowired
    private VotingCardSetComputationService votingCardSetComputationService;

    @Autowired
    private ProgressManagerService<VotingCardGenerationJobStatus> progressManagerService;

    @Autowired
    private TransactionInfoProvider transactionInfoProvider;

    @Autowired
    private SecureLoggingWriter secureLogger;

    @Value("${tenantID}")
    private String tenantId;
    
    @Autowired
    IdleStatusService idleStatusService;

    /**
     * Changes the status of a list voting card sets by performing the
     * appropriate operations. The HTTP call uses a PUT request to the voting
     * card set endpoint with the desired status parameter. If the requested
     * status cannot be transitioned to from the current one, the call will
     * fail.
     *
     * @param electionEventId
     *            the election event id.
     * @param votingCardSetId
     *            the voting card set id.
     * @return a list of ids of the created voting card sets.
     * @throws PayloadStorageException
     * @throws PayloadSignatureException
     * @throws PayloadVerificationException
     */
    @PutMapping(value = VOTING_CARD_SET_URL_PATH, consumes = "application/json", produces = "application/json")
    @ApiOperation(value = "Change the status of a voting card set", notes = "Change the status of a voting card set, performing the necessary operations for the transition")
    @ApiResponses(value = {@ApiResponse(code = 404, message = "Not Found") })
    public ResponseEntity<?> setVotingCardSetStatus(
            @ApiParam(value = "String", required = true) @PathVariable final String electionEventId,
            @ApiParam(value = "String", required = true) @PathVariable final String votingCardSetId,
            @ApiParam(required = true) @RequestBody final VotingCardSetUpdateInputData requestBody)
            throws ResourceNotFoundException, IOException, GeneralCryptoLibException, PayloadStorageException,
            PayloadSignatureException, JsonProcessingException, PayloadVerificationException {

        ResponseEntity<?> response;
        try {

            transactionInfoProvider.generate(tenantId, "", "");

            // SV-6488 Implement a proper FSM down the line.
            switch (requestBody.getStatus()) {
            case PRECOMPUTED:
                votingCardSetPrecomputationService.precompute(votingCardSetId, electionEventId,
                    requestBody.getPrivateKeyPEM(), requestBody.getAdminBoardId());
                response = new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
                break;
            case COMPUTING:
                votingCardSetComputationService.compute(votingCardSetId, electionEventId);
                response = new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
                break;
            case VCS_DOWNLOADED:
                votingCardSetService.download(votingCardSetId, electionEventId);
                response = new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
                break;
            case SIGNED:
                response = signVotingCardSet(electionEventId, votingCardSetId, requestBody.getPrivateKeyPEM());
                break;
            default:
                response = new ResponseEntity<>("Status is not supported", HttpStatus.BAD_REQUEST);
            }

        } catch (InvalidStatusTransitionException e) {
        	LOGGER.info("Error trying to set voting card set status.", e);
            response = ResponseEntity.badRequest().build();
        }
        return response;
    }

    /**
     * Stores a list of voting card sets.
     *
     * @param electionEventId
     *            the election event id.
     * @param votingCardSetId
     *            the voting card set id.
     * @return a list of ids of the created voting card sets.
     * @throws InvalidStatusTransitionException
     */
    @RequestMapping(value = VOTING_CARD_SET_URL_PATH, method = RequestMethod.POST, produces = "application/json")
    @ApiOperation(value = "Create voting card set", notes = "Service to create a voting card set.", response = Void.class)
    @ApiResponses(value = {@ApiResponse(code = 404, message = "Not Found") })
    public ResponseEntity<?> createVotingCardSet(
            @ApiParam(value = "String", required = true) @PathVariable final String electionEventId,
            @ApiParam(value = "String", required = true) @PathVariable final String votingCardSetId,
            final UriComponentsBuilder uriBuilder)
            throws IOException, ResourceNotFoundException, InvalidStatusTransitionException {

        transactionInfoProvider.generate(tenantId, "", "");

        secureLogger.log(Level.INFO, new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.GENERATING_VCS)
            .objectId(votingCardSetId).electionEvent(electionEventId).createLogInfo());

        DataGeneratorResponse response = null;
        try {
            response = votingCardSetService.generate(votingCardSetId, electionEventId);
        } catch (IOException e) {
            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.VCS_GENERATION_FAILED)
                    .objectId(votingCardSetId).electionEvent(electionEventId)
                    .additionalInfo("#err_desc", e.getMessage()).createLogInfo());
            throw e;
        }
        if (response.isSuccessful()) {

            secureLogger.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.VCS_GENERATED_SUCCESSFULLY)
                    .objectId(votingCardSetId).electionEvent(electionEventId).createLogInfo());

            final URI uri = uriBuilder.path("/electionevent/{electionEventId}/progress/{jobId}")
                .buildAndExpand(electionEventId, votingCardSetId, response.getResult()).toUri();
            return ResponseEntity.created(uri).body(response);
        }
        secureLogger.log(Level.ERROR,
            new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.VCS_GENERATION_FAILED)
                .objectId(votingCardSetId).electionEvent(electionEventId)
                .additionalInfo("#err_desc", "Response is not successful: " + response.getResult()).createLogInfo());

        return new ResponseEntity<Void>(HttpStatus.INTERNAL_SERVER_ERROR);
    }

    /**
     * Returns an voting card set identified by election event and its id.
     *
     * @param electionEventId
     *            the election event id.
     * @param votingCardSetId
     *            the voting card set id.
     * @return An voting card set identified by election event and its id.
     */
    @RequestMapping(value = VOTING_CARD_SET_URL_PATH, method = RequestMethod.GET, produces = "application/json")
    @ResponseBody
    @ApiOperation(value = "Get voting card set", notes = "Service to retrieve a given voting card set.", response = String.class)
    @ApiResponses(value = {@ApiResponse(code = 404, message = "Not Found") })
    public ResponseEntity<String> getVotingCardSet(
            @ApiParam(value = "String", required = true) @PathVariable final String electionEventId,
            @ApiParam(value = "String", required = true) @PathVariable final String votingCardSetId) {
        Map<String, Object> attributeValueMap = new HashMap<>();
        attributeValueMap.put(JsonConstants.JSON_ATTRIBUTE_NAME_ID, votingCardSetId);
        attributeValueMap.put(JsonConstants.JSON_STRUCTURE_NAME_ELECTION_EVENT_ID, electionEventId);
        String result = votingCardSetRepository.find(attributeValueMap);
        JsonObject jsonObject = JsonUtils.getJsonObject(result);
        if (!jsonObject.isEmpty()) {
            return new ResponseEntity<>(result, HttpStatus.OK);
        }
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

    /**
     * Returns a list of all voting card sets.
     *
     * @param electionEventId
     *            the election event id.
     * @return The list of voting card sets.
     */
    @RequestMapping(value = "/electionevent/{electionEventId}", method = RequestMethod.GET, produces = "application/json")
    @ResponseBody
    @ApiOperation(value = "List voting card sets", notes = "Service to retrieve the list of voting card sets.", response = String.class)
    public String getVotingCardSets(@PathVariable String electionEventId) {
        Map<String, Object> attributeValueMap = new HashMap<>();
        attributeValueMap.put(JsonConstants.JSON_STRUCTURE_NAME_ELECTION_EVENT_ID, electionEventId);
        return votingCardSetRepository.list(attributeValueMap);
    }

    /**
     * Change the state of the voting card set from generated to signed for a
     * given election event and voting card set id.
     *
     * @param electionEventId
     *            the election event id.
     * @param votingCardSetId
     *            the voting card set id.
     * @return HTTP status code 200 - If the voting card set is successfully
     *         signed. HTTP status code 404 - If the resource is not found. HTTP
     *         status code 412 - If the votig card set is already signed.
     */
    private ResponseEntity<Void> signVotingCardSet(String electionEventId, String votingCardSetId,
            String privateKeyPEM) {
        
        if (!idleStatusService.isIdReady(votingCardSetId)) {
            return new ResponseEntity<>(HttpStatus.OK);
        }

        String fetchingErrorMessage = "An error occurred while fetching the given voting card set to sign";
        String signingErrorMessage = "An error occurred while signing the given voting card set";
        try {

            transactionInfoProvider.generate(tenantId, "", "");

            secureLogger.log(Level.INFO, new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.SIGNING_VCS)
                .objectId(votingCardSetId).electionEvent(electionEventId).createLogInfo());

            if (votingCardSetService.sign(electionEventId, votingCardSetId, privateKeyPEM)) {
                secureLogger.log(Level.INFO,
                    new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.VCS_SIGNED_SUCCESSFULLY)
                        .objectId(votingCardSetId).electionEvent(electionEventId).createLogInfo());
                return new ResponseEntity<>(HttpStatus.OK);
            } else {
                LOGGER.error(fetchingErrorMessage);

                secureLogger.log(Level.ERROR,
                    new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.VCS_SIGNING_FAILED)
                        .objectId(votingCardSetId).electionEvent(electionEventId)
                        .additionalInfo("err_desc", fetchingErrorMessage + ": problems with voting card set json")
                        .createLogInfo());

                return new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
            }
        } catch (ResourceNotFoundException e) {
            LOGGER.error(fetchingErrorMessage, e);
            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.VCS_SIGNING_FAILED)
                    .objectId(votingCardSetId).electionEvent(electionEventId)
                    .additionalInfo("err_desc", fetchingErrorMessage + e.getMessage()).createLogInfo());
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        } catch (GeneralCryptoLibException | IOException e) {
            LOGGER.error(signingErrorMessage, e);
            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.VCS_SIGNING_FAILED)
                    .objectId(votingCardSetId).electionEvent(electionEventId)
                    .additionalInfo("err_desc", signingErrorMessage + ": " + e.getMessage()).createLogInfo());

            return new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
        } finally {
            idleStatusService.freeIdleStatus(votingCardSetId);
        }
    }

    /**
     * Get the status/progress of the specified voting card generation job.
     *
     * @param electionEventId
     *            the election event id.
     * @param jobId
     *            the job execution id.
     * @return HTTP status code 200 - If we got the voting card status. HTTP
     *         status code 404 - If the resource is not found.
     */
    @RequestMapping(value = "/electionevent/{electionEventId}/progress/{jobId}", method = RequestMethod.GET)
    @ApiOperation(value = "Get voting card generation status", notes = "Service to get the status/progress of a specific voting card generation job", response = VotingCardGenerationJobStatus.class)
    @ApiResponses(value = {@ApiResponse(code = 404, message = "Not Found"),
            @ApiResponse(code = 412, message = "Precondition Failed") })
    public ResponseEntity<VotingCardGenerationJobStatus> getJobProgress(
            @ApiParam(value = "String", required = true) @PathVariable String electionEventId,
            @ApiParam(value = "String", required = true) @PathVariable String jobId) {

        final VotingCardGenerationJobStatus status = progressManagerService.getForJob(jobId);
        return ResponseEntity.ok(status);
    }

    /**
     * Get the status/progress of all voting card generation jobs with a
     * specific status (started by default) .
     *
     * @return HTTP status code 200 - If we got the voting card status. HTTP
     *         status code 404 - If the resource is not found.
     */
    @RequestMapping(value = "/progress/jobs", method = RequestMethod.GET)
    @ApiOperation(value = "Get voting card generation status", notes = "Service to get the status/progress of all started voting card generation job", response = VotingCardGenerationJobStatus.class)
    @ApiResponses(value = {@ApiResponse(code = 404, message = "Not Found"),
            @ApiResponse(code = 412, message = "Precondition Failed") })
    public ResponseEntity<List<VotingCardGenerationJobStatus>> getJobsProgress(
            @RequestParam(value = "status", required = false) String jobStatus) {

        final List<VotingCardGenerationJobStatus> jobsProgressByStatus = StringUtils.isBlank(jobStatus)
                ? progressManagerService.getAll() : progressManagerService.getAllByStatus(jobStatus);
        return ResponseEntity.ok(jobsProgressByStatus);
    }

}
