/*
 * 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.application.service;

import com.scytl.cryptolib.certificates.utils.LdapHelper;
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.sdm.application.exception.AdminBoardUploadServiceException;
import com.scytl.products.ov.sdm.application.log.SdmSecureLogEvent;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.commons.io.IOUtils;
import org.codehaus.jackson.JsonParseException;
import org.owasp.esapi.ESAPI;
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.stereotype.Service;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.certificates.factory.CryptoX509Certificate;
import com.scytl.products.ov.commons.infrastructure.exception.OvCommonsInfrastructureException;
import com.scytl.products.ov.commons.infrastructure.remote.client.NodeIdentifier;
import com.scytl.products.ov.commons.infrastructure.remote.client.RestClientConnectionManager;
import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitConsumer;
import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitException;
import com.scytl.products.ov.commons.path.PathResolver;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.commons.validation.ValidationResult;
import com.scytl.products.ov.sdm.domain.model.administrationauthority.AdministrationAuthorityRepository;
import com.scytl.products.ov.sdm.domain.model.certificateRegistry.Certificate;
import com.scytl.products.ov.sdm.domain.model.electionevent.ElectionEventRepository;
import com.scytl.products.ov.sdm.domain.model.status.Status;
import com.scytl.products.ov.sdm.domain.model.status.SynchronizeStatus;
import com.scytl.products.ov.sdm.domain.service.ConfigConstants;
import com.scytl.products.ov.sdm.infrastructure.JsonConstants;
import com.scytl.products.ov.sdm.infrastructure.clients.CertificateRegistryClient;

import okhttp3.ResponseBody;
import retrofit2.Retrofit;

/**
 * Service for upload Admin board information
 */
@Service
public class AdminBoardUploadService {

	private static final String ENDPOINT_ADMIN_BOARD_CONTENTS_GET = "/certificates/tenant/{tenantId}/name/{certificateName}/status";

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

    private static final String NULL_ELECTION_EVENT_ID = "";

    private static final String TENANT_ID_PARAM = "tenantId";

    private static final String CERTIFICATE_NAME_PARAM = "certificateName";

    @Autowired
    private AdministrationAuthorityRepository administrationAuthorityRepository;

    @Autowired
    private ElectionEventRepository electionEventRepository;

    @Autowired
    private PathResolver pathResolver;

    @Value("${CR_URL}")
    private String apiGateWayURL;

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

    @Autowired
    SecureLoggingWriter secureLogger;

    @Autowired
    HashService hashService;

