/*
 * 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.io.PrintWriter;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonValue;

import com.scytl.products.ov.sdm.domain.model.ballot.BallotRepository;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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.domain.model.ballotbox.BallotBoxRepository;
import com.scytl.products.ov.sdm.domain.model.ballotboxcounting.BallotBoxCountingRepository;
import com.scytl.products.ov.sdm.domain.model.ballottext.BallotTextRepository;
import com.scytl.products.ov.sdm.domain.model.electionevent.ElectionEventRepository;
import com.scytl.products.ov.sdm.domain.model.tallysheet.ElectionTypes;
import com.scytl.products.ov.sdm.domain.model.votingcardset.VotingCardSetRepository;
import com.scytl.products.ov.sdm.infrastructure.JsonConstants;

/**
 * Service for handling tallysheets.
 */
@Service
public class TallySheetService {

    private static final String CSV_SINGLE_QUOTES = "'";

    private static final String CSV_EMPTY_STRING = "";

    private static final String CSV_SEPARATOR = ";";

    private static final String CSV_BREAK_LINE = "\n";

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

    private static final String CSV_BLANK = " ";

    private static final String CSV_PERCENTAJE = "%";

    private static final int FIRST_ELEMENT = 0;

    @Autowired
    private ElectionEventRepository electionEventRepository;

    @Autowired
    private BallotBoxRepository ballotBoxRepository;

    @Autowired
    private BallotRepository ballotRepository;

    @Autowired
    private BallotTextRepository ballotTextRepository;

    @Autowired
    private BallotBoxCountingRepository ballotBoxCountingRepository;

    @Autowired
    private VotingCardSetRepository votingCardSetRepository;

    public void generate(PrintWriter writer, String electionEventId, String ballotBoxId)
            throws ResourceNotFoundException, IOException {

        LOGGER.info("Writing on TallySheetService...");

        // data
        JsonObject countingJSON = getCountingJson(electionEventId, ballotBoxId);

        Integer numVotes = countingJSON.getJsonArray("result").getJsonObject(0).getInt("numVotes");
        JsonArray countingResults = countingJSON.getJsonArray("result").getJsonObject(0).getJsonArray("countingResults");
        JsonArray errors = countingJSON.getJsonArray("result").getJsonObject(0).getJsonArray("errors");

        // ballot text
        String ballotId = ballotBoxRepository.getBallotId(ballotBoxId);
        JsonArray ballotTextJson = getBallotTextJson(ballotId);
        Map<String, Map<String, String>> idToTextMap = getIdToTextMap(ballotTextJson, ballotId);

        // header
        String ballotTitle = getTitle(idToTextMap.get(ballotId));
        writeHeader(writer, electionEventId, ballotBoxId, ballotTitle, numVotes, errors);

        // Group counting results by contestId
        Map<String, List<JsonObject>> fromContestToQuestionAndAnswerMap = getContestResultsMap(countingResults);
        for (Map.Entry<String, List<JsonObject>> contestResult : fromContestToQuestionAndAnswerMap.entrySet()) {
            switch (getElectionType(contestResult)) {
                case ELECTION:
                    // group results by list
                    Map<String, Map<String, JsonObject>> listIdToResults = groupByList(countingResults);
                    generateElectionTally(writer, contestResult, idToTextMap, listIdToResults, ballotId);
                    break;
                case VOTATION:
                    generateVotationTally(writer, contestResult, idToTextMap);
                    break;
            }
        }
    }

    private String getTitle(Map<String, String> map) {
        String text = "";
        if (map.get("title") != null) {
            text = map.get("title");
        } else if (map.get("text") != null) {
            text = map.get("text");
        }
        return text;
    }

