package com.reactnativesecp256k1urnm;

import android.util.Base64;

import androidx.annotation.NonNull;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

import org.bitcoin.NativeSecp256k1;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

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

public class Secp256k1UrnmExt extends ReactContextBaseJavaModule {
  public static final String NAME = "Secp256k1UrnmExt";

  private final Executor executor = Executors.newSingleThreadExecutor();

  SecureRandom GRandom = new SecureRandom();

  private static byte[] Ase(byte[] byteData, byte[] byteKey, int opmode, IvParameterSpec iv) throws Exception {
    Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
    SecretKeySpec skeySpec = new SecretKeySpec(byteKey, "AES");
    cipher.init(opmode, skeySpec, iv);
    return cipher.doFinal(byteData);
  }

  private void AesECDH(final String priv, final String pub, final byte[] data, final int mode, final Promise promise) {
    executor.execute(() -> {
      try {
        byte[] privraw = Base64.decode(priv, Base64.NO_PADDING);
        byte[] pubraw = Base64.decode(pub, Base64.NO_PADDING);
        byte[] sec = NativeSecp256k1.createECDHSecret(privraw, pubraw);
        byte[] eData = data;
        byte[] iv = new byte[]{
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        };
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        if (mode == Cipher.ENCRYPT_MODE) {
          int paddingLen = 16 - (data.length % 16);
          if (paddingLen < 2) {
            paddingLen += 16;
          }
          paddingLen--;
          byte[] random = new byte[paddingLen];
          GRandom.nextBytes(random);
          byte[] newData = new byte[random.length + data.length + 1];
          newData[0] = (byte) paddingLen;
          System.arraycopy(random, 0, newData, 1, random.length);
          System.arraycopy(data, 0, newData, 1 + random.length, data.length);
          eData = newData;
        }

        byte[] encryped = Ase(eData, sec, mode, ivSpec);

        if (mode == Cipher.ENCRYPT_MODE) {
          promise.resolve(Base64.encodeToString(encryped, Base64.NO_PADDING | Base64.NO_WRAP));
        } else {
          // DoUnpadding
          int dataStart = (int) encryped[0] + 1;
          int realLen = encryped.length - dataStart;
          byte[] realData = new byte[realLen];
          System.arraycopy(encryped, dataStart, realData, 0, realLen);
          promise.resolve(new String(realData, StandardCharsets.UTF_8));
        }
      } catch (Exception ex) {
        ex.printStackTrace();
        promise.reject("Error", ex.toString());
      }
    });
  }


  public Secp256k1UrnmExt(ReactApplicationContext reactContext) {
    super(reactContext);
  }

  @Override
  @NonNull
  public String getName() {
    return NAME;
  }

  @ReactMethod
  public void generateKey(final Promise promise) {
    executor.execute(() -> {
      try {
        byte[] privraw = new byte[32];
        do {
          GRandom.nextBytes(privraw);
        } while (!NativeSecp256k1.secKeyVerify(privraw));
        promise.resolve(Base64.encodeToString(privraw, Base64.NO_PADDING | Base64.NO_WRAP));
      } catch (Exception ex) {
        ex.printStackTrace();
        promise.reject("Error", ex.toString());
      }
    });
  }

  @ReactMethod
  public void encryptECDH(final String priv, final String pub, final String data, final Promise promise) {
    AesECDH(priv, pub, data.getBytes(StandardCharsets.UTF_8), Cipher.ENCRYPT_MODE, promise);
  }

  @ReactMethod
  public void decryptECDH(final String priv, final String pub, final String data, final Promise promise) {
    AesECDH(priv, pub, Base64.decode(data, Base64.NO_PADDING), Cipher.DECRYPT_MODE, promise);
  }
}
