package common.util;

import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyAgreement;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import common.log.Log;

public class EncryptUtil {
	public static final String KEY_ALGO_NAME = "tcr-ed25519";
	public static final String KEY_ALGO_DISPLAY = "Ed25519";
	
	private static final long CRC24_INIT = 0xB704CEL;
	private static final long CRC24_POLY = 0x1864CFBL;

	public static SecretKey makeKeyAgreement(PrivateKey key, PublicKey pubkey) {
		try {
			KeyAgreement agreement = KeyAgreement.getInstance("X25519");
            agreement.init(key);
			agreement.doPhase(pubkey, true);
			return new SecretKeySpec(agreement.generateSecret(), 0, 16, "AES");
		}
		catch(NoSuchAlgorithmException | InvalidKeyException e) {
			Log.SYSTEM.error(e, "Konnte Diffie-Hellman-Schlüsselaushandlung nicht absolvieren");
			return null;
		}
	}

	public static KeyPair createDHKeypair() {
		try {
			KeyPairGenerator pairgen = KeyPairGenerator.getInstance("X25519");
			return pairgen.generateKeyPair();
		}
		catch(NoSuchAlgorithmException e) {
			Log.SYSTEM.error(e, "Konnte Schlüsselpaar für Diffie-Hellman nicht generieren");
			return null;
		}
	}

	public static KeyPair createKeypair() {
		try {
			KeyPairGenerator pairgen = KeyPairGenerator.getInstance("Ed25519");
			return pairgen.generateKeyPair();
		}
		catch(NoSuchAlgorithmException e) {
			Log.SYSTEM.error(e, "Konnte Schlüsselpaar nicht generieren");
			return null;
		}
	}
	
	public static byte[] createSignature(PrivateKey key, byte[] token) {
	    try {
			Signature sig = Signature.getInstance("Ed25519");
		    sig.initSign(key);
		    sig.update(token);
			return sig.sign();
		}
		catch(SignatureException | InvalidKeyException | NoSuchAlgorithmException e) {
			Log.SYSTEM.error(e, "Konnte Signatur nicht generieren");
			return null;
		}
	}
	
	public static boolean verifySignature(PublicKey key, byte[] token, byte[] signature) {
	    try {
			Signature sig = Signature.getInstance("Ed25519");
		    sig.initVerify(key);
		    sig.update(token);
			return sig.verify(signature);
		}
		catch(SignatureException | InvalidKeyException | NoSuchAlgorithmException e) {
			Log.SYSTEM.error(e, "Konnte Signatur nicht verifizieren");
			return false;
		}
	}
	
	public static PublicKey decodeDHPublicKey(byte[] encoded) {
		try {
			EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
			KeyFactory factory = KeyFactory.getInstance("X25519");
			return factory.generatePublic(spec);
		}
		catch(NoSuchAlgorithmException | InvalidKeySpecException e) {
			Log.SYSTEM.error(e, "Öffentlicher Schlüssel für Diffie-Hellman konnte nicht dekodiert werden");
			return null;
		}
	}
	
	public static PublicKey decodePublicKey(byte[] encoded) {
		try {
			EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
			KeyFactory factory = KeyFactory.getInstance("Ed25519");
			return factory.generatePublic(spec);
		}
		catch(NoSuchAlgorithmException | InvalidKeySpecException e) {
			Log.SYSTEM.error(e, "Öffentlicher Schlüssel konnte nicht dekodiert werden");
			return null;
		}
	}
	
	public static PrivateKey decodePrivateKey(byte[] encoded) {
		try {
			EncodedKeySpec spec = new PKCS8EncodedKeySpec(encoded);
			KeyFactory factory = KeyFactory.getInstance("Ed25519");
			return factory.generatePrivate(spec);
		}
		catch(NoSuchAlgorithmException | InvalidKeySpecException e) {
			Log.SYSTEM.error(e, "Privater Schlüssel konnte nicht dekodiert werden");
			return null;
		}
	}

	public static byte[] encryptData(Key key, byte[] data) {
		return cipher(Cipher.ENCRYPT_MODE, key, data);
	}

	public static byte[] decryptData(Key key, byte[] data) {
		return cipher(Cipher.DECRYPT_MODE, key, data);
	}

	private static byte[] cipher(int mode, Key key, byte[] data) {
		try {
			return createCipher(mode, key.getAlgorithm(), key).doFinal(data);
		}
		catch(IllegalBlockSizeException | BadPaddingException e) {
			Log.SYSTEM.error(e, "Konnte Daten nicht ver- oder entschlüsseln");
			return null;
		}
	}

	private static Cipher createCipher(int mode, String transformation, Key key) {
		try {
			Cipher cipher = Cipher.getInstance(transformation);
			cipher.init(mode, key);
			return cipher;
		}
		catch(InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException e) {
			Log.SYSTEM.error(e, "Konnte Verschlüsselungsverfahren nicht initialisieren");
			return null;
		}
	}

