/*
 * 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.config.webapp.mvc;

import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_ELECTIONEVENT_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_JOB_INSTANCE_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_STATUS;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_TENANT_ID;

import java.io.PrintWriter;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
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.servlet.mvc.method.annotation.StreamingResponseBody;
import org.springframework.web.util.UriComponentsBuilder;

import com.google.common.base.Strings;
import com.scytl.products.oscore.logging.core.provider.TransactionInfoProvider;
import com.scytl.products.ov.config.commands.api.ConfigurationService;
import com.scytl.products.ov.config.commands.api.output.BallotBoxesServiceOutput;
import com.scytl.products.ov.config.commands.api.output.ElectionEventServiceOutput;
import com.scytl.products.ov.config.commands.api.output.ProgressOutput;
import com.scytl.products.ov.config.commands.ballotbox.BallotBoxParametersHolder;
import com.scytl.products.ov.config.commands.electionevent.CreateElectionEventParametersHolder;
import com.scytl.products.ov.config.commands.voters.VotersParametersHolder;
import com.scytl.products.ov.config.commons.beans.StartVotingCardGenerationJobResponse;
import com.scytl.products.ov.config.commons.beans.VotingCardGenerationJobStatus;
import com.scytl.products.ov.config.exceptions.ConfigurationEngineException;
import com.scytl.products.ov.config.exceptions.CreateBallotBoxesException;
import com.scytl.products.ov.config.exceptions.CreateElectionEventException;
import com.scytl.products.ov.config.exceptions.CreateVotingCardSetException;
import com.scytl.products.ov.config.exceptions.specific.VerificationCardIdsGenerationException;
import com.scytl.products.ov.config.webapp.mvc.commands.ballotbox.BallotBoxWebappAdapter;
import com.scytl.products.ov.config.webapp.mvc.commands.electionevent.ElectionEventWebappAdapter;
import com.scytl.products.ov.config.webapp.mvc.commands.voters.VotersWebappAdapter;
import com.scytl.products.ov.config.webapp.mvc.dto.input.CreateBallotBoxesInput;
import com.scytl.products.ov.config.webapp.mvc.dto.input.CreateElectionEventInput;
import com.scytl.products.ov.config.webapp.mvc.dto.input.CreateVotingCardSetInput;
import com.scytl.products.ov.sdm.commons.domain.CreateVerificationCardIdsInput;

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;

/**
 *
 */
@Controller
@Api(value = "Configuration Engine REST API", description = "")
public class ConfigurationEngineController {

    static final String SEPARATOR = "\n";

    private static final Logger LOG = LoggerFactory.getLogger("std");

    @Autowired
    private ConfigurationService configurationService;

    @Autowired
    private ElectionEventWebappAdapter electionEventWebappAdapter;

    @Autowired
    private VotersWebappAdapter votersWebappAdapter;

    @Autowired
    private BallotBoxWebappAdapter ballotBoxWebappAdapter;

    @Autowired
    private TransactionInfoProvider transactionInfoProvider;

    /**
     * REST Service for connectivity validation purposes
     */
    @RequestMapping(value = "/check", method = RequestMethod.GET)
    @ResponseBody
    @ApiOperation(value = "Health Check Service", response = Void.class, notes = "Service to validate application is up & running.")
    @ApiResponses(value = {@ApiResponse(code = 500, message = "Internal Server Error") })
    public ResponseEntity<Void> serviceCheck(final HttpServletRequest httpServletRequest) {
        return new ResponseEntity<>(HttpStatus.OK);
    }

    /**
     * REST Service for createElectionEvent
     */
    @RequestMapping(value = "/createElectionEvent", method = RequestMethod.POST)
    @ResponseBody
    @ApiOperation(value = "Create Election Event Service", notes = "Executes the 'create election event' action. Receives as a input a json with the election event id (eeid), "
        + "a set of properties (validityPeriod, challengeResExpTime, authTokenExpTime, challengeLength, numVotesPerVotingCard, "
        + "numVotesPerAuthToken, maxNumberOfAttempts) and the output path (including timestamp folder). "
        + "Creates all election datapacks (keypair certificate, keystore and keystore password) for the ElectionEventCA, AuthoritiesCA, ServicesCA, CredentialsCA and "
        + "AdminBoard.", response = ElectionEventServiceOutput.class)
    @ApiResponses(value = {@ApiResponse(code = 500, message = "Internal Server Error") })
    public ResponseEntity<ElectionEventServiceOutput> createElectionEvent(final HttpServletRequest httpServletRequest,
            @ApiParam(value = "CreateElectionEventInput", required = true) @RequestBody final CreateElectionEventInput input)
            throws CreateElectionEventException {

        // when we have tenantId
        String tenant = "100";
        transactionInfoProvider.generate(tenant, httpServletRequest.getRemoteAddr(),
            httpServletRequest.getLocalAddr());

        CreateElectionEventParametersHolder holder = electionEventWebappAdapter.adapt(input);

        ElectionEventServiceOutput electionEventServiceOutput = configurationService.createElectionEvent(holder);

        return new ResponseEntity<>(electionEventServiceOutput, HttpStatus.OK);
    }