	private void writeHeader(PrintWriter writer, String electionEventId, String ballotBoxId, String ballotTitle,
			Integer numVotes, JsonArray errors)
            throws ResourceNotFoundException, IOException {

        String electionEventTitle = getElectionEventTitle(electionEventId);
        String ballotBoxTitle = getBallotBoxTitle(ballotBoxId);

        writer.print(electionEventTitle + CSV_BREAK_LINE);
        writer.print(ballotBoxTitle + CSV_BREAK_LINE);
        writer.print(ballotTitle + CSV_BREAK_LINE);
        writer.print(CSV_BREAK_LINE);

        // Voting cards sets and number of voting cards
        Map<String, Object> params = new HashMap<>();
        params.put("ballotBox.id", ballotBoxId);
        String votingCardSetsAsJson = votingCardSetRepository.list(params);
        JsonObject votingCardSetsJson = JsonUtils.getJsonObject(votingCardSetsAsJson);
        JsonArray vcss = (JsonArray) votingCardSetsJson.get("result");
        int numberOfVoters = 0;
        StringBuilder vcsInfo = new StringBuilder();
        for (JsonValue vcs : vcss) {
            JsonObject vcsJsonObject = JsonUtils.getJsonObject(vcs.toString());
            if ("SIGNED".equalsIgnoreCase(vcsJsonObject.getString("status"))) {
                int numberOfVotingCards = vcsJsonObject.getInt("numberOfVotingCardsToGenerate");
                numberOfVoters = numberOfVoters + numberOfVotingCards;
                vcsInfo.append("Voting Cards of " + vcsJsonObject.getString("defaultTitle") + CSV_SEPARATOR
                        + numberOfVotingCards + CSV_BREAK_LINE);
            }
        }
        writer.print("Number of voters (Number of Voting Cards)" + CSV_SEPARATOR + numberOfVoters + CSV_BREAK_LINE);
        writer.print(vcsInfo.toString());
        writer.print(CSV_BREAK_LINE);

        // cast votes
        writer.print("Votes Casts (confirmed)" + CSV_SEPARATOR + numVotes + CSV_BREAK_LINE);

        // When blank votes are supported!
        String blankVotes = "0";
        writer.print("Blank Votes" + CSV_SEPARATOR + blankVotes + CSV_BREAK_LINE);

        int invalidVotes = errors.size();
        writer.print("Invalid Votes" + CSV_SEPARATOR + invalidVotes + CSV_BREAK_LINE);

        int validVotes = numVotes - invalidVotes;
        writer.print("Valid Votes" + CSV_SEPARATOR + validVotes + CSV_BREAK_LINE);
        writer.print(CSV_BREAK_LINE);

        DecimalFormat df = new DecimalFormat("#.00");
        double participation = 0;
        if (numberOfVoters > 0) {
            participation = ((double) numVotes) * 100 / numberOfVoters;
        }
        writer.print("Electoral Participation" + CSV_SEPARATOR + df.format(participation) + CSV_BLANK + CSV_PERCENTAJE
                + CSV_BREAK_LINE);
        writer.print(CSV_BREAK_LINE);
    }

    private ElectionTypes getElectionType(Entry<String, List<JsonObject>> contest) {
        for (JsonObject c : contest.getValue()) {
            String type = c.getString("electionType").toUpperCase();
            try {
                return ElectionTypes.valueOf(type);
            } catch (IllegalArgumentException e) {
                LOGGER.error("Not found election of type {}", type, e);
            }
        }
        return null;
    }

    private void generateElectionTally(PrintWriter writer, Map.Entry<String, List<JsonObject>> contestResultsMap,
                                       Map<String, Map<String, String>> idToTextMap, Map<String, Map<String, JsonObject>> listIdToResults, String ballotId) {
        // is election Type
        LOGGER.info("Writing tallysheet for election...");

        String contestId = contestResultsMap.getKey();
        Map<String, String> contestMapTexts = idToTextMap.get(contestId);
        String contestTitle = getTitle(contestMapTexts);
        writer.print(contestTitle + CSV_BREAK_LINE);
        JsonObject ballot = JsonUtils.getJsonObject(ballotRepository.find(ballotId));

        List<JsonObject> results = contestResultsMap.getValue();
        for (JsonObject result : results) {
            String representationType = result.getString("representationType");
            if ("LIST".equals(representationType)) {
                // list header
                String listId = result.getString("questionId");
                Map<String, String> titleMap = idToTextMap.get(listId);
                Map<String, String> listHeaderMap = getListHeader(contestMapTexts);
                for (Map.Entry<String, String> listHeader : listHeaderMap.entrySet()) {
                    // exist the title for the current list
                    String listTitleValue = titleMap.get(listHeader.getKey());
                    if (listTitleValue != null) {
                        writer.print(listHeader.getValue() + CSV_SEPARATOR + listTitleValue + CSV_BREAK_LINE);
                    }
                }

                // list information
                Integer numVotes = Integer.parseInt(result.getString("numVotes"));
                Integer complementary = Integer.parseInt(result.getString("complementaryVotes"));
                Integer totalVotes = numVotes + complementary;
                writer.print("Total Candidate Votes" + CSV_SEPARATOR + numVotes + CSV_BREAK_LINE);
                writer.print("Complementary Votes" + CSV_SEPARATOR + complementary + CSV_BREAK_LINE);
                writer.print("Total Party Votes" + CSV_SEPARATOR + totalVotes + CSV_BREAK_LINE);

                // candidates header
                writer.print(getCandidateHeader(contestMapTexts) + CSV_SEPARATOR + "Votes" + CSV_SEPARATOR + CSV_PERCENTAJE
                        + CSV_BREAK_LINE);

                // candidate information
                Map<String, JsonObject> resultsByList = listIdToResults.get(listId);
                for (Map.Entry<String, JsonObject> r : resultsByList.entrySet()) {
                    JsonObject candidateResult = r.getValue();
                    if ("CANDIDATE".equals(candidateResult.getString("representationType"))) {
                        String candidateId = candidateResult.getString("questionId");
                        final String candidateTitles = getInfo(idToTextMap.get(candidateId));
                        Integer votesCandidate = Integer.valueOf(candidateResult.getString("numVotes"));
                        final String candidateIdRepresentation = getCandidateAlias(contestId, ballot, candidateId);
                        DecimalFormat df = new DecimalFormat("#.00");
                        double percent;
                        if (numVotes == 0) {
                            percent = 0;
                        } else {
                            percent = ((double) votesCandidate) * 100 / numVotes;
                        }

                        writer.print(candidateIdRepresentation + CSV_SEPARATOR + candidateTitles + CSV_SEPARATOR +
                                votesCandidate + CSV_SEPARATOR + df.format(percent)
                                + CSV_BREAK_LINE);
                    }
                }
                writer.print(CSV_BREAK_LINE);
            }
        }

    }

