/*
 * 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.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

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.sdm.application.log.SdmSecureLogEvent;
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.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.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.products.ov.commons.beans.exceptions.ApplicationException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.config.shares.exception.SharesException;
import com.scytl.products.ov.sdm.application.service.AdminBoardService;
import com.scytl.products.ov.sdm.domain.model.administrationauthority.ActivateOutputData;
import com.scytl.products.ov.sdm.domain.model.administrationauthority.ConstituteOutcomeMessages;
import com.scytl.products.ov.sdm.domain.model.administrationauthority.ReadShareInputData;
import com.scytl.products.ov.sdm.domain.model.administrationauthority.ReadShareOutputData;
import com.scytl.products.ov.sdm.domain.model.administrationauthority.ReconstructInputData;
import com.scytl.products.ov.sdm.domain.model.administrationauthority.ReconstructOutputData;
import com.scytl.products.ov.sdm.domain.model.administrationauthority.WriteShareInputData;
import com.scytl.products.ov.sdm.domain.model.status.SmartCardStatus;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;

/**
 * The admin board end-point.
 */
@RestController
@RequestMapping("/sdm-ws-rest/adminboards")
@Api(value = "Administration Board REST API", description = "")
public class AdminAuthorityResource {

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

	@Autowired
	private AdminBoardService adminBoardService;

	@Autowired
	private TransactionInfoProvider transactionInfoProvider;

	@Autowired
	private SecureLoggingWriter secureLogger;

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

	/**
	 * Returns a list of all admin boards.
	 *
	 * @return The list of admin boards.
	 */
	@RequestMapping(method = RequestMethod.GET, produces = "application/json")
	@ResponseBody
	@ApiOperation(value = "List administration boards", response = String.class, notes = "Service to retrieve the list of adminitration boards.")
	public String getAdminBoards() {
		return adminBoardService.getAdminBoards();
	}

