/*
 * 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 java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Path;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import javax.annotation.PostConstruct;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;

import com.scytl.products.ov.sdm.application.constant.Constants;
import com.scytl.products.ov.sdm.application.exception.BallotBoxServiceException;
import com.scytl.products.ov.sdm.domain.model.keytranslation.BallotBoxTranslation;
import org.apache.commons.lang3.StringUtils;
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.google.gson.JsonParser;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.products.ov.commons.beans.BallotBox;
import com.scytl.products.ov.commons.beans.BallotBoxContextData;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.configuration.JSONSignatureConfiguration;
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.sign.JSONSigner;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.sdm.domain.common.SignedObject;
import com.scytl.products.ov.sdm.domain.model.ballotbox.BallotBoxRepository;
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.OrchestratorClient;
import com.scytl.products.ov.sdm.infrastructure.service.ConfigurationEntityStatusService;
import com.scytl.products.ov.utils.ConfigObjectMapper;

import okhttp3.ResponseBody;
import retrofit2.Response;
import retrofit2.Retrofit;

/**
 * Service to operate with ballot boxes
 */
@Service
public class BallotBoxService {

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

    @Autowired
    private ConfigurationEntityStatusService statusService;

    @Autowired
    private BallotBoxRepository ballotBoxRepository;

    @Autowired
    private PathResolver pathResolver;

    @Autowired
    private KeyStoreService keystoreService;

    private ConfigObjectMapper mapper;

    private JSONSigner signer;

    @Value("${OR_URL}")
    private String orchestratorUrl;

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

    @PostConstruct
    public void init() {
        mapper = new ConfigObjectMapper();
        signer = new JSONSigner(JSONSignatureConfiguration.RSA_PSS_SHA256);
    }

    /**
     * Sign the ballot box configuration and change the state of the ballot box
     * from ready to SIGNED for a given election event and ballot box id.
     *
     * @param electionEventId
     *            the election event id.
     * @param ballotBoxId
     *            the ballot box id.
     * @param privateKeyPEM
     *            the administration board private key in PEM format.
     * @throws ResourceNotFoundException
     *             if the ballot box is not found.
     * @throws GeneralCryptoLibException
     *             if the private key cannot be read.
     */
    public void sign(String electionEventId, String ballotBoxId, final String privateKeyPEM)
            throws ResourceNotFoundException, GeneralCryptoLibException, IOException {

        PrivateKey privateKey = PemUtils.privateKeyFromPem(privateKeyPEM);

        JsonObject ballotBoxJsonObject = getValidBallotBox(electionEventId, ballotBoxId);
        String ballotId = ballotBoxJsonObject.getJsonObject(JsonConstants.JSON_ATTRIBUTE_NAME_BALLOT)
            .getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);

        Path ballotBoxConfigurationFilesPath =
            getCommonPathForConfigurationFiles(electionEventId, ballotBoxId, ballotId);

        LOGGER.info("Signing ballot box " + ballotBoxId);
        signBallotBoxJSON(privateKey, ballotBoxConfigurationFilesPath);
        LOGGER.info("Signing ballot box context data");
        signBallotBoxContextDataJSON(privateKey, ballotBoxConfigurationFilesPath);

        LOGGER.info("Changing the ballot box status");
        statusService.updateWithSynchronizedStatus(Status.SIGNED.name(), ballotBoxId, ballotBoxRepository,
            SynchronizeStatus.PENDING);