    private Map<String, String> getListHeader(Map<String, String> map) {
        Map<String, String> result = new HashMap<>();
        for (Map.Entry<String, String> m : map.entrySet()) {
            if (m.getKey().contains("listColumn")) {
                result.put(m.getKey(), m.getValue());
            }
        }
        return result;
    }

    private String getCandidateHeader(Map<String, String> map) {
        List<String> result = new ArrayList<>();
        for (Map.Entry<String, String> m : map.entrySet()) {
            if (m.getKey().contains("candidateColumn")) {
                result.add(m.getValue());
            }
        }
        return StringUtils.join(result, CSV_SEPARATOR);
    }

    private String getInfo(Map<String, String> map) {
        return StringUtils.join(map.values(), CSV_SEPARATOR);
    }

    private void generateVotationTally(PrintWriter writer, Map.Entry<String, List<JsonObject>> contest,
                                       Map<String, Map<String, String>> idToTextMap) {
        // is votation
        LOGGER.info("Writing tallysheet for votation...");

        String contestId = contest.getKey();

        String contestTitle = getTitle(idToTextMap.get(contestId));
        writer.print(contestTitle + CSV_BREAK_LINE);

        List<JsonObject> results = contest.getValue();
        for (JsonObject result : results) {

            String question = result.getString("questionId");
            question = question.replaceAll("[\\[|\\]]", "");
            String questionTitle = getTitle(idToTextMap.get(question.replace("\\[", "")));
            String optionId = result.getString("optionId");
            String optionIdTitle = getTitle(idToTextMap.get(optionId));
            String numVotes = result.getString("numVotes");

            writer.print("Question" + CSV_BLANK + CSV_SINGLE_QUOTES + questionTitle + CSV_SINGLE_QUOTES + CSV_SEPARATOR
                    + CSV_BREAK_LINE);
            writer.print("Answer Choice" + CSV_BLANK + CSV_SINGLE_QUOTES + optionIdTitle + CSV_SINGLE_QUOTES + CSV_SEPARATOR
                    + numVotes + CSV_BREAK_LINE);
        }
    }

    private Map<String, Map<String, JsonObject>> groupByList(JsonArray countingResults) {

        Map<String, Map<String, JsonObject>> listIdToResults = new HashMap<>();

        Map<String, JsonObject> listsIds = getLists(countingResults);
        for (Map.Entry<String, JsonObject> listId : listsIds.entrySet()) {
            Map<String, JsonObject> resultValues = getCandidatesByListId(listId.getKey(), countingResults);
            listIdToResults.put(listId.getKey(), resultValues);
        }

        return listIdToResults;
    }

    private Map<String, JsonObject> getCandidatesByListId(String listId, JsonArray countingResults) {

        Map<String, JsonObject> candidates = new HashMap<>();

        for (JsonValue jsonValue : countingResults) {
            JsonObject result = JsonUtils.getJsonObject(jsonValue.toString());
            String representationType = result.getString("representationType");
            if ("CANDIDATE".equals(representationType)) {
                String list = result.getString("listId");
                if (list.equals(listId)) {
                    String question = result.getString("questionId");
                    candidates.put(question, result);
                }
            }
        }

        return candidates;
    }