    /**
     * Uploads admin board certificates of all adminBoards pending to
     * synchronize
     */
    public void uploadSyncronizableAdminBoards(String electionEventId, PrivateKey requestSigningKey) {

        // query ballot boxes to process
        JsonArray adminBoards = getAdminBoardDataToUpload(electionEventId);

        try {
            // process them
            for (int i = 0; i < adminBoards.size(); i++) {
                JsonObject adminBoard = adminBoards.getJsonObject(i);
                String adminBoardId = adminBoard.getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);

                Path signedCertificatePath = pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR,
                    ConfigConstants.CSR_FOLDER, adminBoardId + ".pem");
                FileInputStream fisTargetFile = new FileInputStream(signedCertificatePath.toFile());
                String content = IOUtils.toString(fisTargetFile, "UTF-8");
                X509Certificate certificate =
                    (X509Certificate) com.scytl.cryptolib.primitives.primes.utils.PemUtils.certificateFromPem(content);
                CryptoX509Certificate cryptoX509Certificate = new CryptoX509Certificate(certificate);
                String certificateName = cryptoX509Certificate.getSubjectDn().getCommonName();

                LOGGER.info("Checking if the Administration board certificate has to be uploaded");
                
                if (checkAdminBoarCertificateIsEmpty(tenantId, certificateName)) {
                    LOGGER.info("Uploading administration board certificate");
                    uploadCertificate(adminBoardId, tenantId, content, certificateName, requestSigningKey);
                } else {

                    LOGGER.info("The administration board certificate was already uploaded");

                    // for now it will updated as success
                    JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
                    jsonObjectBuilder.add(JsonConstants.JSON_ATTRIBUTE_NAME_ID, adminBoardId);
                    jsonObjectBuilder.add(JsonConstants.JSON_ATTRIBUTE_NAME_SYNCHRONIZED,
                        SynchronizeStatus.SYNCHRONIZED.getIsSynchronized().toString());
                    jsonObjectBuilder.add(JsonConstants.JSON_ATTRIBUTE_NAME_DETAILS,
                        SynchronizeStatus.SYNCHRONIZED.getStatus());
                    administrationAuthorityRepository.update(jsonObjectBuilder.build().toString());
                }
            }
        } catch (IOException | GeneralCryptoLibException e) {
            LOGGER.error("Error trying to get the certificate information", e);
            secureLogger.log(Level.ERROR,
                new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.SYNCHRONIZING_WITH_VP_FAILED)
                    .electionEvent(electionEventId)
                    .additionalInfo("err_desc", "Error trying to get the certificate information: " + e.getMessage())
                    .createLogInfo());

        }

    }

    /**
     * query admin boards to process
     *
     * @return JsonArray with ballot boxes to upload
     */
    private JsonArray getAdminBoardDataToUpload(String electionEvent) {

        Map<String, Object> params = new HashMap<>();

        params.put(JsonConstants.JSON_ATTRIBUTE_NAME_STATUS, Status.CONSTITUTED.name());
        params.put(JsonConstants.JSON_ATTRIBUTE_NAME_SYNCHRONIZED,
            SynchronizeStatus.PENDING.getIsSynchronized().toString());
        // If there is an election event as parameter, it will be included in
        // the query
        if (!NULL_ELECTION_EVENT_ID.equals(electionEvent)) {
            JsonObject electionEventObject = JsonUtils.getJsonObject(electionEventRepository.find(electionEvent));
            String adminBoardId =
                electionEventObject.getJsonObject(JsonConstants.JSON_ATTRIBUTE_NAME_ADMINISTRATION_AUTHORITY)
                    .getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);
            params.put(JsonConstants.JSON_ATTRIBUTE_NAME_ID, adminBoardId);
        }
        String serializedBallotBoxes = administrationAuthorityRepository.list(params);

        return JsonUtils.getJsonObject(serializedBallotBoxes).getJsonArray(JsonConstants.JSON_ATTRIBUTE_NAME_RESULT);
    }

    /**
     * Uploads certificated
     *
     * @throws IOException
     * @throws JsonParseException
     */
    private void uploadCertificate(String adminBoardId, String tenantId, String content, String certName,
            PrivateKey requestSigningKey) throws IOException {

        LOGGER.info("Trying to upload Certificate...");

        Retrofit restAdapter = null;
        try {
            restAdapter = RestClientConnectionManager.getInstance().getRestClientWithInterceptorAndJacksonConverter(
                apiGateWayURL, requestSigningKey, NodeIdentifier.SECURE_DATA_MANAGER);

        } catch (OvCommonsInfrastructureException e) {
            LOGGER.error("Error trying to get a RestClientConnectionManager instance.");
			throw new AdminBoardUploadServiceException("Error trying to get a RestClientConnectionManager instance.",
					e);
        }

        CertificateRegistryClient crClient = restAdapter.create(CertificateRegistryClient.class);

        Certificate certificate = new Certificate();
        certificate.setCertificateName(certName);
        String sanitizedContent = ESAPI.encoder().canonicalize(content);
        certificate.setCertificateContent(sanitizedContent);
        JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
        String certCn = "";
        String certSn = "";
		try (ResponseBody responseBody = RetrofitConsumer
				.processResponse(crClient.saveCertificate(tenantId, certificate))) {
	       	jsonObjectBuilder.add(JsonConstants.JSON_ATTRIBUTE_NAME_ID, adminBoardId);
	
	        try {
	            X509Certificate x509Certificate = (X509Certificate) PemUtils.certificateFromPem(sanitizedContent);
	            final LdapHelper ldapHelper = new LdapHelper();
	            String subjectDn = x509Certificate.getSubjectX500Principal().getName();
	            certCn = ldapHelper.getAttributeFromDistinguishedName(subjectDn, "CN");
	            certSn = String.valueOf(x509Certificate.getSerialNumber());
	
	        } catch (GeneralCryptoLibException e) {
	            LOGGER.error("An error ocurred while parsing admin board certificate {}", certName, e);
	        }

            jsonObjectBuilder.add(JsonConstants.JSON_ATTRIBUTE_NAME_SYNCHRONIZED,
                SynchronizeStatus.SYNCHRONIZED.getIsSynchronized().toString());
            jsonObjectBuilder.add(JsonConstants.JSON_ATTRIBUTE_NAME_DETAILS,
                SynchronizeStatus.SYNCHRONIZED.getStatus());
            LOGGER.info("The administration board certificate was successfully uploaded");

            String b64CertificateHash = "";
            try {
                b64CertificateHash = hashService.getB64Hash(sanitizedContent.getBytes(StandardCharsets.UTF_8));
            } catch (HashServiceException e) {
                String message = "Error computing the hash of admin board certificate {}";
                LOGGER.error(message,certName,e);
            }

            secureLogger.log(Level.INFO,
                new LogContent.LogContentBuilder().objectId(adminBoardId)
                    .logEvent(SdmSecureLogEvent.ADMIN_BOARD_CERTIFICATE_UPLOADED_SUCCESSFULLY)
                    .additionalInfo("file_h", b64CertificateHash).additionalInfo("cert_cn", certCn)
                    .additionalInfo("cert_sn", certSn).createLogInfo());

        } catch (RetrofitException e){
        	LOGGER.error("Error trying to process Response.", e);
            jsonObjectBuilder.add(JsonConstants.JSON_ATTRIBUTE_NAME_ID, adminBoardId);
            jsonObjectBuilder.add(JsonConstants.JSON_ATTRIBUTE_NAME_DETAILS, SynchronizeStatus.FAILED.getStatus());

            secureLogger.log(Level.ERROR, new LogContent.LogContentBuilder().objectId(adminBoardId)
                .logEvent(SdmSecureLogEvent.ADMIN_BOARD_CERTIFICATE_UPLOADING_FAILED).additionalInfo("cert_cn", certCn)
                .additionalInfo("cert_sn", certSn).additionalInfo("err_desc", "Error trying to process Response.").createLogInfo());
        }

        LOGGER.info("Trying to update repository...");
        administrationAuthorityRepository.update(jsonObjectBuilder.build().toString());
    }

    private boolean checkAdminBoarCertificateIsEmpty(String tenantId, String name) {

        Boolean result = Boolean.TRUE;
        WebTarget target = ClientBuilder.newClient().target(apiGateWayURL + ENDPOINT_ADMIN_BOARD_CONTENTS_GET);

        Response response = target.resolveTemplate(TENANT_ID_PARAM, tenantId)
            .resolveTemplate(CERTIFICATE_NAME_PARAM, name).request(MediaType.APPLICATION_JSON).get();
        if (response.getStatus() == Response.Status.OK.getStatusCode()) {
            String json = response.readEntity(String.class);
            try {
                ValidationResult validationResult = ObjectMappers.fromJson(json, ValidationResult.class);
                // adias: if the certificate exists the result will be
                // TRUE/SUCCESS so we have to return the inverse
                // if we want to check if "empty"
                result = !validationResult.isResult();
            } catch (IOException e) {
                LOGGER.error("Error checking if a ballot box is empty", e);
            }
        }
        return result;
    }

}