        LOGGER.info("The ballot box was successfully signed");

    }

    /**
     * Gets the set of ballot boxes states by ballot boxes ids list
     *
     * @param ballotBoxIdList
     *            the ballot box ids list
     * @return set of the states of ballot boxes
     * @throws ResourceNotFoundException
     *             the resource not found exception
     */
    public Set<String> getBallotBoxesStateSetByBallotBoxesIdList(List<String> ballotBoxIdList)
            throws ResourceNotFoundException {

        Set<String> stateList = new HashSet<>();
        for (String ballotBoxId : ballotBoxIdList) {
            JsonObject ballotBox = getBallotBoxJsonObject(ballotBoxId);
            stateList.add(ballotBox.getString(JsonConstants.JSON_ATTRIBUTE_NAME_STATUS));
        }
        return stateList;
    }

    /**
     * Gets the Ballot Box Json Object
     *
     * @param ballotBoxId
     *            the ballot box id
     * @return the ballot box json object
     * @throws ResourceNotFoundException
     *             the resource not found exception
     */
    public JsonObject getBallotBoxJsonObject(String ballotBoxId) throws ResourceNotFoundException {

        String ballotBox = ballotBoxRepository.find(ballotBoxId);

        if (StringUtils.isEmpty(ballotBox) || JsonConstants.JSON_EMPTY_OBJECT.equals(ballotBox)) {
            throw new ResourceNotFoundException("Ballot Box not found");
        }

        return JsonUtils.getJsonObject(ballotBox);
    }

    private void signBallotBoxContextDataJSON(final PrivateKey privateKey, final Path ballotBoxConfigurationFilesPath)
            throws IOException {
        Path ballotBoxContextDataJSONPath = ballotBoxConfigurationFilesPath
            .resolve(ConfigConstants.CONFIG_DIR_NAME_BALLOTBOX_CONTEXT_DATA_JSON).toAbsolutePath();
        Path signedBallotBoxContextDataJSONPath = ballotBoxConfigurationFilesPath
            .resolve(ConfigConstants.CONFIG_DIR_NAME_SIGNED_BALLOTBOX_CONTEXT_DATA_JSON).toAbsolutePath();

        BallotBoxContextData ballotBoxContextData =
            mapper.fromJSONFileToJava(new File(ballotBoxContextDataJSONPath.toString()), BallotBoxContextData.class);

        String signedBallotBoxContextData = signer.sign(privateKey, ballotBoxContextData);
        SignedObject signedBallotBoxContextDataObject = new SignedObject();
        signedBallotBoxContextDataObject.setSignature(signedBallotBoxContextData);
        mapper.fromJavaToJSONFile(signedBallotBoxContextDataObject,
            new File(signedBallotBoxContextDataJSONPath.toString()));
    }

    private void signBallotBoxJSON(final PrivateKey privateKey, final Path ballotBoxConfigurationFilesPath)
            throws IOException {
        Path ballotBoxJSONPath =
            ballotBoxConfigurationFilesPath.resolve(ConfigConstants.CONFIG_DIR_NAME_BALLOTBOX_JSON).toAbsolutePath();
        Path signedBallotBoxJSONPath = ballotBoxConfigurationFilesPath
            .resolve(ConfigConstants.CONFIG_DIR_NAME_SIGNED_BALLOTBOX_JSON).toAbsolutePath();

        BallotBox ballotBox = mapper.fromJSONFileToJava(new File(ballotBoxJSONPath.toString()), BallotBox.class);

        String signedBallotBox = signer.sign(privateKey, ballotBox);
        SignedObject signedBallotBoxObject = new SignedObject();
        signedBallotBoxObject.setSignature(signedBallotBox);
        mapper.fromJavaToJSONFile(signedBallotBoxObject, new File(signedBallotBoxJSONPath.toString()));
    }

    protected Path getCommonPathForConfigurationFiles(final String electionEventId, final String ballotBoxId,
            final String ballotId) {

        return pathResolver.resolve(ConfigConstants.CONFIG_FILES_BASE_DIR, electionEventId,
            ConfigConstants.CONFIG_DIR_NAME_ONLINE, ConfigConstants.CONFIG_DIR_NAME_ELECTIONINFORMATION,
            ConfigConstants.CONFIG_DIR_NAME_BALLOTS, ballotId, ConfigConstants.CONFIG_DIR_NAME_BALLOTBOXES,
            ballotBoxId);
    }

    private JsonObject getValidBallotBox(String electionEventId, String ballotBoxId) throws ResourceNotFoundException {

        Optional<JsonObject> possibleBallotBox = getPossibleValidBallotBox(electionEventId, ballotBoxId);

        if (!possibleBallotBox.isPresent()) {
            throw new ResourceNotFoundException("Ballot box not found");
        }

        return possibleBallotBox.get();
    }

    private Optional<JsonObject> getPossibleValidBallotBox(final String electionEventId, final String ballotBoxId) {

        Optional<JsonObject> ballotBox = Optional.empty();

        Map<String, Object> attributeValueMap = new HashMap<>();
        attributeValueMap.put(JsonConstants.JSON_STRUCTURE_NAME_ELECTION_EVENT_ID, electionEventId);
        attributeValueMap.put(JsonConstants.JSON_ATTRIBUTE_NAME_ID, ballotBoxId);
        attributeValueMap.put(JsonConstants.JSON_ATTRIBUTE_NAME_STATUS, Status.READY.name());
        String ballotBoxResultListAsJson = ballotBoxRepository.list(attributeValueMap);
        if (StringUtils.isEmpty(ballotBoxResultListAsJson)) {
            return ballotBox;
        } else {
            JsonArray ballotBoxResultList = JsonUtils.getJsonObject(ballotBoxResultListAsJson)
                .getJsonArray(JsonConstants.JSON_ATTRIBUTE_NAME_RESULT);
            // Assume that there is just one element as result of the search.
            if (ballotBoxResultList != null && !ballotBoxResultList.isEmpty()) {
                ballotBox = Optional.of(ballotBoxResultList.getJsonObject(0));
            } else {
                return ballotBox;
            }
        }
        return ballotBox;
    }

    /**
     * query ballot boxes to process in the synchronization process
     *
     * @return JsonArray with ballot boxes to upload
     */
    public JsonArray getBallotBoxesReadyToSynchronize(String electionEvent) {

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

        params.put(JsonConstants.JSON_ATTRIBUTE_NAME_STATUS, Status.SIGNED.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 (!Constants.NULL_ELECTION_EVENT_ID.equals(electionEvent))
            params.put(JsonConstants.JSON_STRUCTURE_NAME_ELECTION_EVENT_ID, electionEvent);
        String serializedBallotBoxes = ballotBoxRepository.list(params);

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

    /**
     * Updates the state of the synchronization status of the ballot box
     *
     * @param ballotBoxId
     * @param success
     */
    public void updateSynchronizationStatus(String ballotBoxId, boolean success) {
        JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
        jsonObjectBuilder.add(JsonConstants.JSON_ATTRIBUTE_NAME_ID, ballotBoxId);
        if (success) {
            jsonObjectBuilder.add(JsonConstants.JSON_ATTRIBUTE_NAME_SYNCHRONIZED,
                SynchronizeStatus.SYNCHRONIZED.getIsSynchronized().toString());
            jsonObjectBuilder.add(JsonConstants.JSON_ATTRIBUTE_NAME_DETAILS,
                SynchronizeStatus.SYNCHRONIZED.getStatus());
        } else {
            jsonObjectBuilder.add(JsonConstants.JSON_ATTRIBUTE_NAME_DETAILS, SynchronizeStatus.FAILED.getStatus());
        }
        ballotBoxRepository.update(jsonObjectBuilder.build().toString());
    }

    public BallotBoxTranslation createBallotBoxTranslation(JsonObject ballotBox, String ballotBoxId,
            String electionEventId) {
        BallotBoxTranslation ballotBoxTranslation = new BallotBoxTranslation();
        ballotBoxTranslation.setElectionEventId(electionEventId);
        ballotBoxTranslation.setBallotBoxId(ballotBoxId);
        ballotBoxTranslation.setTenantId(tenantId);
        ballotBoxTranslation.setAlias(ballotBox.getString(JsonConstants.JSON_ATTRIBUTE_NAME_ALIAS));
        return ballotBoxTranslation;
    }

    /**
     * Check if ballot boxes are mixed and updates their status in the database
     * 
     * @param electionEventId
     * @throws IOException
     * @throws RetrofitException
     */
    public void updateBallotBoxesMixingStatus(String electionEventId) throws IOException, RetrofitException {
        Map<String, Object> ballotBoxesParams = new HashMap<>();
        ballotBoxesParams.put(JsonConstants.JSON_ATTRIBUTE_NAME_STATUS, Status.SIGNED.name());
        ballotBoxesParams.put(JsonConstants.JSON_STRUCTURE_NAME_ELECTION_EVENT_ID, electionEventId);

        String ballotBoxJSON = ballotBoxRepository.list(ballotBoxesParams);
        com.google.gson.JsonArray ballotBoxes = new JsonParser().parse(ballotBoxJSON).getAsJsonObject()
            .get(JsonConstants.JSON_ATTRIBUTE_NAME_RESULT).getAsJsonArray();

        OrchestratorClient client = getOrchestratorClient();

        for (int i = 0; i < ballotBoxes.size(); i++) {
            com.google.gson.JsonObject ballotBoxInArray = ballotBoxes.get(i).getAsJsonObject();
            String ballotBoxId = ballotBoxInArray.get(JsonConstants.JSON_ATTRIBUTE_NAME_ID).getAsString();

            Response<ResponseBody> ballotBoxMixingStatusResponse =
                RetrofitConsumer.executeCall(client.getBallotBoxMixingStatus(tenantId, electionEventId, ballotBoxId));

            com.google.gson.JsonObject jsonObject;
            try (ResponseBody body = ballotBoxMixingStatusResponse.body(); Reader reader = body.charStream()) {
                jsonObject = new JsonParser().parse(reader).getAsJsonObject();
            }

            // Only update the status if it's "MIXED", the other possible mixing
            // statuses are not handled by SDM
            // SV-6483 also handle ERROR status ?
            final String mixedStatusValue = Status.MIXED.toString();
            if (jsonObject.get("status") != null && mixedStatusValue.equals(jsonObject.get("status").getAsString())) {
                ballotBoxInArray.addProperty(JsonConstants.JSON_ATTRIBUTE_NAME_STATUS, mixedStatusValue);
                ballotBoxRepository.update(ballotBoxInArray.toString());
            }
        }

    }

    /**
     * Gets the orchestrator Retrofit client
     */
    private OrchestratorClient getOrchestratorClient() {
        try {
            PrivateKey privateKey = keystoreService.getPrivateKey();
            Retrofit restAdapter =
                RestClientConnectionManager.getInstance().getRestClientWithInterceptorAndJacksonConverter(
                    orchestratorUrl, privateKey, NodeIdentifier.SECURE_DATA_MANAGER);
            return restAdapter.create(OrchestratorClient.class);
        } catch (OvCommonsInfrastructureException e) {
            throw new BallotBoxServiceException("Error trying to get a RestClientConnectionManager instance.", e);
        }
    }
}
