/*
 * 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.choices.codes;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import javax.crypto.SecretKey;

import org.apache.commons.lang3.StringUtils;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.primitives.PrimitivesServiceAPI;
import com.scytl.cryptolib.api.symmetric.SymmetricServiceAPI;
import com.scytl.cryptolib.derivation.cryptoapi.CryptoAPIDerivedKey;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.products.ov.config.exceptions.CreateVotingCardSetException;
import com.scytl.products.ov.config.exceptions.specific.GenerateVerificationCardCodesException;
import com.scytl.products.ov.constants.Constants;

public final class ChoicesCodesGenerator implements ChoicesCodesGenerationAPI {

    private final PrimitivesServiceAPI primitivesService;

    private final SymmetricServiceAPI symmetricService;

    public ChoicesCodesGenerator(final PrimitivesServiceAPI primitivesService,
            final SymmetricServiceAPI symmetricService) {

        this.primitivesService = primitivesService;
        this.symmetricService = symmetricService;
    }

    @Override
    public String generateVoteCastingCode() {
        return getRandomNumber(Constants.NUM_DIGITS_VOTE_CASTING_CODE);
    }

    @Override
    public String generateShortChoiceCode() {
        return getRandomNumber(Constants.NUM_DIGITS_SHORT_CHOICE_CODE);
    }

    @Override
    public byte[] generateLongChoiceCode(String eeid, String verificationCardId, ZpGroupElement partialCode,
            List<String> attributesWithCorrectness) {

        List<String> inputs = new ArrayList<>();
        inputs.add(partialCode.getValue().toString());
        inputs.add(verificationCardId);
        inputs.add(eeid);
        inputs.addAll(attributesWithCorrectness);
        String inputString = String.join(Constants.EMPTY, inputs);
        byte[] inputData = inputString.getBytes(StandardCharsets.UTF_8);
        try {
            return primitivesService.getHash(inputData);
        } catch (GeneralCryptoLibException e) {
            throw new GenerateVerificationCardCodesException(
                "An error occurred while trying to generate a long choices code", e);
        }
    }

    @Override
    public ChoicesCodesMappingEntry generateMapping(final byte[] mappingData, final byte[] longCode,
            final SecretKey codesKey) {
        byte[] codesKeyBytes = codesKey.getEncoded();
        int codesKeyBytesLength = codesKeyBytes.length;
        int longCodeLength = longCode.length;
        byte[] derivedKeySeed = new byte[longCodeLength + codesKeyBytesLength];
        System.arraycopy(longCode, 0, derivedKeySeed, 0, longCodeLength);
        System.arraycopy(codesKeyBytes, 0, derivedKeySeed, longCodeLength, codesKeyBytesLength);

        CryptoAPIDerivedKey derivedKey;
        try {
            byte[] derivedKeySeedHash = primitivesService.getHash(derivedKeySeed);
            derivedKey = primitivesService.getKDFDeriver().deriveKey(derivedKeySeedHash, derivedKeySeedHash.length);
        } catch (GeneralCryptoLibException e) {
            throw new CreateVotingCardSetException("An error occurred while trying to derive a key: " + e.getMessage(),
                e);
        }

        SecretKey mappingDataEncryptionKey;
        try {
            mappingDataEncryptionKey = symmetricService.getSecretKeyForEncryptionFromDerivedKey(derivedKey);
        } catch (GeneralCryptoLibException e) {
            throw new CreateVotingCardSetException(
                "An error occurred while trying to create a secret key from derived data: " + e.getMessage(), e);
        }

        byte[] encryptedmappingData;
        try {
            encryptedmappingData = symmetricService.encrypt(mappingDataEncryptionKey, mappingData);
        } catch (GeneralCryptoLibException e) {
            throw new CreateVotingCardSetException(
                "An error occurred while trying to encrypt mData using the derived secret key: " + e.getMessage(), e);
        }

        byte[] hashedLongCodeBytes;
        try {
            hashedLongCodeBytes = primitivesService.getHash(longCode);
        } catch (GeneralCryptoLibException e) {
            throw new CreateVotingCardSetException(
                "An error occurred while trying generate a hash of a key: " + e.getMessage(), e);
        }

        return new ChoicesCodesMappingEntry(hashedLongCodeBytes, encryptedmappingData);
    }

    private String getRandomNumber(final int numDigits) {

        try {
            String randomNumber = primitivesService.getCryptoRandomInteger().nextRandom(numDigits).toString();
            return StringUtils.leftPad(randomNumber, numDigits, '0');
        } catch (GeneralCryptoLibException e) {
            throw new CreateVotingCardSetException(
                "Error while attempting to generate a random number with " + numDigits + " digits: " + e, e);
        }
    }
}
