/*
 * 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.ea.domain.model.extendedauthentication.validation;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ejb.Stateless;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Collection;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
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.ov.commons.beans.domain.model.authentication.AuthenticationToken;
import com.scytl.products.ov.commons.beans.domain.model.authentication.ExtendedAuthenticationUpdate;
import com.scytl.products.ov.commons.beans.domain.model.authentication.ExtendedAuthenticationUpdateRequest;
import com.scytl.products.ov.commons.beans.exceptions.ApplicationException;
import com.scytl.products.ov.commons.beans.exceptions.AuthTokenRepositoryException;
import com.scytl.products.ov.commons.beans.exceptions.AuthTokenValidationException;
import com.scytl.products.ov.commons.beans.exceptions.ExtendedAuthValidationException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.validation.ValidationErrorType;
import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitConsumer;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.validation.ValidationResult;
import com.scytl.products.ov.commons.verify.JSONVerifier;
import com.scytl.products.ov.ea.domain.model.extendedauthentication.ExtendedAuthentication;
import com.scytl.products.ov.ea.domain.model.extendedauthentication.validation.rules.CertificateValidation;
import com.scytl.products.ov.ea.services.infrastructure.logging.ExtendedAuthenticationLogConstants;
import com.scytl.products.ov.ea.services.infrastructure.logging.ExtendedAuthenticationLogEvents;
import com.scytl.products.ov.ea.services.infrastructure.persistence.ExtendedAuthenticationService;
import com.scytl.products.ov.ea.services.infrastructure.remote.client.AuthenticationClient;
import com.scytl.products.ov.ea.services.infrastructure.remote.client.CertificateChainValidationRequest;

import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitException;

/**
 * This service provides the functionality to validateToken a authentication token based on the predefined rules.
 */
@Stateless
public class ExtendedAuthValidationServiceImpl implements ExtendedAuthValidationService {

    private final Collection<CertificateValidation> validations = new ArrayList<>();

    @Inject
    private AuthenticationTokenService authenticationTokenService;

    @Inject
    private ExtendedAuthenticationService extendedAuthenticationService;

    private static final Logger LOG = LoggerFactory.getLogger("std");
    
    @Inject
    protected SecureLoggingWriter secureLoggingWriter;

    @Inject
    private TrackIdInstance trackIdInstance;

    private static final JSONVerifier jsonVerifier = new JSONVerifier();

    private AuthenticationClient auRestClient;

    @Inject
    public ExtendedAuthValidationServiceImpl(final AuthenticationClient auRestClient) {
        this.auRestClient = auRestClient;
    }
    
    @Inject
    @Any
    void setValidations(Instance<CertificateValidation> instance) {
        for (CertificateValidation validation : instance) {
            validations.add(validation);
        }
    }

