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

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

import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.persistence.PersistenceException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

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

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
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.domain.LogEvent;
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.DuplicateEntryException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.validation.CryptographicOperationException;
import com.scytl.products.ov.commons.beans.validation.ValidationErrorType;
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.validation.ValidationError;
import com.scytl.products.ov.commons.validation.ValidationResult;
import com.scytl.products.ov.cr.services.domain.model.certificate.CertificateEntity;
import com.scytl.products.ov.cr.services.domain.model.certificate.CertificateRepository;
import com.scytl.products.ov.cr.services.domain.model.platform.CrCertificateValidationService;
import com.scytl.products.ov.cr.services.infrastructure.log.CertificateRegistryLogConstants;
import com.scytl.products.ov.cr.services.infrastructure.log.CertificateRegistryLogEvents;
import com.scytl.products.ov.cr.services.utils.CertificateTools;
import com.scytl.products.ov.commons.beans.validation.CertificateValidationResult;
import com.scytl.products.ov.commons.beans.validation.CertificateValidationService;

@Path(CertificateRegistryResource.RESOURCE_PATH)
@Stateless(name = "cert-CertificatesDataResource")
public class CertificateRegistryResource {

    public static final String RESOURCE_PATH = "certificates/public";

    public static final String SAVE_CERTIFICATE = "secured";

    public static final String SAVE_CERTIFICATE_FOR_TENANT = "secured/tenant/{tenantId}";

    public static final String SAVE_CERTIFICATE_FOR_ELECTION_EVENT = "secured/tenant/{tenantId}/electionevent/{electionEventId}";

    public static final String GET_CERTIFICATE = "name/{certificateName}";

    public static final String GET_CERTIFICATE_FOR_TENANT = "tenant/{tenantId}/name/{certificateName}";

    public static final String GET_CERTIFICATE_FOR_TENANT_EEID =
            "tenant/{tenantId}/electionevent/{electionEventId}/name/{certificateName}";

    public static final String CHECK_IF_CERTIFICATE_EXIST = "tenant/{tenantId}/name/{certificateName}/status";

    // 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";

    private static final String QUERY_PARAMETER_CERTIFICATE_NAME = "certificateName";

    private static final Logger LOG = LoggerFactory.getLogger(CertificateRegistryResource.class);

    @Inject
    private CertificateRepository certificateRepository;

    @Inject
    @CrCertificateValidationService
    private CertificateValidationService certificateValidationService;

    @Inject
    private SecureLoggingWriter secureLoggingWriter;

    @Inject
    private TransactionInfoProvider transactionInfoProvider;

    @Inject
    private HttpRequestService httpRequestService;

    @Inject
    private TrackIdInstance trackIdInstance;

    /**
     * Saves a certificate in the repository
     *
     * @param tenantId
     *            - tenant identifier
     * @param electionEventId
     *            - electionEvent identifier
     * @param certificateReader
     *            - content of the certificate
     * @return
     * @throws DuplicateEntryException
     * @throws ResourceNotFoundException
     * @throws CryptographicOperationException
     */
    @POST
    @Path(SAVE_CERTIFICATE_FOR_ELECTION_EVENT)
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response saveCertificate(@HeaderParam(Constants.PARAMETER_HEADER_X_REQUEST_ID) String trackingId,
                                    @PathParam(QUERY_PARAMETER_TENANT_ID) String tenantId,
                                    @PathParam(QUERY_PARAMETER_ELECTION_EVENT_ID) String electionEventId, @NotNull Reader certificateReader,
                                    @Context HttpServletRequest servletRequest)
            throws DuplicateEntryException, ResourceNotFoundException, CryptographicOperationException {

        trackIdInstance.setTrackId(trackingId);

        transactionInfoProvider.generate(tenantId, httpRequestService.getIpClientAddress(servletRequest),
                httpRequestService.getIpServer(servletRequest));

        Response response;
        try {
            CertificateEntity certificateEntity = ObjectMappers.fromJson(certificateReader, CertificateEntity.class);
            certificateEntity.setElectionEventId(electionEventId);
            certificateEntity.setTenantId(tenantId);
            Certificate certificateToValidate = PemUtils.certificateFromPem(certificateEntity.getCertificateContent());
            CryptoAPIX509Certificate cryptoAPIX509Certificate =
                    CertificateTools.getCryptoX509Certificate(certificateToValidate);
            String subjectCommonName = cryptoAPIX509Certificate.getSubjectDn().getCommonName();
            String signerCommonName = cryptoAPIX509Certificate.getIssuerDn().getCommonName();
            CertificateEntity signerEntity = certificateRepository.findByName(signerCommonName);
            Certificate signerCertificate = PemUtils.certificateFromPem(signerEntity.getCertificateContent());
            CertificateValidationResult x509CertificateValidationResult =
                    certificateValidationService.validateCertificate(certificateToValidate, signerCertificate);
            if (x509CertificateValidationResult.isValid()) {
                certificateRepository.save(certificateEntity);
                response = Response.ok().build();
            } else {
                secureLoggingWriter.log(Level.ERROR,
                        buildLogMessage(CertificateRegistryLogEvents.CERTIFICATE_VALIDATION_FAILED, subjectCommonName,
                                trackingId, CertificateRegistryLogEvents.CERTIFICATE_VALIDATION_FAILED.getInfo()));
                response = Response.status(Response.Status.PRECONDITION_FAILED).build();
            }

        } catch (IOException | GeneralCryptoLibException e) {
        	LOG.info("Error trying to save certificate for election event.", e);
            response = Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
        }
        return response;

    }