    private Map<String, JsonObject> getLists(JsonArray countingResults) {
        Map<String, JsonObject> listsIds = new HashMap<>();
        for (int i = 0; i < countingResults.size(); i++) {
            JsonObject result = countingResults.getJsonObject(i);
            String representationType = result.getString("representationType");
            if ("LIST".equals(representationType)) {
                String id = result.getString("questionId");
                listsIds.put(id, result);
            }
        }

        return listsIds;
    }

    private Map<String, List<JsonObject>> getContestResultsMap(JsonArray countingResults) {

        Map<String, List<JsonObject>> contestResultsMap = new HashMap<>();

        for (int i = 0; i < countingResults.size(); i++) {

            JsonObject result = countingResults.getJsonObject(i);
            String contestId = result.getString("contestId");

            List<JsonObject> resultValue = contestResultsMap.get(contestId);
            if (resultValue == null) {
                resultValue = new ArrayList<>();
            }
            resultValue.add(result);
            contestResultsMap.put(contestId, resultValue);
        }

        return contestResultsMap;
    }

    @SuppressWarnings("unchecked")
    private Map<String, Map<String, String>> getIdToTextMap(JsonArray ballotTextJSON, String ballotId) throws IOException {

        Map<String, Map<String, String>> result = new HashMap<>();
        for (int i = 0; i < ballotTextJSON.size(); i++) {
            JsonObject jsonObject = ballotTextJSON.getJsonObject(i);
            JsonObject ballotObject = jsonObject.getJsonObject("ballot");
            String currentballotId = ballotObject.getString("id");

            if (ballotId.trim().equals(currentballotId.trim())) {
                JsonObject texts = jsonObject.getJsonObject("texts");
                return (Map<String, Map<String, String>>) ObjectMappers.fromJson(texts.toString(), Map.class);
            }
        }

        return result;
    }

    private JsonArray getBallotTextJson(String ballotId) {
        Map<String, Object> params = new HashMap<>();
        params.put("ballot.id", ballotId);
        String ballotText = ballotTextRepository.list(params);

        JsonObject ballotTextJSON = JsonUtils.getJsonObject(ballotText);
        return ballotTextJSON.getJsonArray("result");
    }

    private String getBallotBoxTitle(String ballotBoxId) throws ResourceNotFoundException {
        String ballotBoxJson = ballotBoxRepository.find(ballotBoxId);
        if (StringUtils.isEmpty(ballotBoxJson) || JsonConstants.JSON_EMPTY_OBJECT.equals(ballotBoxJson)) {
            throw new ResourceNotFoundException("Ballot box not found");
        }
        
        return JsonUtils.getJsonObject(ballotBoxJson).getString("defaultTitle");
    }

    private String getElectionEventTitle(String electionEventId) throws ResourceNotFoundException {
        String electionEventJson = electionEventRepository.find(electionEventId);
        if (StringUtils.isEmpty(electionEventJson) || JsonConstants.JSON_EMPTY_OBJECT.equals(electionEventJson)) {
            throw new ResourceNotFoundException("Electoral Event not found");
        }
        
        return JsonUtils.getJsonObject(electionEventJson).getString("defaultTitle");
    }

    private JsonObject getCountingJson(String electionEventId, String ballotBoxId) {
        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 countingResult = ballotBoxCountingRepository.find(attributeValueMap);
        
        return JsonUtils.getJsonObject(countingResult);
    }


    /**
     * Searchs the candidate alias in the content of a ballot
     * @param contestId contestID
     * @param ballot ballot ID
     * @param candidateId candidate ID
     * @return the alias of the candidate
     */
    private String getCandidateAlias(String contestId, JsonObject ballot, String candidateId) {

        final JsonArray jsonArray = ballot.getJsonArray(JsonConstants.JSON_ATTRIBUTE_NAME_CONTESTS);

        final JsonObject jsonObject = jsonArray.stream().map(x -> (JsonObject) x)
                .filter(node ->
                        node.getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID).equals(contestId))
                .flatMap(x -> x.getJsonArray(JsonConstants.JSON_ATTRIBUTE_NAME_OPTION_ATTRIBUTES).stream())
                .map(node -> (JsonObject) node)
                .filter(x -> x.getString(JsonConstants.JSON_ATTRIBUTE_NAME_ID).equals(candidateId))
                .collect(Collectors.toList()).get(FIRST_ELEMENT);

        return jsonObject.containsKey(JsonConstants.JSON_ATTRIBUTE_NAME_ALIAS)?
                jsonObject.getString(JsonConstants.JSON_ATTRIBUTE_NAME_ALIAS):CSV_EMPTY_STRING;


    }


}
