/*
 * 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.au.services.domain.service;

import java.io.IOException;
import java.security.PublicKey;
import java.security.cert.Certificate;

import javax.ejb.Stateless;
import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.products.ov.au.services.domain.common.SignedObject;
import com.scytl.products.ov.au.services.domain.model.adminboard.AdminBoard;
import com.scytl.products.ov.au.services.domain.model.adminboard.AdminBoardRepository;
import com.scytl.products.ov.au.services.domain.model.authentication.AuthenticationCerts;
import com.scytl.products.ov.au.services.domain.model.authentication.AuthenticationCertsRepository;
import com.scytl.products.ov.au.services.domain.model.authentication.AuthenticationContentEntity;
import com.scytl.products.ov.au.services.domain.model.authentication.AuthenticationContentRepository;
import com.scytl.products.ov.au.services.domain.model.authentication.ElectionEventData;
import com.scytl.products.ov.au.services.domain.service.exception.ElectionEventException;
import com.scytl.products.ov.au.services.infrastructure.remote.AuRemoteCertificateService;
import com.scytl.products.ov.commons.beans.AuthenticationContextData;
import com.scytl.products.ov.commons.beans.AuthenticationVoterData;
import com.scytl.products.ov.commons.beans.domain.model.certificate.CertificateEntity;
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.ResourceNotFoundException;
import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitException;
import com.scytl.products.ov.commons.infrastructure.remote.service.RemoteCertificateService;
import com.scytl.products.ov.commons.ui.ws.rs.ErrorCodes;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.validation.ValidationResult;
import com.scytl.products.ov.commons.verify.JSONVerifier;

/**
 * Service for operating on election event data.
 */
@Stateless
public class ElectionEventService {
    
    public static final String ADMINISTRATION_BOARD_CN_PREFIX = "AdministrationBoard ";
    
    // The name of the resource handle by this web service.
    private static final String RESOURCE_NAME = "electioneventdata";

    // The name of the query parameter tenantId
    private static final String QUERY_PARAMETER_TENANT_ID = "tenantId";

    // The name of the query parameter electionEventId
    private static final String QUERY_PARAMETER_ELECTION_EVENT_ID = "electionEventId";

    // An instance of the authentication content repository
    @Inject
    private AuthenticationContentRepository authenticationContentRepository;

    // An instance of the authentication certificates repository
    @Inject
    private AuthenticationCertsRepository authenticationCertsRepository;
   
    private static final Logger LOG = LoggerFactory.getLogger("std");
    

    @Inject
    @AuRemoteCertificateService
    private RemoteCertificateService remoteCertificateService;

    @Inject
    private AdminBoardRepository adminBoardRepository;

    @Inject
    private JSONVerifier jsonVerifier;

    /**
     * Save the election event data
     * @param tenantId Id of the tenant.
     * @param electionEventId Id of the election event.
     * @param adminBoardId Id of the administrator board.
     * @param electionEventData The election data itself.
     * @throws ElectionEventException 
     * @throws IOException
     * @throws ApplicationException
     */
    public void saveElectionEventData(String tenantId, String electionEventId, String adminBoardId,
            ElectionEventData electionEventData) throws ElectionEventException, IOException, ApplicationException{
        LOG.info("Saving election event information for electionEventId: {}, and tenantId: {}.", electionEventId, tenantId);

        validateParameters(tenantId, electionEventId);

        PublicKey adminBoardPublicKey = getAdminBoardPublicKey(adminBoardId);

        try {
            saveAuthenticationContext(tenantId, electionEventId, electionEventData,
                adminBoardPublicKey);

            saveAuthenticationCertificates(tenantId, electionEventId, electionEventData,
                adminBoardPublicKey);

            saveAdminBoard(tenantId, electionEventId, adminBoardId);

            LOG.info("Election event information with electionEventId: {}, and tenantId: {} saved.", electionEventId,
                tenantId);

        } catch (DuplicateEntryException ex) {
            // debug because this exception should not be logged in production
            LOG.debug(ex.getMessage(), ex);
            LOG.warn("Duplicate entry tried to be inserted for electionEventId: {}, and tenantId: {}.", electionEventId,
                tenantId);
        }
    }

    private void saveAdminBoard(String tenantId, String electionEventId,
            String adminBoardId) throws DuplicateEntryException {
        //saving the relationship between election event and admin board
        AdminBoard adminBoard = new AdminBoard();
        adminBoard.setAdminBoardId(adminBoardId);
        adminBoard.setElectionEventId(electionEventId);
        adminBoard.setTenantId(tenantId);
        adminBoardRepository.save(adminBoard);
    }