    /**
     * Saves a certificate in the repository
     *
     * @param tenantId
     *            - tenant identifier
     * @param certificateReader
     *            - content of the certificate
     * @return
     * @throws DuplicateEntryException
     * @throws ResourceNotFoundException
     * @throws CryptographicOperationException
     */
    @POST
    @Path(SAVE_CERTIFICATE_FOR_TENANT)
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response saveCertificate(@HeaderParam(Constants.PARAMETER_HEADER_X_REQUEST_ID) String trackingId,
                                      @PathParam(QUERY_PARAMETER_TENANT_ID) String tenantId, @NotNull Reader certificateReader,
                                      @Context HttpServletRequest servletRequest)
            throws DuplicateEntryException, ResourceNotFoundException, CryptographicOperationException {

        trackIdInstance.setTrackId(trackingId);

        transactionInfoProvider.generate(tenantId, httpRequestService.getIpClientAddress(servletRequest),
                httpRequestService.getIpServer(servletRequest));

        Response response;
        try {
            CertificateEntity certificateEntity = ObjectMappers.fromJson(certificateReader, CertificateEntity.class);
            certificateEntity.setTenantId(tenantId);

            Certificate certificateToValidate = PemUtils.certificateFromPem(certificateEntity.getCertificateContent());
            CryptoAPIX509Certificate cryptoAPIX509Certificate =
                    CertificateTools.getCryptoX509Certificate(certificateToValidate);
            String subjectCommonName = cryptoAPIX509Certificate.getSubjectDn().getCommonName();
            String signerCommonName = cryptoAPIX509Certificate.getIssuerDn().getCommonName();
            String platformName = cryptoAPIX509Certificate.getSubjectDn().getOrganization();
            certificateEntity.setPlatformName(platformName);
            CertificateEntity signerEntity = certificateRepository.findByName(signerCommonName);
            Certificate signerCertificate = PemUtils.certificateFromPem(signerEntity.getCertificateContent());
            CertificateValidationResult x509CertificateValidationResult =
                    certificateValidationService.validateCertificate(certificateToValidate, signerCertificate);
            if (x509CertificateValidationResult.isValid()) {
                certificateRepository.save(certificateEntity);
                response = Response.ok().build();
            } else {
                secureLoggingWriter.log(Level.ERROR,
                        buildLogMessage(CertificateRegistryLogEvents.CERTIFICATE_VALIDATION_FAILED, subjectCommonName,
                                trackingId, CertificateRegistryLogEvents.CERTIFICATE_VALIDATION_FAILED.getInfo()));
                response = Response.status(Response.Status.PRECONDITION_FAILED).build();
            }

        } catch (IOException | GeneralCryptoLibException e) {
        	LOG.info("Error trying to save certificate for tenant.", e);
            response = Response.status(Response.Status.BAD_REQUEST).build();
        }
        return response;
    }

    /**
     * Saves a certificate in the repository
     *
     * @param certificateReader
     * @return
     * @throws DuplicateEntryException
     * @throws ResourceNotFoundException
     * @throws CryptographicOperationException
     */
    @POST
    @Path(SAVE_CERTIFICATE)
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response saveCertificate(@HeaderParam(Constants.PARAMETER_HEADER_X_REQUEST_ID) String trackingId,
                                    @NotNull Reader certificateReader, @Context HttpServletRequest servletRequest)
            throws DuplicateEntryException, ResourceNotFoundException, CryptographicOperationException {

        trackIdInstance.setTrackId(trackingId);

        transactionInfoProvider.generate("", httpRequestService.getIpClientAddress(servletRequest),
                httpRequestService.getIpServer(servletRequest));

        Response response;
        try {
            CertificateEntity certificateEntity = ObjectMappers.fromJson(certificateReader, CertificateEntity.class);
            Certificate certificateToValidate = PemUtils.certificateFromPem(certificateEntity.getCertificateContent());
            CryptoAPIX509Certificate cryptoAPIX509Certificate =
                    CertificateTools.getCryptoX509Certificate(certificateToValidate);
            String subjectCommonName = cryptoAPIX509Certificate.getSubjectDn().getCommonName();

            String signerCommonName = cryptoAPIX509Certificate.getIssuerDn().getCommonName();
            CertificateEntity signerEntity = certificateRepository.findByName(signerCommonName);
            Certificate signerCertificate = PemUtils.certificateFromPem(signerEntity.getCertificateContent());

            CertificateValidationResult x509CertificateValidationResult =
                    certificateValidationService.validateCertificate(certificateToValidate, signerCertificate);
            if (x509CertificateValidationResult.isValid()) {
                certificateRepository.save(certificateEntity);
                response = Response.ok().build();
            } else {
                secureLoggingWriter.log(Level.ERROR,
                        buildLogMessage(CertificateRegistryLogEvents.CERTIFICATE_VALIDATION_FAILED, subjectCommonName,
                                trackingId, CertificateRegistryLogEvents.CERTIFICATE_VALIDATION_FAILED.getInfo()));
                response = Response.status(Response.Status.PRECONDITION_FAILED).build();
            }

        } catch (PersistenceException e) {
        	throw new DuplicateEntryException("Error trying to save certificate.", e);
        } catch (IOException | GeneralCryptoLibException e) {
        	LOG.info("Error trying to save certificate.", e);
            response = Response.status(Response.Status.BAD_REQUEST).build();
        }
        return response;

    }

