/*
 * 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 java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.JAXBException;

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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.xml.sax.SAXException;

import com.scytl.product.ov.sdm.plugin.PhaseName;
import com.scytl.product.ov.sdm.plugin.Plugins;
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.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.path.PathResolver;
import com.scytl.products.ov.sdm.application.log.SdmSecureLogEvent;
import com.scytl.products.ov.sdm.application.service.ElectionEventService;
import com.scytl.products.ov.sdm.application.service.ExportImportService;
import com.scytl.products.ov.sdm.domain.model.operation.OperationResult;
import com.scytl.products.ov.sdm.domain.model.operation.OperationsData;
import com.scytl.products.ov.sdm.domain.model.operation.OperationsOutputCode;
import com.scytl.products.ov.sdm.domain.model.status.Status;
import com.scytl.products.ov.sdm.domain.service.ConfigConstants;
import com.scytl.products.ov.sdm.plugin.ExecutionListenerImpl;
import com.scytl.products.ov.sdm.plugin.KeyParameter;
import com.scytl.products.ov.sdm.plugin.Parameters;
import com.scytl.products.ov.sdm.plugin.PluginSequenceResolver;
import com.scytl.products.ov.sdm.plugin.SequentialExecutor;
import com.scytl.products.ov.sdm.plugin.XmlObjectsLoader;

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;

@RestController
@RequestMapping("/sdm-ws-rest/operation")
@Api(value = "SDM Operations REST API")
public class OperationsResource {

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

    @Autowired
    private ExportImportService exportImportService;

    @Autowired
    private ElectionEventService electionEventService;

    @Autowired
    private PathResolver pathResolver;

    @Autowired
    private SequentialExecutor sequentialExecutor;

    @Autowired
    private TransactionInfoProvider transactionInfoProvider;

    @Autowired
    private SecureLoggingWriter secureLogger;

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

    private static final String PLUGIN_FILE_NAME = "plugin.xml";

    @RequestMapping(value = "/generate-pre-voting-outputs/{electionEventId}", method = RequestMethod.POST)
    @ApiOperation(value = "Export operation service", notes = "", response = Void.class)
    @ApiResponses(value = {@ApiResponse(code = 404, message = "Not Found"),
            @ApiResponse(code = 403, message = "Forbidden"),
            @ApiResponse(code = 500, message = "Internal Server Error") })
    public ResponseEntity<OperationResult> generatePreVotingOutputsOperation(
            @ApiParam(value = "String", required = true) @PathVariable String electionEventId,
            @RequestBody final OperationsData request) {

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

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

        if (StringUtils.isEmpty(request.getPrivateKeyInBase64())) {

            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.PRE_VOTING_OUTPUT_GENERATING_FAILED)
                    .electionEvent(electionEventId).additionalInfo("err_desc", "Private key in Base64 is empty")
                    .createLogInfo());

            throw new IllegalArgumentException("PrivateKey parameter is required");
        }
        Parameters parameters = buildParameters(electionEventId, request.getPrivateKeyInBase64(), "");
        ResponseEntity<OperationResult> result = executeOperationForPhase(parameters,
                PhaseName.GENERATE_PRE_VOTING_OUTPUTS, true, SdmSecureLogEvent.PRE_VOTING_OUTPUT_GENERATING_FAILED,
                electionEventId);

        if (result.getStatusCode() == HttpStatus.OK) {

            secureLogger.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.PRE_VOTING_OUTPUT_GENERATED_SUCCESSFULLY)
                    .electionEvent(electionEventId).createLogInfo());
        }

        return result;
    }

    @RequestMapping(value = "/generate-ea-structure/{electionEventId}", method = RequestMethod.POST)
    @ApiOperation(value = "Export operation service", notes = "", response = Void.class)
    @ApiResponses(value = {@ApiResponse(code = 404, message = "Not Found"),
            @ApiResponse(code = 403, message = "Forbidden"),
            @ApiResponse(code = 500, message = "Internal Server Error") })
    public ResponseEntity<OperationResult> extendedAuthenticationMappingDataOperation(
            @ApiParam(value = "String", required = true) @PathVariable String electionEventId,
            @RequestBody final OperationsData request) {
        Parameters parameters = buildParameters(electionEventId, request.getPrivateKeyInBase64(), null);
        return executeOperationForPhase(parameters, PhaseName.PREPARE_VC_GENERATION, true, null, null);
    }

    @RequestMapping(value = "/generate-post-voting-outputs/{electionEventId}/{ballotBoxStatus}", method = RequestMethod.POST)
    @ApiOperation(value = "Export operation service", notes = "", response = Void.class)
    @ApiResponses(value = {@ApiResponse(code = 404, message = "Not Found"),
            @ApiResponse(code = 403, message = "Forbidden"),
            @ApiResponse(code = 500, message = "Internal Server Error") })
    public ResponseEntity<OperationResult> generatePostVotingOutputsOperation(
            @ApiParam(value = "String", required = true) @PathVariable String electionEventId,
            @ApiParam(value = "String", required = true) @PathVariable String ballotBoxStatus,
            @RequestBody final OperationsData request) {

        if (StringUtils.isEmpty(request.getPrivateKeyInBase64())) {
            throw new IllegalArgumentException("PrivateKey parameter is required");
        }
        PhaseName phaseName;
        try {
            Status status = Status.valueOf(ballotBoxStatus.toUpperCase());
            phaseName = determinePhase(status);
            if (phaseName == null) {
                return handleException(OperationsOutputCode.ERROR_STATUS_NAME);
            }
        } catch (Exception e) {
            int errorCode = OperationsOutputCode.ERROR_STATUS_NAME.value();
            return handleException(e, errorCode);
        }

        Parameters parameters = buildParameters(electionEventId, request.getPrivateKeyInBase64(), "");
        return executeOperationForPhase(parameters, phaseName, true, null, null);
    }

    private PhaseName determinePhase(Status status) {
        PhaseName phaseName = null;
        switch (status) {
        // for this status there is no "phase" associated. it MUST be an error
        // (until it is not)
        case SIGNED:
            break;
        case BB_DOWNLOADED:
            phaseName = PhaseName.DOWNLOAD;
            break;
        case DECRYPTED:
            phaseName = PhaseName.DECRYPTION;
            break;
        case TALLIED:
            phaseName = PhaseName.TALLY;
            break;
        default:
            break;
        }
        return phaseName;
    }

    /**
     * Execute export
     *
     * @param electionEventId
     * @param request
     * @return
     * @throws ResourceNotFoundException
     * @throws IOException
     * @throws URISyntaxException
     */
    @RequestMapping(value = "/export/{electionEventId}", method = RequestMethod.POST)
    @ApiOperation(value = "Export operation service", notes = "", response = Void.class)
    @ApiResponses(value = {@ApiResponse(code = 404, message = "Not Found"),
            @ApiResponse(code = 403, message = "Forbidden"),
            @ApiResponse(code = 500, message = "Internal Server Error") })
    public ResponseEntity<OperationResult> exportOperation(
            @ApiParam(value = "String", required = true) @PathVariable String electionEventId,
            @RequestBody final OperationsData request) {
        try {

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

            if (StringUtils.isEmpty(request.getPath())) {
                throw new ResourceNotFoundException("path parameter is required");
            }

            String normalizedPath = Paths.get(request.getPath()).toFile().getAbsolutePath();

            Parameters parameters = buildParameters(electionEventId, "", normalizedPath);

            /**
             * If there is a command defined within the plugin xml file and it
             * fails the export won't be carried on
             */
            final ResponseEntity<OperationResult> operationResultResponseEntity =
                executeOperationForPhase(parameters, PhaseName.EXPORT, false, null, null);
            if (!operationResultResponseEntity.getStatusCode().equals(HttpStatus.OK)) {
                return operationResultResponseEntity;
            }

            exportImportService.dumpDatabase(electionEventId);

            String eeAlias = electionEventService.getElectionEventAlias(electionEventId);

            if (request.isElectionEventData()) {
                exportImportService.exportElectionEventWithoutElectionInformation(request.getPath(), electionEventId,
                    eeAlias);
                exportImportService.exportElectionEventElectionInformation(request.getPath(), electionEventId, eeAlias);
            }

            if (request.isVotingCardsData()) {
                exportImportService.exportVotingCards(request.getPath(), electionEventId, eeAlias);
            }

            if (request.isCustomerData()) {
                exportImportService.exportCustomerSpecificData(request.getPath(), electionEventId, eeAlias);
            }
            
            if (request.isComputedChoiceCodes()) {
                exportImportService.exportComputedChoiceCodes(request.getPath(), electionEventId, eeAlias);
            }
            
            if (request.isPreComputedChoiceCodes()) {
                exportImportService.exportPreComputedChoiceCodes(request.getPath(), electionEventId, eeAlias);
            }
            
            if (request.isBallotBoxes()) {
                exportImportService.exportBallotBoxes(request.getPath(), electionEventId, eeAlias);
            }

        } catch (ResourceNotFoundException e) {
            int errorCode = OperationsOutputCode.MISSING_PARAMETER.value();
            return handleException(e, errorCode);
        } catch (IOException e) {
            int errorCode = OperationsOutputCode.ERROR_IO_OPERATIONS.value();
            return handleException(e, errorCode);
        } catch (Exception e) {
            int errorCode = OperationsOutputCode.GENERAL_ERROR.value();
            return handleException(e, errorCode);
        }
        return new ResponseEntity<>(HttpStatus.OK);
    }

    /**
     * Execute import
     *
     * @param request
     * @return
     * @throws IOException
     */
    @RequestMapping(value = "/import", method = RequestMethod.POST)
    @ApiOperation(value = "Import operation service", notes = "", response = Void.class)
    @ApiResponses(value = {@ApiResponse(code = 404, message = "Not Found"),
            @ApiResponse(code = 403, message = "Forbidden"),
            @ApiResponse(code = 500, message = "Internal Server Error") })
    public ResponseEntity<OperationResult> importOperation(@RequestBody final OperationsData request) {

        try {

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

            if (StringUtils.isEmpty(request.getPath())) {
                throw new ResourceNotFoundException("path parameter is required");
            }

            exportImportService.importData(request.getPath());
            exportImportService.importDatabase();

        } catch (IOException e) {
            int errorCode = OperationsOutputCode.ERROR_IO_OPERATIONS.value();
            return handleException(e, errorCode);
        } catch (ResourceNotFoundException e) {
            int errorCode = OperationsOutputCode.MISSING_PARAMETER.value();
            return handleException(e, errorCode);
        } catch (Exception e) {
            int errorCode = OperationsOutputCode.GENERAL_ERROR.value();
            return handleException(e, errorCode);
        }
        return new ResponseEntity<>(HttpStatus.OK);
    }

    protected List<String> getCommands(PhaseName phaseName) throws IOException, JAXBException, SAXException {

        Path pluginXmlPath = pathResolver.resolve(ConfigConstants.SDM_DIR_NAME).resolve(PLUGIN_FILE_NAME);
        if (!Files.exists(pluginXmlPath)) {
            pluginXmlPath = pathResolver.resolve(ConfigConstants.SDM_DIR_NAME)
                .resolve(ConfigConstants.SDM_CONFIG_DIR_NAME).resolve(PLUGIN_FILE_NAME);
            if (!Files.exists(pluginXmlPath)) {
                LOGGER.error("The plugin.xml file is not found");
                return new ArrayList<String>();
            }
        }

        Plugins plugins = XmlObjectsLoader.unmarshal(pluginXmlPath);
        PluginSequenceResolver pluginSequence = new PluginSequenceResolver(plugins);
        return pluginSequence.getActionsForPhase(phaseName);
    }

    private ResponseEntity<OperationResult> executeOperationForPhase(Parameters parameters, PhaseName phaseName,
                                                                     boolean failOnEmptyCommandsForPhase, SdmSecureLogEvent secureLogEvent, String electionEventId) {
        try {
            List<String> commandsForPhase = getCommands(phaseName);

            if (failOnEmptyCommandsForPhase && commandsForPhase.isEmpty()) {
                logSecure(secureLogEvent, electionEventId,
                    "The request can not be performed for 4005, Missing commands for phase");

                return handleException(OperationsOutputCode.MISSING_COMMANDS_FOR_PHASE);
            }
            ExecutionListenerImpl listener = new ExecutionListenerImpl();
            sequentialExecutor.execute(commandsForPhase, parameters, listener);

            if (listener.getError() != 0) {
                logSecure(secureLogEvent, electionEventId, "The request can not be performed for the current resource: "
                    + listener.getError() + ", " + listener.getMessage());
                return handleException(listener);
            }

        } catch (IOException e) {
            int errorCode = OperationsOutputCode.ERROR_IO_OPERATIONS.value();
            logSecure(secureLogEvent, electionEventId, errorCode + ": " + e.getMessage());
            return handleException(e, errorCode);
        } catch (JAXBException | SAXException e) {
            int errorCode = OperationsOutputCode.ERROR_PARSING_FILE.value();
            logSecure(secureLogEvent, electionEventId, errorCode + ": " + e.getMessage());
            return handleException(e, errorCode);
        } catch (Exception e) {
            int errorCode = OperationsOutputCode.GENERAL_ERROR.value();
            logSecure(secureLogEvent, electionEventId, errorCode + ": " + e.getMessage());
            return handleException(e, errorCode);
        }
        return new ResponseEntity<>(HttpStatus.OK);
    }

    private void logSecure(final SdmSecureLogEvent secureLogEvent, final String electionEventId,
            final String errorMessage) {
        if (secureLogEvent != null) {
            secureLogger.log(Level.ERROR, new LogContent.LogContentBuilder().logEvent(secureLogEvent)
                .electionEvent(electionEventId).additionalInfo("err_desc", errorMessage).createLogInfo());
        }
    }

    private Parameters buildParameters(String electionEventId, String privateKeyInBase64, String path) {
        Parameters parameters = new Parameters();

        if (StringUtils.isNotEmpty(electionEventId)) {
            String electionEventAlias = electionEventService.getElectionEventAlias(electionEventId);
            parameters.addParam(KeyParameter.EE_ALIAS.name(), electionEventAlias);
            parameters.addParam(KeyParameter.EE_ID.name(), electionEventId);
        }
        Path sdmPath = pathResolver.resolve(ConfigConstants.SDM_DIR_NAME);
        parameters.addParam(KeyParameter.SDM_PATH.name(), sdmPath.toString().replace("\\", "/"));
        if (StringUtils.isNotEmpty(privateKeyInBase64)) {
            parameters.addParam(KeyParameter.PRIVATE_KEY.name(), privateKeyInBase64);
        }
        if (StringUtils.isNotEmpty(path)) {
            parameters.addParam(KeyParameter.USB_LETTER.name(), path.replace("\\", "/"));
        }
        return parameters;
    }

    private ResponseEntity<OperationResult> handleException(Exception e, int errorCode) {
        LOGGER.error("The request can not be performed for the current resource " + errorCode, e);
        OperationResult output = new OperationResult();
        output.setError(errorCode);
        output.setException(e.getClass().getName());
        output.setMessage(e.getMessage());
        return new ResponseEntity<>(output, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    private ResponseEntity<OperationResult> handleException(int error, String message) {
        LOGGER.error("The request can not be performed for the current resource " + error + ": " + message);
        OperationResult output = new OperationResult();
        output.setError(error);
        output.setMessage(message);
        return new ResponseEntity<>(output, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    private ResponseEntity<OperationResult> handleException(ExecutionListenerImpl listener) {
        return handleException(listener.getError(), listener.getMessage());
    }

    private ResponseEntity<OperationResult> handleException(OperationsOutputCode operationsOutputCode) {
        return handleException(operationsOutputCode.value(), operationsOutputCode.getReasonPhrase());
    }
}