    /**
     * REST Service for createBallotBoxes
     */
    @RequestMapping(value = "/createBallotBoxes", method = RequestMethod.POST)
    @ResponseBody
    @ApiOperation(value = "Create Ballot Boxes Service", notes = "Executes the 'create ballot boxes' action. Receives as a input a json with the ballotID, the list of Ballots Boxes Ids, "
        + "a set of properties (validityPeriod, start and end) and the output path (including timestamp and eeid folders). "
        + "Creates all datapacks (keypair certificate, keystore and keystore password) for the ballot boxes and save them on the path: "
        + "<b>output/{timestamp}/{eeid}/ONLINE/electionInformation/ballots/{ballotId}/ballotBoxes/{ballotBoxId}</b> folder.", response = BallotBoxesServiceOutput.class)
    @ApiResponses(value = {@ApiResponse(code = 500, message = "Internal Server Error") })
    public ResponseEntity<BallotBoxesServiceOutput> createBallotBoxes(final HttpServletRequest httpServletRequest,
            @ApiParam(value = "CreateBallotBoxesInput", required = true) @RequestBody final CreateBallotBoxesInput input)
            throws CreateBallotBoxesException {

        // when we have tenantId
        String tenant = "100";
        transactionInfoProvider.generate(tenant, httpServletRequest.getRemoteAddr(),
            httpServletRequest.getLocalAddr());

        BallotBoxParametersHolder holder = ballotBoxWebappAdapter.adapt(input);

        BallotBoxesServiceOutput ballotBoxesServiceOutput = configurationService.createBallotBoxes(holder);

        return new ResponseEntity<>(ballotBoxesServiceOutput, HttpStatus.OK);
    }

    /**
     * Runs a voting card set pre-computation, that is, the generation of
     * verification card set IDs for those voting cards. The output is a list of
     * verification card IDs, one per line (hence the 'text/csv' MIME type --
     * https://tools.ietf.org/html/rfc7111)
     */
    @RequestMapping(value = "/precompute", method = RequestMethod.POST, consumes = "application/json", produces = "text/csv")
    @ResponseBody
    @ApiOperation(value = "Precompute operation", notes = "Executes the 'precompute' action. "
        + "Generates the requested number of verification card IDs for a voting card set", response = List.class)
    public ResponseEntity<StreamingResponseBody> precompute(final HttpServletRequest request,
            @RequestBody final CreateVerificationCardIdsInput input)
            throws VerificationCardIdsGenerationException {
        // This value should not be hard-coded.
        final String tenant = "100";

        transactionInfoProvider.generate(tenant, request.getRemoteAddr(), request.getLocalAddr());

        // Get a stream of verification card IDs.
        Stream<String> verificationCardIds = configurationService.createVerificationCardIdStream(
            input.getElectionEventId(), input.getVerificationCardSetId(), input.getNumberOfVerificationCardIds());

        // Push each verification card ID to the response output stream.
        StreamingResponseBody responseBody = outputStream -> {
            try (PrintWriter pw = new PrintWriter(outputStream)) {
                verificationCardIds.forEach(vcid -> {
                    pw.print(vcid);
                    // Ensure consistent new-line characters.
                    pw.print(SEPARATOR);
                    pw.flush();
                });
            }
        };

        return ResponseEntity.ok().contentType(MediaType.parseMediaType("text/csv")).body(responseBody);
    }

    /**
     * REST Service to get the progress
     */
    @RequestMapping(value = "/progress/{id}", method = RequestMethod.GET)
    @ResponseBody
    @ApiOperation(value = "Show Progress Service", notes = "Show Progress in percentage of a voting card set execution.", response = ProgressOutput.class)
    @ApiResponses(value = {@ApiResponse(code = 500, message = "Internal Server Error") })
    public ResponseEntity<ProgressOutput> getProgress(
            @ApiParam(value = "String", required = true) @PathVariable final String id) {
        if (Strings.isNullOrEmpty(id)) {
            return new ResponseEntity<>(configurationService.getProgress(), HttpStatus.OK);
        } else {
            return new ResponseEntity<>(configurationService.getProgress(id), HttpStatus.OK);
        }
    }

    @ExceptionHandler(ConfigurationEngineException.class)
    public ResponseEntity<Void> handleConfigurationEngineError(final HttpServletRequest req,
            final Exception exception) {

        LOG.error("Request: {} raised", req.getRequestURL(), exception);
        return new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleError(final HttpServletRequest req, final Exception exception) {

        LOG.error("Request: {} raised", req.getRequestURL(), exception);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ExceptionUtils.getRootCauseMessage(exception));
    }

