/*
 * 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.channel.cc.orchestrator.mixdec.infrastructure.persistence;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import javax.inject.Inject;
import javax.json.JsonObject;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.opencsv.CSVReader;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.infrastructure.remote.ElectionInformationClient;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.infrastructure.remote.ResourceNotReadyException;
import com.scytl.products.ov.commons.beans.domain.model.ballotbox.BallotBoxId;
import com.scytl.products.ov.commons.beans.exceptions.ApplicationException;
import com.scytl.products.ov.commons.beans.exceptions.LambdaException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.dto.mixing.MixingPayload;
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.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.util.JsonUtils;

import okhttp3.ResponseBody;

public class CleansedBallotBoxRepository {

    private static final Logger LOG = LoggerFactory.getLogger("std");

    private static final int HTTP_NOT_FOUND_STATUS_CODE = 404;

    private static final int HTTP_PRECONDITION_FAILED = 412;

    private static final String PATH_CLEANSED_BALLOT_BOXES = "cleansedballotboxes";

    private static final String PATH_BALLOT_BOXES = "ballotboxes";

    @Inject
    private TrackIdInstance trackId;

    @Inject
    private ElectionInformationClient electionInformationClient;

    public List<String> getBallotBoxEncryptedVotes(final String tenantId, final String electionEventId,
            final String ballotBoxId)
            throws ResourceNotFoundException, ResourceNotReadyException, IOException {

        try (ResponseBody response =
            RetrofitConsumer.processResponse(electionInformationClient.getEncryptedBallotBox(trackId.getTrackId(),
                PATH_CLEANSED_BALLOT_BOXES, tenantId, electionEventId, ballotBoxId));
                CSVReader csvReader = new CSVReader(new InputStreamReader(response.byteStream()))) {

            List<String[]> csvLines = csvReader.readAll();

            List<String> encryptedVotes = new ArrayList<>();
            for (String[] csvLine : csvLines) {
                String line = StringUtils.join(csvLine, ";");
                if (StringUtils.isEmpty(line))
                    break;
                encryptedVotes.add(line);
            }

            return encryptedVotes;
        } catch (RetrofitException e) {
            if (e.getHttpCode() == HTTP_NOT_FOUND_STATUS_CODE) {
                throw new ResourceNotFoundException("Cleansed ballot box " + ballotBoxId + " is missing ", e);
            } else if (e.getHttpCode() == HTTP_PRECONDITION_FAILED) {
                throw new ResourceNotReadyException("Cleansed ballot box " + ballotBoxId + " not ready ", e);
            } else {
                throw e;
            }
        }
    }

    public Stream<MixingPayload> getMixingPayloads(BallotBoxId ballotBoxId)
            throws ResourceNotFoundException, ResourceNotReadyException, IOException {

        try {
            ResponseBody responseBody = RetrofitConsumer.processResponse(
                electionInformationClient.getMixingPayloads(trackId.getTrackId(), PATH_CLEANSED_BALLOT_BOXES,
                    ballotBoxId.getTenantId(), ballotBoxId.getElectionEventId(), ballotBoxId.getId()));

            // Return the stream of mixing payloads as read from the response.
            return convertStream(new ObjectInputStream(responseBody.byteStream()), MixingPayload.class);
        } catch (RetrofitException e) {
            switch (e.getHttpCode()) {
            case HTTP_NOT_FOUND_STATUS_CODE:
                throw new ResourceNotFoundException("Cleansed ballot box " + ballotBoxId + " is missing ", e);
            case HTTP_PRECONDITION_FAILED:
                throw new ResourceNotReadyException("Cleansed ballot box " + ballotBoxId + " not ready ", e);
            default:
                throw e;
            }
        }
    }

    public JsonObject getBallotBoxInformation(String tenantId, String electionEventId, String ballotBoxId)
            throws ApplicationException {
        try {
            String response =
                RetrofitConsumer.processResponse(electionInformationClient.getBallotBoxInformation(trackId.getTrackId(),
                    PATH_BALLOT_BOXES, tenantId, electionEventId, ballotBoxId));

            return JsonUtils.getJsonObject(response);

        } catch (ResourceNotFoundException e) {
            throw new ApplicationException("Error retrieving Ballot box information ", e);
        }
    }

    /**
     * Converts an ObjectInput to a stream of the required objects.
     *
     * @param oi
     *            the object input
     * @param clazz
     *            the class the deserialised objects must be
     * @param <T>
     *            the type of the deserialised objects
     * @return a stream of objects from the object input stream
     */
    private static <T> Stream<T> convertStream(ObjectInput oi, Class<T> clazz) {

        // Create a spliterator from the object input.
        Spliterator<T> spliterator = Spliterators.spliteratorUnknownSize(new Iterator<T>() {

            private T object;

            private boolean hasFinished;

            @Override
            public boolean hasNext() {
                if (!hasFinished) {
                    try {
                        object = clazz.cast(oi.readObject());
                    } catch (EOFException e) {
                        LOG.debug("No more objects", e);
                        object = null;
                    } catch (IOException e) {
                        throw new UncheckedIOException(e);
                    } catch (ClassNotFoundException e) {
                        throw new LambdaException(e);
                    }
                    hasFinished = true;
                }
                return object != null;
            }

            @Override
            public T next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                }
                hasFinished = false;
                return object;
            }
        }, Spliterator.DISTINCT | Spliterator.IMMUTABLE | Spliterator.ORDERED | Spliterator.NONNULL);

        // Return the stream based on the spliterator, closing the underlying
        // object input stream when this one closes.
        return StreamSupport.stream(spliterator, false).onClose(() -> {
            try {
                oi.close();
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
    }

}