    private void saveAuthenticationCertificates(String tenantId, String electionEventId,
            ElectionEventData electionEventData,
            PublicKey adminBoardPublicKey)
            throws JsonParseException, JsonMappingException, IOException,
            ElectionEventException, JsonProcessingException,
            DuplicateEntryException {
        AuthenticationCerts authenticationCerts = new AuthenticationCerts();
        authenticationCerts.setTenantId(tenantId);
        authenticationCerts.setElectionEventId(electionEventId);

        String signedAuthenticationVoterData = electionEventData.getAuthenticationVoterData();
        SignedObject signedAuthenticationVoterDataObject =
            ObjectMappers.fromJson(signedAuthenticationVoterData, SignedObject.class);

        String signatureAuthenticationVoterData = signedAuthenticationVoterDataObject.getSignature();
        AuthenticationVoterData authenticationVoterData = null;
        try {
            LOG.info("Verifying authentication voter data configuration signature");
            authenticationVoterData =
                    jsonVerifier.verify(adminBoardPublicKey, signatureAuthenticationVoterData, AuthenticationVoterData.class);
            LOG.info("Authentication voter data configuration signature was successfully verified");
        } catch (Exception e) {
            LOG.error("Authentication voter data configuration signature could not be verified", e);
            throw new ElectionEventException(e);
        }
        String authenticationVoterDataJSON = ObjectMappers.toJson(authenticationVoterData);
        authenticationCerts.setJson(authenticationVoterDataJSON);
        authenticationCerts.setSignature(signatureAuthenticationVoterData);
        authenticationCertsRepository.save(authenticationCerts);
    }

    private void saveAuthenticationContext(String tenantId, String electionEventId,
            ElectionEventData electionEventData,
            PublicKey adminBoardPublicKey)
            throws JsonParseException, JsonMappingException, IOException,
            ElectionEventException, JsonProcessingException,
            DuplicateEntryException {
        AuthenticationContentEntity authenticationContent = new AuthenticationContentEntity();
        authenticationContent.setTenantId(tenantId);
        authenticationContent.setElectionEventId(electionEventId);

        String signedAuthenticationContextData = electionEventData.getAuthenticationContextData();

        SignedObject signedAuthenticationContextDataObject =
            ObjectMappers.fromJson(signedAuthenticationContextData, SignedObject.class);
        String signatureAuthenticationContextData = signedAuthenticationContextDataObject.getSignature();

        AuthenticationContextData authenticationContextData = null;
        try {
            LOG.info("Verifying authentication context configuration signature");
            authenticationContextData =
                    jsonVerifier.verify(adminBoardPublicKey, signatureAuthenticationContextData,
                    AuthenticationContextData.class);
            LOG.info("Authentication context configuration signature was successfully verified");
        } catch (Exception e) {
            LOG.error("Authentication context configuration signature could not be verified", e);
            throw new ElectionEventException(e);
        }

        String authenticationContextDataJSON = ObjectMappers.toJson(authenticationContextData);
        authenticationContent.setJson(authenticationContextDataJSON);
        authenticationContentRepository.save(authenticationContent);
    }

    private PublicKey getAdminBoardPublicKey(String adminBoardId)
            throws ElectionEventException {
        LOG.info("Fetching the administration board certificate");
        String adminBoardCommonName = ADMINISTRATION_BOARD_CN_PREFIX + adminBoardId;

	    Certificate adminBoardCert = null;
        try {
        	CertificateEntity adminBoardCertificateEntity =
        			remoteCertificateService.getAdminBoardCertificate(adminBoardCommonName);
	        String adminBoardCertPEM = adminBoardCertificateEntity.getCertificateContent();
            adminBoardCert = PemUtils.certificateFromPem(adminBoardCertPEM);
        } catch (GeneralCryptoLibException | RetrofitException e) {
            LOG.error("An error occurred while fetching the administration board certificate", e);
            throw new ElectionEventException(e);
        }
        return adminBoardCert.getPublicKey();
    }

    // Validate parameters.
    private void validateParameters(String tenantId, String electionEventId) throws ApplicationException {
        if (tenantId == null || tenantId.isEmpty()) {
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_QUERY_PARAMETER_IS_NULL,
                RESOURCE_NAME, ErrorCodes.MISSING_QUERY_PARAMETER, QUERY_PARAMETER_TENANT_ID);
        }

        if (electionEventId == null || electionEventId.isEmpty()) {
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_QUERY_PARAMETER_IS_NULL,
                RESOURCE_NAME, ErrorCodes.MISSING_QUERY_PARAMETER, QUERY_PARAMETER_ELECTION_EVENT_ID);
        }
    }
    /**
     * function for checking if the election event contains sufficient information.
     * @param tenantId Id of the tenant.
     * @param electionEventId Id of the election.
     * @return The result of the check, as a ValidationResult containing true or false.
     */
    public ValidationResult checkIfElectionEventDataIsEmpty(String tenantId, String electionEventId){
        ValidationResult validationResult = new ValidationResult();
        validationResult.setResult(Boolean.FALSE);
        try {
            authenticationContentRepository.findByTenantIdElectionEventId(tenantId, electionEventId);
            authenticationCertsRepository.findByTenantIdElectionEventId(tenantId, electionEventId);
        } catch (ResourceNotFoundException e) {
            // debug because this is indeed not an exception
            LOG.debug(e.getMessage(), e);
            validationResult.setResult(Boolean.TRUE);
        }
        return validationResult;
    }
}
