/*
 * 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.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
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.ProcessingException;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.ObjectNode;
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.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.Ballot;
import com.scytl.products.ov.commons.beans.exceptions.ApplicationException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.util.JsonUtils;
import com.scytl.products.ov.sdm.application.log.SdmSecureLogEvent;
import com.scytl.products.ov.sdm.domain.model.ballot.BallotRepository;
import com.scytl.products.ov.sdm.domain.model.ballotbox.BallotBoxRepository;
import com.scytl.products.ov.sdm.domain.model.ballotboxtallying.BallotBoxTallyingRepository;
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.tallying.TallyingInput;
import com.scytl.products.ov.sdm.domain.service.BallotDataGeneratorService;
import com.scytl.products.ov.sdm.infrastructure.JsonConstants;
import com.scytl.products.ov.sdm.infrastructure.service.ConfigurationEntityStatusService;

/**
 * Service which will invoke the tallying operation
 */
@Service
public class TallyingService {

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

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

    @Autowired
    private BallotBoxRepository ballotBoxRepository;

    @Autowired
    private BallotRepository ballotRepository;

    @Autowired
    BallotBoxTallyingRepository ballotBoxTallyingRepository;

    @Autowired
    private ConfigurationEntityStatusService configurationEntityStatusService;

    @Autowired
    private ElectionEventRepository electionEventRepository;

    @Autowired
    private SecureLoggingWriter secureLogger;

    @Autowired
    private BallotDataGeneratorService ballotDataGeneratorService;

    @Value("${TALLYING_URL}")
    private String tallyingURL;

    @Value("${tallying.enabled}")
    private boolean enabled;

    /**
     * Invokes the ballot box tallying end point, with all the necessary
     * information
     * 
     * @param electionEventId
     *            - identifier of the election Event id
     * @param ballotBoxId
     *            - identifier of the election Event Id
     * @param privateKeyPEM
     *            - private key that is used for signing the tallying result
     *            files, in PEM format.
     * @return true if tallying was successfully executed and false otherwise
     * @throws ResourceNotFoundException
     * @throws IOException
     * @throws ApplicationException
     */
    public boolean tallyBallotBox(final String electionEventId, final String ballotBoxId, final String privateKeyPEM)
            throws ResourceNotFoundException, IOException, ApplicationException {

        if (!enabled) {
            throw new IllegalStateException("Tallying is not enabled.");
        }

        // find the ballot box
        Map<String, Object> attributeValueMap = new HashMap<>();
        attributeValueMap.put(JsonConstants.JSON_ATTRIBUTE_NAME_ID, ballotBoxId);
        attributeValueMap.put(JsonConstants.JSON_STRUCTURE_NAME_ELECTION_EVENT_ID, electionEventId);
        String ballotBoxJson = ballotBoxRepository.list(attributeValueMap);
        JsonArray ballotBoxJsonArray =
            JsonUtils.getJsonObject(ballotBoxJson).getJsonArray(JsonConstants.JSON_ATTRIBUTE_NAME_RESULT);

        // remove all existing ballot files (will be re-generated)
        ballotDataGeneratorService.cleanAll(electionEventId);

        if (!ballotBoxJsonArray.isEmpty()) {

            JsonObject ballotBoxJsonObject = ballotBoxJsonArray.getJsonObject(0);

            // check correct status of bbox
            if (!Status.DECRYPTED.name()
                .equals(ballotBoxJsonObject.getString(JsonConstants.JSON_ATTRIBUTE_NAME_STATUS))) {
                throw new ApplicationException("Ballot box is not decrypted");
            }

            String ballotId = ballotBoxJsonObject.getJsonObject(JsonConstants.JSON_ATTRIBUTE_NAME_BALLOT)
                .getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);

            // re-generate ballot file to make sure it is aligned with latest
            // ballot data
            ballotDataGeneratorService.generate(ballotId, electionEventId);

            String ballotAsJson = ballotRepository.find(ballotId);
            final String[] fields =
                {"defaultTitle", "defaultDescription", "status", "ballotBoxes", "details", "synchronized" };
            ballotAsJson = JsonUtils.removeFieldsFromJson(ballotAsJson, fields);

            // remove election event from contests array in a ballot.
            JsonFactory factory = new JsonFactory();
            JsonParser parser = factory.createJsonParser(ballotAsJson);
            ObjectMapper mapper = new ObjectMapper(factory);
            JsonNode rootNode = mapper.readTree(parser);
            ArrayNode jsonArrayNode = (ArrayNode) rootNode.get("contests");
            if (jsonArrayNode != null) {
                for (JsonNode jsonNodeFromArray : jsonArrayNode) {
                    ObjectNode object = (ObjectNode) jsonNodeFromArray;
                    object.remove(JsonConstants.JSON_ATTRIBUTE_NAME_ELECTION_EVENT);
                }
            }
            ballotAsJson = rootNode.toString();

            Ballot ballot = ObjectMappers.fromJson(ballotAsJson, Ballot.class);

            String electionEventString = electionEventRepository.find(electionEventId);
            String adminBoardId = JsonUtils.getJsonObject(electionEventString)
                .getJsonObject(JsonConstants.JSON_ATTRIBUTE_NAME_ADMINISTRATION_AUTHORITY)
                .getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID);

