package ru.yandex.webmaster3.storage.searchquery.download;

import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Optional;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import com.google.protobuf.InvalidProtocolBufferException;
import org.jetbrains.annotations.NotNull;
import org.joda.time.Instant;
import org.joda.time.Seconds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.webmaster3.core.util.ByteStreamUtil;
import ru.yandex.webmaster3.core.util.TimeUtils;
import ru.yandex.webmaster3.proto.searchquery.download.CsvRequest;

/**
 * @author aherman
 */
public abstract class CSVRequestEncoder {
    private static final Logger log = LoggerFactory.getLogger(CSVRequestEncoder.class);

    private static final long CYPHERING_SALT = 0xE88F170C51161B05L;

    private static final long HMAC_SALT = 0x2AD1FF33CA58120BL;
    private static final long IV_HI = 0x43D3136E33055919L;
    private static final long IV_LO = 0x92188591DB8C4212L;

    private static final String HMAC_NAME = "HmacSHA256";

    public static final int HMAC_LENGTH_BYTES = 256/8;
    private static final String CIPHER_AES_OFB_PKCS5PADDING = "AES/OFB/PKCS5Padding";
    private static final String PROVIDER_SUNJCE = "SunJCE";
    private static final String KEY_AES = "AES";

    public static Optional<CsvRequest.CSVRequestOrBuilder> decode(long userId, Instant now, int requestTTLSeconds,
            EncodedCSVRequest encodedCSVRequest) throws GeneralSecurityException, InvalidProtocolBufferException
    {
        Instant requestTime = TimeUtils.unixTimestampToInstant(encodedCSVRequest.getRequestDate());
        Seconds secondsBetween = Seconds.secondsBetween(requestTime, now);
        if (secondsBetween.getSeconds() > requestTTLSeconds) {
            log.info("Expired link");
            return Optional.empty();
        }

        byte[] expectedHMAC = createHMAC(userId, encodedCSVRequest.getRequestDate(), encodedCSVRequest.getCipheredRequest());
        byte[] actualHMAC = encodedCSVRequest.getHmac();

        if (!Arrays.equals(expectedHMAC, actualHMAC)) {
            log.info("HMAC are different");
            return Optional.empty();
        }
        byte[] requestBytes = decipherRequest(userId, encodedCSVRequest.getRequestDate(), encodedCSVRequest.getCipheredRequest());
        return Optional.of(CsvRequest.CSVRequest.parseFrom(requestBytes));
    }

    public static EncodedCSVRequest encode(long userId, Instant requestDate, CsvRequest.CSVRequest request)
            throws GeneralSecurityException
    {
        int requestTimestamp = TimeUtils.instantToUnixTimestamp(requestDate);

        byte[] requestBytes = request.toByteArray();
        byte[] cipheredRequestBytes = cipherRequest(userId, requestTimestamp, requestBytes);

        byte[] hashCode = createHMAC(userId, requestTimestamp, cipheredRequestBytes);

        return new EncodedCSVRequest(requestTimestamp, cipheredRequestBytes, hashCode);
    }

    private static byte[] createHMAC(long userId, int requestTimestamp, byte[] cipheredRequestBytes)
            throws NoSuchAlgorithmException, InvalidKeyException
    {
        SecretKeySpec hmacKey = createKey(userId, HMAC_SALT);
        Mac mac = Mac.getInstance(HMAC_NAME);
        mac.init(hmacKey);
        byte[] buffer = new byte[8];
        ByteStreamUtil.writeLongLE(buffer, 0, userId);
        mac.update(buffer, 0, 8);
        ByteStreamUtil.writeIntLE(buffer, 0, requestTimestamp);
        mac.update(buffer, 0, 4);
        return mac.doFinal(cipheredRequestBytes);
    }

    private static byte[] cipherRequest(long userId, int requestDate, byte[] bytes) throws GeneralSecurityException {
        Cipher cipher = createCipher(userId, requestDate, Cipher.ENCRYPT_MODE);
        return cipher.doFinal(bytes);
    }

    private static byte[] decipherRequest(long userId, int requestDate, byte[] bytes) throws GeneralSecurityException {
        Cipher cipher = createCipher(userId, requestDate, Cipher.DECRYPT_MODE);
        return cipher.doFinal(bytes);
    }

    @NotNull
    private static Cipher createCipher(long userId, int requestDate, int mode) throws GeneralSecurityException {
        SecretKeySpec key = createKey(userId, CYPHERING_SALT);

        Cipher cipher = Cipher.getInstance(CIPHER_AES_OFB_PKCS5PADDING, PROVIDER_SUNJCE);
        byte[] ivBytes = new byte[16];
        ByteStreamUtil.writeLongLE(ivBytes, 0, IV_HI);
        ByteStreamUtil.writeLongLE(ivBytes, 8, IV_LO ^ (long) requestDate);
        IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);

        cipher.init(mode, key, ivParameterSpec);
        return cipher;
    }

    private static SecretKeySpec createKey(long userId, long salt) {
        byte[] keyBytes = new byte[8 + 8];
        ByteStreamUtil.writeLongLE(keyBytes, 0, salt);
        ByteStreamUtil.writeLongLE(keyBytes, 8, userId);
        return new SecretKeySpec(keyBytes, KEY_AES);
    }
}
