/*
 * 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.ui.ws.rs.application.operation;

import java.io.IOException;
import java.security.cert.X509Certificate;

import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import com.fasterxml.jackson.core.JsonProcessingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.certificates.factory.CryptoX509Certificate;
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.oscore.logging.core.provider.TransactionInfoProvider;
import com.scytl.products.ov.au.services.domain.model.authentication.AuthenticationTokenFactory;
import com.scytl.products.ov.au.services.domain.model.authentication.AuthenticationTokenMessage;
import com.scytl.products.ov.au.services.domain.model.challenge.ChallengeInformation;
import com.scytl.products.ov.au.services.domain.model.validation.CertificateChainValidationRequest;
import com.scytl.products.ov.au.services.domain.service.certificate.CertificateChainValidationService;
import com.scytl.products.ov.au.services.domain.service.challenge.ChallengeInformationValidationService;
import com.scytl.products.ov.au.services.domain.service.exception.AuthenticationTokenGenerationException;
import com.scytl.products.ov.au.services.domain.service.exception.AuthenticationTokenSigningException;
import com.scytl.products.ov.au.services.infrastructure.log.AuthenticationLogConstants;
import com.scytl.products.ov.au.services.infrastructure.log.AuthenticationLogEvents;
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.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.exceptions.SemanticErrorException;
import com.scytl.products.ov.commons.beans.exceptions.SyntaxErrorException;
import com.scytl.products.ov.commons.beans.validation.CryptographicOperationException;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.ui.Constants;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.util.HttpRequestService;
import com.scytl.products.ov.commons.util.ValidationUtils;
import com.scytl.products.ov.commons.validation.ValidationResult;

/**
 * The end point for authentication token resource.
 */
@Path("/tokens")
@Stateless
public class AuthenticationTokenResource {

    // The name of the parameter value tenant id.
    private static final String PARAMETER_VALUE_TENANT_ID = "tenantId";

    // The name of the parameter value credential id.
    private static final String PARAMETER_VALUE_CREDENTIAL_ID = "credentialId";

    // The name of the parameter value election event id.
    private static final String PARAMETER_VALUE_ELECTION_EVENT_ID = "electionEventId";

    // The error logger.
    private static final Logger LOG = LoggerFactory.getLogger("std");

    // The track id instance
    @Inject
    private TrackIdInstance trackIdInstance;

    // An instance of the authentication token factory
    @Inject
    private AuthenticationTokenFactory authenticationTokenFactory;

    // An instance of the challenge information validation service
    @Inject
    private ChallengeInformationValidationService challengeInformationValidationService;

    // An instance of the certificate chain validation service
    @Inject
    private CertificateChainValidationService certificateChainValidationSerivce;

    @Inject
    private TransactionInfoProvider transactionInfoProvider;

    @Inject
    private HttpRequestService httpRequestService;

    @Inject
    private SecureLoggingWriter secureLoggerWriter;

    /**
     * Returns a AuthenticationToken object which matches with the provided parameters.
     * 
     * @param trackingId
     *            - the track id to be used for logging purposes.
     * @param tenantId
     *            - the tenant identifier.
     * @param electionEventId
     *            - the election event identifier.
     * @param credentialId
     *            - the credential identifier.
     * @param challengeInformation
     *            - the challenge information including client and server challenge messages.
     * @param request
     *            - the http servlet request.
     * @return If the operation is successfully performed, returns a response with HTTP status code 200 and the
     *         authentication token in json format.
     * @throws ApplicationException
     * @throws SemanticErrorException
     * @throws SyntaxErrorException
     * @throws CryptographicOperationException
     * @throws ResourceNotFoundException
     * @throws IOException
     */