    @RequestMapping(value = "{tenantId}/{electionEventId}/jobs", method = RequestMethod.POST)
    @ResponseBody
	public ResponseEntity<StartVotingCardGenerationJobResponse> startVotingCardGenerationJob(
			@PathVariable(JOB_IN_PARAM_TENANT_ID) final String tenantId,
            @PathVariable(JOB_IN_PARAM_ELECTIONEVENT_ID) final String electionEventId,
            @RequestBody final CreateVotingCardSetInput input, final HttpServletRequest servletRequest,
            final UriComponentsBuilder uriBuilder) throws CreateVotingCardSetException {
        VotersParametersHolder holder = votersWebappAdapter.adapt(input);
        final StartVotingCardGenerationJobResponse response =
            configurationService.startVotingCardGenerationJob(tenantId, electionEventId, holder);

        final String jobId = response.getJobId();
        final URI location =
            uriBuilder.path(servletRequest.getServletPath() + "/{jobId}").buildAndExpand(jobId).toUri();

        return ResponseEntity.created(location).body(response);
    }

    @ApiOperation(value = "Get list of jobs", notes = "Gets the list of jobs for the specified Tenant and ElectionEvent. "
        + "Returns a list of objects with details about the jobs, including, the Id, Status and Start Time  and "
        + "Progress details. May return an empty list.", response = VotingCardGenerationJobStatus.class, produces = MediaType.APPLICATION_JSON_VALUE)
    @ApiResponses(value = {@ApiResponse(code = 200, message = "OK", response = List.class),
            @ApiResponse(code = 500, message = "Internal Server Error", response = String.class) })
    @RequestMapping(value = "{tenantId}/{electionEventId}/jobs", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ResponseEntity<List<VotingCardGenerationJobStatus>> getJobs(@PathVariable(JOB_IN_PARAM_TENANT_ID) final String tenantId,
            @PathVariable(JOB_IN_PARAM_ELECTIONEVENT_ID) final String electionEventId) {

        List<VotingCardGenerationJobStatus> status = configurationService.getJobs();
        return ResponseEntity.ok(status);
    }

    @ApiOperation(value = "Get list of jobs with specific status", notes = "Gets the list of jobs with a specific status, for the specified Tenant and ElectionEvent. "
        + "Returns a list of objects with details about the Jobs, including, the Id, Status and Start Time  and "
        + "Progress details. May return an empty list.", response = VotingCardGenerationJobStatus.class, produces = MediaType.APPLICATION_JSON_VALUE)
    @ApiResponses(value = {@ApiResponse(code = 200, message = "OK", response = List.class),
            @ApiResponse(code = 500, message = "Internal Server Error", response = String.class) })
    @RequestMapping(value = "{tenantId}/{electionEventId}/jobs", method = RequestMethod.GET, params = "status", produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ResponseEntity<List<VotingCardGenerationJobStatus>> getJobsWithStatus(@PathVariable(JOB_IN_PARAM_TENANT_ID) final String tenantId,
            @PathVariable(JOB_IN_PARAM_ELECTIONEVENT_ID) final String electionEventId,
            @RequestParam(JOB_IN_PARAM_STATUS) final String jobStatus) {

        List<VotingCardGenerationJobStatus> status =
            configurationService.getJobsWithStatus(tenantId, electionEventId, jobStatus);
        return ResponseEntity.ok(status);
    }

    @ApiOperation(value = "Get status information of job instance", notes = "Gets status information of a specific job instance of a specific election event and tenant. Returned data "
        + "includes Id, Status, Start time and detailed progress status.", response = VotingCardGenerationJobStatus.class, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    @ApiResponses(value = {@ApiResponse(code = 200, message = "OK", response = VotingCardGenerationJobStatus.class),
            @ApiResponse(code = 404, message = "Not Found"),
            @ApiResponse(code = 500, message = "Internal Server Error", response = String.class) })
    @RequestMapping(value = "{tenantId}/{electionEventId}/jobs/{jobInstanceId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ResponseEntity<VotingCardGenerationJobStatus> getVotingCardGenerationJobStatus(
            @PathVariable(JOB_IN_PARAM_TENANT_ID) final String tenantId,
            @PathVariable(JOB_IN_PARAM_ELECTIONEVENT_ID) final String electionEventId,
            @PathVariable(JOB_IN_PARAM_JOB_INSTANCE_ID) final String jobInstanceId) {
        final Optional<VotingCardGenerationJobStatus> jobStatus =
            configurationService.getVotingCardGenerationJobStatus(tenantId, electionEventId, jobInstanceId);

        if (jobStatus.isPresent()) {
            return ResponseEntity.ok(jobStatus.get());
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}