	public static Cipher createCipher(int mode, Key key) {
		try {
			Cipher cipher = Cipher.getInstance("AES/CFB8/NoPadding");
			cipher.init(mode, (Key)key, (AlgorithmParameterSpec)(new IvParameterSpec(key.getEncoded())));
			return cipher;
		}
		catch(GeneralSecurityException e) {
			throw new RuntimeException(e);
		}
	}
	
	public static String getXorSha512Hash(byte[] data) {
		byte[] hash;
		try {
			MessageDigest digest = MessageDigest.getInstance("SHA-512");
			hash = digest.digest(data);
		}
		catch(NoSuchAlgorithmException e) {
			Log.SYSTEM.error(e, "Konnte Schlüssel-Prüfwert nicht berechnen");
			return "<?>";
		}
		byte[] xor = new byte[8];
		for(int z = 0; z < hash.length / xor.length; z++) {
			for(int n = 0; n < xor.length; n++) {
				xor[n] ^= hash[z * xor.length + n];
			}
		}
		return Util.getHexString(xor);
	}
	
	public static String getArmoredPubkey(PublicKey pubkey, String cn) {
		StringBuilder sb = new StringBuilder(EncryptUtil.KEY_ALGO_NAME + " ");
		sb.append(Base64.getEncoder().encodeToString(pubkey.getEncoded()));
		sb.append(' ').append(Base64.getEncoder().encodeToString(crc24(pubkey.getEncoded())));
		return cn == null || cn.isEmpty() ? sb.toString() : sb.append(' ').append(Util.sanitizeCommonName(cn)).toString();
	}
	
	public static Pair<PublicKey, String> parseArmoredPubkey(String armor) throws IllegalArgumentException {
		String[] tok = armor.trim().split(" ");
		if(tok.length != 3 && tok.length != 4)
			throw new IllegalArgumentException("Key muss aus 3 oder 4 Segmenten bestehen");
		if(!tok[0].equals(EncryptUtil.KEY_ALGO_NAME))
			throw new IllegalArgumentException("Algorithmus '" + tok[0] + "' ist nicht unterstützt, es wird derzeit nur " + EncryptUtil.KEY_ALGO_NAME + " verwendet");
		byte[] key;
		try {
			key = Base64.getDecoder().decode(tok[1]);
		}
		catch(IllegalArgumentException e) {
			throw new IllegalArgumentException("Schlüssel ist nicht im Base64-Format", e);
		}
		byte[] hash;
		try {
			hash = Base64.getDecoder().decode(tok[2]);
		}
		catch(IllegalArgumentException e) {
			throw new IllegalArgumentException("Prüfwert ist nicht im Base64-Format", e);
		}
		if(hash.length != 3)
			throw new IllegalArgumentException("Prüfwert hat die falsche Länge, erwarte 3 Bytes, habe " + hash.length + " Bytes");
		byte[] keyHash = crc24(key);
		if(!Arrays.equals(hash, keyHash))
			throw new IllegalArgumentException("Prüfwert ist falsch, erwarte " + Util.getHexString(hash) + ", habe " + Util.getHexString(keyHash));
		PublicKey pubkey;
		try {
			EncodedKeySpec spec = new X509EncodedKeySpec(key);
			KeyFactory factory = KeyFactory.getInstance("Ed25519");
			pubkey = factory.generatePublic(spec);
		}
		catch(NoSuchAlgorithmException | InvalidKeySpecException e) {
			throw new IllegalArgumentException("Öffentlicher Schlüssel konnte nicht dekodiert werden", e);
		}
		if(tok.length == 4 && !Util.isValidCommonName(tok[3]))
			throw new IllegalArgumentException("Name muss aus 'a-z' '0-9' und '-' bestehen, kann keine aufeinander folgenden Bindestriche haben und darf nicht damit beginnen oder darin enden");
		return new Pair<PublicKey, String>(pubkey, tok.length == 4 ? tok[3] : null);
	}

	private static byte[] crc24(byte[] data) {
		long crc = CRC24_INIT;
		for(byte bt : data) {
			crc ^= (long)bt << 16;
			for(int z = 0; z < 8; z++) {
				crc <<= 1;
				if((crc & 0x1000000) != 0)
					crc ^= CRC24_POLY;
			}
		}
		int value = (int)(crc & 0xFFFFFFL);
		return new byte[] {(byte)((value >> 16) & 0xff), (byte)((value >> 8) & 0xff), (byte)(value & 0xff)};
	}
	
	public static byte[] hashPassword(String password, byte[] salt) {
		try {
			KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 256);
			SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
			return factory.generateSecret(spec).getEncoded();
		}
		catch(NoSuchAlgorithmException | InvalidKeySpecException e) {
			Log.SYSTEM.error(e, "Konnte Passwort-Prüfwert nicht berechnen");
			return null;
		}
	}
	
	public static Pair<byte[], byte[]> hashPassword(String password) {
		SecureRandom rand = new SecureRandom();
		byte[] salt = new byte[64];
		rand.nextBytes(salt);
		return new Pair<byte[], byte[]>(hashPassword(password, salt), salt);
	}
}