    /**
     * @see ExtendedAuthValidationService#validateToken(String, String, AuthenticationToken)
     */
    @Override
    public boolean validateToken(String tenantId, String electionEventId, AuthenticationToken authenticationToken)
            throws AuthTokenValidationException {

    	try {

			final ValidationResult validationResult = authenticationTokenService.validateToken(tenantId,
					electionEventId, authenticationToken);

			if (!validationResult.isResult()) {
				LOG.error("Error validating the token");
				secureLoggingWriter.log(Level.ERROR, new LogContent.LogContentBuilder()
						.logEvent(ExtendedAuthenticationLogEvents.EARENEW_ERROR_INVALID_VALIDATION_TOKEN).objectId("")
						.user(authenticationToken.getVoterInformation().getCredentialId())
						.electionEvent(electionEventId)
						.additionalInfo(ExtendedAuthenticationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
						.additionalInfo(ExtendedAuthenticationLogConstants.ERR_DESC, "Error validating the token").createLogInfo());

				throw new AuthTokenValidationException(ValidationErrorType.FAILED);
			}
		
    	} catch (AuthTokenRepositoryException e) {
    		LOG.error("Error validating the token", e);
			throw new AuthTokenValidationException(ValidationErrorType.FAILED);
		}
        return true;
    }

    /**
     * @see ExtendedAuthValidationService#validateCertificate(String, AuthenticationToken)
     */
    @Override
    public boolean validateCertificate(String certificate, AuthenticationToken authenticationToken)
            throws ExtendedAuthValidationException {
        for (CertificateValidation validation : validations) {
            validation.validateCertificate(certificate, authenticationToken);
        }
        return true;
    }

    /**
     * @see ExtendedAuthValidationService#verifySignature(ExtendedAuthenticationUpdateRequest, AuthenticationToken)
     *      (ExtendedAuthenticationUpdateRequest) (String, AuthenticationToken) This verification comes from a plain
     *      json signed in javascript. In this scenario is possible to simply deserialize the object using directly the
     *      map that the signature contains. In other scenarios may be better to include the whole signed object in a
     *      single entry of the map(e.g. "signedObject") , and during the verifying process try to claim the object
     *      using that key
     */
    @Override
    public ExtendedAuthenticationUpdate verifySignature(
            ExtendedAuthenticationUpdateRequest extendedAuthenticationUpdateRequest,
            AuthenticationToken authenticationToken) throws ExtendedAuthValidationException {

        try {
            final Certificate certificate =
                PemUtils.certificateFromPem(extendedAuthenticationUpdateRequest.getCertificate());

            final ExtendedAuthenticationUpdate verify = jsonVerifier.verifyFromMap(certificate.getPublicKey(),
                    extendedAuthenticationUpdateRequest.getSignature(), ExtendedAuthenticationUpdate.class);

            if (!verify.getAuthenticationTokenSignature().equals(authenticationToken.getSignature())) {

                String errMsg = "The signature has not been properly verified";
                LOG.error(errMsg);
                secureLoggingWriter.log(Level.ERROR,
                    new LogContent.LogContentBuilder()
                        .logEvent(ExtendedAuthenticationLogEvents.EARENEW_ERROR_INVALID_VALIDATION_SIGNATURE)
                        .objectId("").user(authenticationToken.getVoterInformation().getCredentialId())
                        .electionEvent(authenticationToken.getVoterInformation().getElectionEventId())
                        .additionalInfo(ExtendedAuthenticationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                        .additionalInfo(ExtendedAuthenticationLogConstants.ERR_DESC, errMsg).createLogInfo());

                throw new ExtendedAuthValidationException(ValidationErrorType.INVALID_SIGNATURE);
            }
            return verify;

        } catch (GeneralCryptoLibException e) {
            LOG.error("error reading the PEM certificate");
            throw new ExtendedAuthValidationException(ValidationErrorType.INVALID_CERTIFICATE, e);
        } catch (SignatureException | IllegalStateException | UnsupportedJwtException | ExpiredJwtException e) {
            LOG.error("The signature has not been properly verified", e);
            throw new ExtendedAuthValidationException(ValidationErrorType.INVALID_SIGNATURE, e);
        }

    }

    /**
     * @see ExtendedAuthValidationService#validateTokenWithAuthIdAndCredentialId(AuthenticationToken,
     *      ExtendedAuthenticationUpdate, String, String)
     */
    @Override
    public void validateTokenWithAuthIdAndCredentialId(AuthenticationToken authenticationToken,
            ExtendedAuthenticationUpdate extendedAuthenticationUpdate, String tenantId, String electionEventId)
            throws ResourceNotFoundException, ApplicationException, ExtendedAuthValidationException {

        final ExtendedAuthentication extendedAuthentication =
            extendedAuthenticationService.retrieveExistingExtendedAuthenticationForRead(tenantId,
                extendedAuthenticationUpdate.getOldAuthID(), electionEventId);
        if (!extendedAuthentication.getCredentialId()
            .equals(authenticationToken.getVoterInformation().getCredentialId())) {
            String errMsg = "INVALID CREDENTIAL ID IN AUTHID";
            LOG.error(errMsg);
            secureLoggingWriter.log(Level.ERROR, new LogContent.LogContentBuilder()
                .logEvent(ExtendedAuthenticationLogEvents.EARENEW_ERROR_INVALID_VALIDATION_CREDENTIAL_ID).objectId("")
                .user(authenticationToken.getVoterInformation().getCredentialId()).electionEvent(electionEventId)
                .additionalInfo(ExtendedAuthenticationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                .additionalInfo(ExtendedAuthenticationLogConstants.ERR_DESC, errMsg).createLogInfo());
            throw new ExtendedAuthValidationException(ValidationErrorType.INVALID_CREDENTIAL_ID_IN_AUTHID);
        }

    }

    /**
     * @inheritDoc
     */
    @Override
    public void validateCertificateChain(String tenantId, String electionEventId, String certificate,
            AuthenticationToken authenticationToken) throws ExtendedAuthValidationException {

        CertificateChainValidationRequest certificateChainValidationRequest = new CertificateChainValidationRequest();
        certificateChainValidationRequest.setCertificateContent(certificate);

        try {
            ValidationResult validationResult = RetrofitConsumer.processResponse(auRestClient.validateCertificateChain(tenantId, electionEventId,
                certificateChainValidationRequest));

            if (validationResult.isResult()) {
                LOG.info("The certificate chain is VALID.");
            } else {
                String errMsg = "The certificate chain is INVALID.";
                LOG.error(errMsg);
                secureLoggingWriter.log(Level.ERROR,
                    new LogContent.LogContentBuilder()
                        .logEvent(ExtendedAuthenticationLogEvents.EARENEW_ERROR_INVALID_VALIDATION_CERTIFICATE_CHAIN)
                        .objectId("").user(authenticationToken.getVoterInformation().getCredentialId())
                        .electionEvent(electionEventId)
                        .additionalInfo(ExtendedAuthenticationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                        .additionalInfo(ExtendedAuthenticationLogConstants.ERR_DESC, errMsg).createLogInfo());
                throw new ExtendedAuthValidationException(ValidationErrorType.INVALID_CERTIFICATE_CHAIN);
            }

        } catch (RetrofitException error) {
            String errMsg = "Validate certificate chain failed: " + error;
            LOG.error(errMsg);
            throw new ExtendedAuthValidationException(ValidationErrorType.FAILED);
        }
    }
}