    /**
     * Searches for a certificate with the given parameters
     *
     * @param tenantId
     *            - tenant identifier
     * @param electionEventId
     *            - election event identifier
     * @param certificateName
     *            - certificate name
     * @return an object with the certificate data
     * @throws DuplicateEntryException
     * @throws ResourceNotFoundException
     */
    @GET
    @Path(GET_CERTIFICATE_FOR_TENANT_EEID)
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response getCertificate(@PathParam(QUERY_PARAMETER_TENANT_ID) String tenantId,
                                   @PathParam(QUERY_PARAMETER_ELECTION_EVENT_ID) String electionEventId,
                                   @PathParam(QUERY_PARAMETER_CERTIFICATE_NAME) String certificateName)
            throws DuplicateEntryException, ResourceNotFoundException {

        CertificateEntity certificateEntity = certificateRepository.findByTenanElectionEventAndCertificateName(tenantId,
                electionEventId, certificateName);
        return Response.ok(certificateEntity).build();

    }

    /**
     * Searches for a certificate with the given parameters
     *
     * @param tenantId
     *            - tenant identifier
     * @param certificateName
     *            - certificate name
     * @return an object with the certificate data
     * @throws DuplicateEntryException
     * @throws ResourceNotFoundException
     */
    @GET
    @Path(GET_CERTIFICATE_FOR_TENANT)
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response getCertificate(@PathParam(QUERY_PARAMETER_TENANT_ID) String tenantId,
                                   @PathParam(QUERY_PARAMETER_CERTIFICATE_NAME) String certificateName)
            throws DuplicateEntryException, ResourceNotFoundException {

        CertificateEntity certificateEntity =
                certificateRepository.findByTenantAndCertificateName(tenantId, certificateName);
        return Response.ok(certificateEntity).build();

    }

    /**
     * Searches for a certificate with the given parameters
     *
     * @param certificateName
     *            - certificate name
     * @return an object with the certificate data
     * @throws DuplicateEntryException
     * @throws ResourceNotFoundException
     */
    @GET
    @Path(GET_CERTIFICATE)
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response getCertificate(@PathParam(QUERY_PARAMETER_CERTIFICATE_NAME) String certificateName)
            throws DuplicateEntryException, ResourceNotFoundException {

        CertificateEntity certificateEntity = certificateRepository.findByName(certificateName);
        return Response.ok(certificateEntity).build();
    }

    /**
     * Returns the result of validate if certificate exists for the given
     * parameters
     *
     * @param tenantId
     *            - the tenant identifier.
     * @param name
     *            - the ballot box identifier.
     * @return Returns the result of the validation.
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path(CHECK_IF_CERTIFICATE_EXIST)
    public Response checkIfCertificateExist(@PathParam(QUERY_PARAMETER_TENANT_ID) String tenantId,
                                            @PathParam(QUERY_PARAMETER_CERTIFICATE_NAME) String name) {

        LOG.info("Validating if exists any certificate for the tenant {} and name {} is empty.", tenantId, name);

        Long numCertificates = certificateRepository.checkIfCertificateExist(tenantId, name);

        ValidationResult validationResult = new ValidationResult();
        // set to true if we FOUND a certificate
        validationResult.setResult(numCertificates > 0);
        ValidationError validationError = new ValidationError();
        if (validationResult.isResult()) {
            validationError.setValidationErrorType(ValidationErrorType.SUCCESS);
        }
        validationResult.setValidationError(validationError);

        LOG.info(" Certificate {} exists?: {}", name, validationResult.isResult());

        return Response.ok().entity(validationResult).build();
    }

    private LogContent buildLogMessage(final LogEvent event, final String objectId, final String trackId,
                                       final String errorDescription) {
        return new LogContent.LogContentBuilder().logEvent(event).objectId(objectId)
                .additionalInfo(CertificateRegistryLogConstants.INFO_TRACK_ID, trackId)
                .additionalInfo(CertificateRegistryLogConstants.INFO_ERR_DESC, errorDescription).createLogInfo();
    }
}