    @Path("/tenant/{tenantId}/electionevent/{electionEventId}/credential/{credentialId}")
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response getAuthenticationToken(@HeaderParam(Constants.PARAMETER_HEADER_X_REQUEST_ID) String trackingId,
            @PathParam(PARAMETER_VALUE_TENANT_ID) final String tenantId,
            @PathParam(PARAMETER_VALUE_ELECTION_EVENT_ID) final String electionEventId,
            @PathParam(PARAMETER_VALUE_CREDENTIAL_ID) final String credentialId,
            @NotNull final ChallengeInformation challengeInformation, @Context final HttpServletRequest request)
            throws AuthenticationTokenGenerationException, AuthenticationTokenSigningException, ApplicationException,
            SyntaxErrorException, SemanticErrorException, ResourceNotFoundException, CryptographicOperationException,
            IOException {
        // set the track id to be logged
        trackIdInstance.setTrackId(trackingId);

        // transaction id generation
        transactionInfoProvider.generate(tenantId, httpRequestService.getIpClientAddress(request),
            httpRequestService.getIpServer(request));

        // in case something from the input is not valid, it throws an exception
        validateInput(tenantId, electionEventId, credentialId);

        // validate input challengeInformation for syntax or semantic errors
        ValidationUtils.validate(challengeInformation);

        // get certificate and certificate subject common name.
        String certificatePem = challengeInformation.getCertificate();
        CryptoX509Certificate cryptoCertificate;
        try {
            cryptoCertificate =
                new CryptoX509Certificate((X509Certificate) PemUtils.certificateFromPem(certificatePem));
        } catch (GeneralCryptoLibException e) {
            LOG.error("Cryptographic error when opening credential ID authorization certificate: ", e);
            return Response.status(Status.PRECONDITION_FAILED).build();
        }
        String subjectCommonName = cryptoCertificate.getSubjectDn().getCommonName();

        // validate subject common name.
        if (!subjectCommonName.contains(challengeInformation.getCredentialId())) {
            LOG.error("Validation of credential ID authorization certificate subject common name failed.");
            secureLoggerWriter.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(AuthenticationLogEvents.CLIENT_CERTIFICATE_NAME_MISMATCH)
                    .electionEvent(electionEventId)
                    .additionalInfo(AuthenticationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .createLogInfo());
            return Response.status(Status.PRECONDITION_FAILED).build();
        }

        ValidationResult certChainValidationResult =
            certificateChainValidationSerivce.validate(tenantId, electionEventId, certificatePem);

        // if the certificate chain is successfully validated, then validate
        // challenge and generate the token
        if (certChainValidationResult.isResult()) {

            secureLoggerWriter.log(Level.INFO, new LogContent.LogContentBuilder()
                .logEvent(AuthenticationLogEvents.CERTIFICATE_CHAIN_VALIDATION_SUCCESS).electionEvent(electionEventId)
                .additionalInfo(AuthenticationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                .createLogInfo());

            // validate challenge
            ValidationResult challengeValidationResult = challengeInformationValidationService.validate(tenantId,
                electionEventId, credentialId, challengeInformation);

            // if the challenge is successfully validated, then generate the
            // token
            if (challengeValidationResult.isResult()) {
                // generate authentication token
                AuthenticationTokenMessage authenticationTokenMessage =
                    authenticationTokenFactory.buildAuthenticationToken(tenantId, electionEventId, credentialId);
                // returns the result of operation
                String json = ObjectMappers.toJson(authenticationTokenMessage);
                return Response.ok(json).build();
            } else {
                LOG.error("Validation of credential ID authorization challenge failed.");
                return Response.status(Status.PRECONDITION_FAILED).build();
            }
        } else {
            LOG.error("Validation of credential ID authorization certificate chain failed.");
            secureLoggerWriter.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(AuthenticationLogEvents.CERTIFICATE_CHAIN_VALIDATION_FAILED)
                    .electionEvent(electionEventId)
                    .additionalInfo(AuthenticationLogConstants.INFO_TRACK_ID, trackIdInstance.getTrackId())
                    .createLogInfo());
            return Response.status(Status.PRECONDITION_FAILED).build();
        }
    }

    // Does a basic validation of the input. In case something is wrong, just
    // throws an exception.
    private void validateInput(final String tenantId, final String electionEventId, final String credentialId)
            throws ApplicationException {
        if (tenantId == null || tenantId.isEmpty()) {
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_TENANT_ID_IS_NULL);
        }
        if (electionEventId == null || electionEventId.isEmpty()) {
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_ELECTION_EVENT_ID_IS_NULL);
        }
        if (credentialId == null || credentialId.isEmpty()) {
            throw new ApplicationException(ApplicationExceptionMessages.EXCEPTION_MESSAGE_CREDENTIAL_ID_IS_NULL);
        }
    }

    @Path("/tenant/{tenantId}/electionevent/{electionEventId}/chain/validate")
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response validateCertificateChain(@PathParam(PARAMETER_VALUE_TENANT_ID) final String tenantId,
            @PathParam(PARAMETER_VALUE_ELECTION_EVENT_ID) final String electionEventId,
            @NotNull final CertificateChainValidationRequest certificateChainValidationRequest) throws JsonProcessingException {

        // validate certificate chain
        ValidationResult certChainValidationResult = certificateChainValidationSerivce.validate(tenantId,
            electionEventId, certificateChainValidationRequest.getCertificateContent());

        String json = ObjectMappers.toJson(certChainValidationResult);

        if (certChainValidationResult.isResult()) {
            LOG.info("Requested certificate chain verification has passed: VALID CHAIN.");
        } else {
            LOG.warn("Requested certificate chain verification has not passed: INVALID CHAIN.");
        }

        return Response.ok(json).build();
    }
}