            TallyingInput input = new TallyingInput();
            input.setBallot(ballot);
            input.setBallotBoxID(ballotBoxId);
            input.setTenantID(tenantId);
            input.setCertId(adminBoardId);
            input.setPrivateKeyPEM(privateKeyPEM);
            JsonObject electionEvent = JsonUtils.getJsonObject(electionEventString);
            String writeInAlphabet = electionEvent.getJsonObject(JsonConstants.JSON_ATTRIBUTE_NAME_SETTINGS)
                .getString(JsonConstants.JSON_PARAM_NAME_WRITE_IN_ALPHABET);
            String decodedWriteInAlphabet =
                new String(Base64.getDecoder().decode(writeInAlphabet), StandardCharsets.UTF_8);
            // the first character of the alphabet is the write-in separator
            input.setWriteInSeparator(decodedWriteInAlphabet.substring(0, 1));

            // Run tallying process in the crypto operations server
            Response response;
            try {
                WebTarget ballotBoxClient = ClientBuilder.newClient().target(tallyingURL);

                response = ballotBoxClient.request()
                    .post(Entity.entity(ObjectMappers.toJson(input), MediaType.APPLICATION_JSON));
            } catch (ProcessingException e) {
                LOGGER.error("Error performing post request", e);

                secureLogger.log(Level.INFO,
                    new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.TALLYING_RESULTS_FAILED)
                        .objectId(ballotBoxId).electionEvent(electionEventId)
                        .additionalInfo("err_desc", "Error performing post request: " + e.getMessage())
                        .createLogInfo());

                return false;
            }

            if (response.getStatus() == Response.Status.OK.getStatusCode()) {
                // the ballot id used for save ballot box in the ballot
                // directory

                String countResult = response.readEntity(String.class);

                JsonObjectBuilder builder = JsonUtils.jsonObjectToBuilder(JsonUtils.getJsonObject(countResult));
                builder.add(JsonConstants.JSON_ATTRIBUTE_NAME_ELECTION_EVENT,
                    Json.createObjectBuilder().add(JsonConstants.JSON_ATTRIBUTE_NAME_ID, electionEventId));
                builder.add(JsonConstants.JSON_ATTRIBUTE_NAME_ID, ballotBoxId);

                ballotBoxTallyingRepository.save(builder.build().toString());

                // change status to "TALLIED"
                configurationEntityStatusService.update(Status.TALLIED.name(),
                    ballotBoxJsonObject.getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID), ballotBoxRepository);

                LOGGER.info("Successfully TALLIED ballot box");
                return true;
            } else {
                secureLogger.log(Level.INFO,
                    new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.TALLYING_RESULTS_FAILED)
                        .objectId(ballotBoxId).electionEvent(electionEventId)
                        .additionalInfo("err_desc", "Response status: " + response.getStatus()).createLogInfo());
            }

        } else {
            secureLogger.log(Level.INFO,
                new LogContent.LogContentBuilder().logEvent(SdmSecureLogEvent.TALLYING_RESULTS_FAILED)
                    .objectId(ballotBoxId).electionEvent(electionEventId)
                    .additionalInfo("err_desc", "Ballot box JSON array is empty").createLogInfo());
        }

        return false;
    }

    /**
     * Returns whether the service is enabled.
     * 
     * @return the service is enabled.
     */
    public boolean isEnabled() {
        return enabled;
    }
}