	/**
	 * Execute the constitute action: create keypair, split private key on shares and keep them in memory. It also puts the
	 * public key into a CSR.
	 *
	 * @param adminBoardId the admin board ID.
	 * @param file - file containing the keystore
	 * @param keystorePassword - password to open keystore
	 */
	@RequestMapping(value = "/constitute/{adminBoardId}", method = RequestMethod.POST)
	@ApiOperation(value = "Constitute Service", notes = "Service to generate a key pair and splits the private key into shares.", response = Void.class)
	public ResponseEntity<Void> constitute(
			@ApiParam(value = "String", required = true) @PathVariable final String adminBoardId,
			@ApiParam(value ="file", required = true) @RequestParam("file")  MultipartFile file,
			@ApiParam(value = "keyStorePassword", required = true) @RequestParam("keystorePassword") final char[] keystorePassword)
					throws ResourceNotFoundException, SharesException, IOException {

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

		secureLogger.log(Level.INFO, new LogContent.LogContentBuilder()
				.logEvent(SdmSecureLogEvent.ADMIN_BOARD_CONSTITUTING)
				.objectId(adminBoardId).user(adminBoardId)
				.createLogInfo());

		try(InputStream in = file.getInputStream()){

			adminBoardService.constitute(adminBoardId, in, keystorePassword);

		} catch (ResourceNotFoundException | SharesException | IOException ex) {

			secureLogger.log(Level.ERROR, new LogContent.LogContentBuilder()
					.logEvent(SdmSecureLogEvent.ADMIN_BOARD_CONSTITUTION_FAILED)
					.objectId(adminBoardId).user(adminBoardId)
					.additionalInfo("err_desc", ex.getMessage()).createLogInfo());

			throw ex;
		}

		secureLogger.log(Level.INFO, new LogContent.LogContentBuilder()
				.logEvent(SdmSecureLogEvent.ADMIN_BOARD_CONSTITUTION_COMPLETED_SUCCESSFULLY)
				.objectId(adminBoardId).user(adminBoardId)
				.createLogInfo());

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

	/**
	 * Check smartcard reader status: it can be EMPTY or INSERTED.
	 *
	 * @return STATUS
	 */
	@RequestMapping(value = "/shares/status", method = RequestMethod.GET)
	@ApiOperation(value = "Check smartcard reader status", notes = "Checks the smartcard reader status.", response = SmartCardStatus.class)
	public ResponseEntity<SmartCardStatus> status() {

		SmartCardStatus status = adminBoardService.getSmartCardReaderStatus();

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

	/**
	 * Write share for the corresponding member.
	 *
	 * @return STATUS
	 */
	@RequestMapping(value = "/{adminBoardId}/shares/{shareNumber}", method = RequestMethod.POST)
	@ApiOperation(value = "Write share", notes = "Writes a the share specified by the share number into the smartcard", response = Void.class)
	public ResponseEntity<Void> writeShare(
			@ApiParam(value = "String", required = true) @PathVariable final String adminBoardId,
			@ApiParam(value = "Integer", required = true) @PathVariable final Integer shareNumber,
			@ApiParam(value = "WriteShareInputData", required = true) @RequestBody final WriteShareInputData inputData)
					throws ResourceNotFoundException, SharesException, IOException, ApplicationException {

		adminBoardService.writeShare(adminBoardId, shareNumber, inputData.getPin());

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

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

		ConstituteOutcomeMessages constituteOutcomeMessages = new ConstituteOutcomeMessages();
		List<String> messages = new ArrayList<String>();
		messages.add(exception.getMessage());
		constituteOutcomeMessages.setMessages(messages);

		return new ResponseEntity<>(constituteOutcomeMessages, HttpStatus.PRECONDITION_FAILED);
	}

	/**
	 * Executes the activate action.
	 */
	@RequestMapping(value = "/{adminBoardId}/activate", method = RequestMethod.POST)
	@ApiOperation(value = "Activate", notes = "Starts the process to activate an administration" +
		" board. The issuer and the subject public key are returned.", response = ActivateOutputData.class)
	public ResponseEntity<ActivateOutputData>
			activate(@ApiParam(value = "String", required = true) @PathVariable String adminBoardId)
			throws GeneralCryptoLibException {

		// transaction id generation
		transactionInfoProvider.generate(tenantId, "", "");

		secureLogger.log(Level.INFO, new LogContent.LogContentBuilder()
				.logEvent(SdmSecureLogEvent.ADMIN_BOARD_ACTIVATING)
				.objectId(adminBoardId).user(adminBoardId)
				.createLogInfo());

		try {

			ActivateOutputData output = adminBoardService.activate(adminBoardId);
			return new ResponseEntity<>(output, HttpStatus.OK);

		} catch (GeneralCryptoLibException ex) {

			secureLogger.log(Level.ERROR, new LogContent.LogContentBuilder()
					.logEvent(SdmSecureLogEvent.ADMIN_BOARD_ACTIVATING_FAILED)
					.objectId(adminBoardId).user(adminBoardId)
					.additionalInfo("err_desc", ex.getMessage()).createLogInfo());
			throw ex;
		}

	}

	/**
	 * Executes the read share action, and returns the serialized share in Base64 encoded format.
	 */
	@RequestMapping(value = "/{adminBoardId}/read/{shareNumber}", method = RequestMethod.POST)
	@ApiOperation(value = "Read share", notes = "Service to read a share and returns it serialized.", response = ReadShareOutputData.class)
	public ResponseEntity<ReadShareOutputData> readShare(
			@ApiParam(value = "String", required = true) @PathVariable String adminBoardId,
			@ApiParam(value = "Integer", required = true) @PathVariable Integer shareNumber,
			@ApiParam(value = "ReadShareInputData", required = true) @RequestBody ReadShareInputData inputData) {

		try {
			String share =
				adminBoardService.readShare(adminBoardId, shareNumber, inputData.getPin(), inputData.getPublicKeyPEM());

			ReadShareOutputData outputData = new ReadShareOutputData();
			outputData.setSerializedShare(share);

			return new ResponseEntity<>(outputData, HttpStatus.OK);
		} catch (ResourceNotFoundException e) {
			String errorMessage = "Missing information required to read admin board share";
			logError(e, errorMessage, adminBoardId);
			return new ResponseEntity<>(HttpStatus.NOT_FOUND);
		} catch (SharesException e) {
			String errorMessage = "Error occurred reading the admin board share";
			LOGGER.error(errorMessage, e);
			return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
		} catch (IllegalArgumentException e) {
			String errorMessage = "Invalid input data to read admin board share";
			LOGGER.error(errorMessage, e);
			return new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED);
		}
	}

	private void logError(final ResourceNotFoundException e, final String errorMessage, final String adminBoardId) {

		// transaction id generation
		transactionInfoProvider.generate(tenantId, "", "");

		secureLogger.log(Level.ERROR, new LogContent.LogContentBuilder()
				.logEvent(SdmSecureLogEvent.ADMIN_BOARD_ACTIVATING_FAILED)
				.objectId(adminBoardId).user(adminBoardId)
				.additionalInfo("err_desc", e.getMessage()).createLogInfo());
		LOGGER.error(errorMessage, e);
	}

	/**
	 * Executes the reconstruct action, given a list of serialized admin board shares.
	 */
	@RequestMapping(value = "/{adminBoardId}/reconstruct", method = RequestMethod.POST)
	@ApiOperation(value = "Reconstruct", notes = "Reconstructs the private key of an administration board, " +
		"given a list of serialized private keys.", response = ReconstructOutputData.class)
	public ResponseEntity<ReconstructOutputData> reconstruct(
			@ApiParam(value = "String", required = true) @PathVariable String adminBoardId,
			@ApiParam(value = "ReconstructInputData", required = true) @RequestBody ReconstructInputData inputData)
					throws ResourceNotFoundException, SharesException, IOException, GeneralCryptoLibException {
		String privateKeyPEM;

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

		try {
			 privateKeyPEM = adminBoardService.reconstruct(adminBoardId, inputData.getSerializedShares(),
					inputData.getSerializedPublicKey());
		} catch ( SharesException | GeneralCryptoLibException ex) {
			secureLogger.log(Level.INFO, new LogContent.LogContentBuilder()
					.logEvent(SdmSecureLogEvent.ADMIN_BOARD_ACTIVATING_FAILED)
					.objectId(adminBoardId).user(adminBoardId)
					.additionalInfo("err_desc", ex.getMessage()).createLogInfo());
			throw ex;
		}

		ReconstructOutputData output = new ReconstructOutputData();

		output.setSerializedPrivateKey(privateKeyPEM);

		secureLogger.log(Level.INFO, new LogContent.LogContentBuilder()
				.logEvent(SdmSecureLogEvent.ADMIN_BOARD_ACTIVATED_SUCCESSFULLY)
				.objectId(adminBoardId).user(adminBoardId)
				.createLogInfo());

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



}
