package br.net.cartoriodigital;

import rx.Observable;
import rx.Observer;
import rx.Subscriber;
import rx.observables.ConnectableObservable;
import rx.schedulers.Schedulers;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
import java.security.Signature;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.text.Normalizer;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import java.util.Enumeration;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;

import org.json.JSONArray;
import org.json.JSONObject;

public class Application {

  private static final String PATH_LOG = "C:\\ext\\hostlog.txt";

  private final AtomicBoolean interrompe;

  public static KeyStore keyStore;
  public static Provider provider;

  public Application() {
    this.interrompe = new AtomicBoolean(false);
  }

  public static void main(String[] args) {
    log("Starting the app...");

    final Application app = new Application();

    ConnectableObservable<String> obs = app.getObservable();
    obs.observeOn(Schedulers.computation()).subscribe(new Observer<String>() {
      public void onCompleted() {
      }

      public void onError(Throwable throwable) {
      }

      public void onNext(String s) {

        log("Host received " + s);

        JSONObject obj = new JSONObject(s);

        if (obj.getString("type").equals("sign")) {
          // Carrega provider
          try {
            for (int i = 1; i < 11; i++) {
              log("Trying to load provide: " + i);
              provider = Security.getProvider("SunMSCAPI");
              if (provider != null) {
                i = 11;
              }
              Thread.sleep(1000);
            }

            if (provider != null) {
              provider.setProperty("Signature.SHA1withRSA",
                  "sun.security.mscapi.RSASignature$SHA1");
              Security.addProvider(provider);
            }

            Security.addProvider(provider);

            // Carrega KeyStore
            KeyStore ks = KeyStore.getInstance("Windows-MY", provider);
            ks.load(null, null);
            log("KeyStore of provider " + provider.getName() + " loaded.");
            keyStore = ks;

            // Carrega Private Key
            log("Will load private key");
            String certAlias = getPublicKey(obj.getString("thumbprint"), false);
            Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initSign((PrivateKey) keyStore.getKey(certAlias, null));
            signature.update(s.getBytes());

            log("Will sign");

            // Assina Conteúdo
            String signedContent = Base64.getEncoder().encodeToString(signature.sign());

            JSONObject jsonObjSuccess = new JSONObject();
            jsonObjSuccess.put("success", true);
            jsonObjSuccess.put("signedContent", signedContent);

            app.sendMessage(jsonObjSuccess.toString());

            log("Sent");
          } catch (Exception e) {
            log("Erro inesperado dentro da iteracao");
            String sl = "{\"success\":false," + "\"message\":\"" + "invalid" + "\"}";
            try {
              app.sendMessage(sl);
            } catch (IOException ex) {
              ex.printStackTrace();
            }
          }
        } else if (obj.getString("type").equals("cert")) {
          try {
            KeyStore keyStore = KeyStore.getInstance("Windows-MY", "SunMSCAPI");
            keyStore.load(null, null);
            Enumeration<String> al = keyStore.aliases();

            JSONArray jsonArray = new JSONArray();

            Integer i = 0;
            while (al.hasMoreElements()) {
              i++;
              String alias = al.nextElement();
              X509Certificate cert = (X509Certificate) keyStore
                  .getCertificate(alias);

              log("Cert: " + alias + " / " + getThumbPrint(cert));

              JSONObject jsonObj = new JSONObject();

              String aliasWoInvalidChars = Normalizer.normalize(alias,
                  Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "");
              String issuerWoInvalidChars = Normalizer.normalize(
                  cert.getIssuerDN().getName(), Normalizer.Form.NFD)
                  .replaceAll("[^\\p{ASCII}]", "");

              LdapName ln = new LdapName(issuerWoInvalidChars);

              for (Rdn rdn : ln.getRdns()) {
                if (rdn.getType().equalsIgnoreCase("CN")) {
                  issuerWoInvalidChars = rdn.getValue().toString();
                  break;
                }
              }

              jsonObj.put("alias", aliasWoInvalidChars);
              jsonObj.put("issuer", issuerWoInvalidChars);
              jsonObj.put("expires", cert.getNotAfter());
              jsonObj.put("thumbprint", getThumbPrint(cert));

              jsonArray.put(jsonObj);
            }

            JSONObject jsonObjSuccess = new JSONObject();
            jsonObjSuccess.put("success", true);
            jsonObjSuccess.put("certificates", jsonArray);

            app.sendMessage(jsonObjSuccess.toString());
          } catch (IOException | KeyStoreException | NoSuchProviderException
              | NoSuchAlgorithmException | CertificateException
              | InvalidNameException e) {
            e.printStackTrace();
          }
        } else {
          String sl = "{\"success\":false," + "\"message\":\"" + "invalido" + "\"}";
          try {
            app.sendMessage(sl);
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    });

    obs.connect();

    while (!app.interrompe.get()) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        break;
      }
    }

    System.exit(0);
  }

  private ConnectableObservable<String> getObservable() {
    ConnectableObservable<String> observable = Observable
        .create(new Observable.OnSubscribe<String>() {
          public void call(Subscriber<? super String> subscriber) {
            subscriber.onStart();
            try {
              while (true) {
                String _s = readMessage(System.in);
                subscriber.onNext(_s);
              }
            } catch (InterruptedIOException ioe) {
              log("Blocked communication");
            } catch (Exception e) {
              subscriber.onError(e);
            }
            subscriber.onCompleted();
          }
        }).subscribeOn(Schedulers.io()).publish();

    observable.subscribe(new Observer<String>() {
      public void onCompleted() {
        log("App closed.");
        interrompe.set(true);
      }

      public void onError(Throwable throwable) {
        log("Unexpected error!");
        interrompe.set(true);
      }

      public void onNext(String s) {
      }
    });

    return observable;
  }

  private String readMessage(InputStream in) throws IOException {
    byte[] b = new byte[4];
    in.read(b);

    int size = getInt(b);
    log(String.format("The size is %d", size));

    if (size == 0) {
      throw new InterruptedIOException("Blocked communication");
    }

    b = new byte[size];
    in.read(b);

    return new String(b, "UTF-8");
  }

  private void sendMessage(String message) throws IOException {
    System.out.write(getBytes(message.length()));
    System.out.write(message.getBytes("UTF-8"));
    System.out.flush();
    log("mandou: " + message);
  }

  public int getInt(byte[] bytes) {
    return (bytes[3] << 24) & 0xff000000 | (bytes[2] << 16) & 0x00ff0000
        | (bytes[1] << 8) & 0x0000ff00 | (bytes[0] << 0) & 0x000000ff;
  }

  public byte[] getBytes(int length) {
    byte[] bytes = new byte[4];
    bytes[0] = (byte) (length & 0xFF);
    bytes[1] = (byte) ((length >> 8) & 0xFF);
    bytes[2] = (byte) ((length >> 16) & 0xFF);
    bytes[3] = (byte) ((length >> 24) & 0xFF);
    return bytes;
  }

  private static void log(String message) {
    File file = new File(PATH_LOG);

    try {
      if (!file.exists()) {
        file.createNewFile();
      }

      FileWriter fileWriter = new FileWriter(file.getAbsoluteFile(), true);
      BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);

      DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
      Date date = new Date();

      bufferedWriter.write(dateFormat.format(date) + ": " + message + "\r\n");
      bufferedWriter.close();
    } catch (Exception e) {
      log("ERROR ==> Method (log)" + e.getMessage());
      e.printStackTrace();
    }
  }

  public static String getThumbPrint(X509Certificate cert)
      throws NoSuchAlgorithmException, CertificateEncodingException {
    MessageDigest md = MessageDigest.getInstance("SHA-1");
    byte[] der = cert.getEncoded();
    md.update(der);
    byte[] digest = md.digest();
    return hexify(digest);
  }

  public static String hexify(byte bytes[]) {

    char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a',
        'b', 'c', 'd', 'e', 'f' };

    StringBuffer buf = new StringBuffer(bytes.length * 2);

    for (int i = 0; i < bytes.length; ++i) {
      buf.append(hexDigits[(bytes[i] & 0xf0) >> 4]);
      buf.append(hexDigits[bytes[i] & 0x0f]);
    }

    return buf.toString();
  }

  private static String stringHexa(byte[] bytes) {
    StringBuilder stringBuilder = new StringBuilder();

    try {
      for (int i = 0; i < bytes.length; i++) {
        int parteAlta = ((bytes[i] >> 4) & 0xf) << 4;
        int parteBaixa = bytes[i] & 0xf;
        if (parteAlta == 0) {
          stringBuilder.append('0');
        }

        stringBuilder.append(Integer.toHexString(parteAlta | parteBaixa));
      }
    } catch (Exception e) {
      log("ERROR ==> Method (stringHexa)" + e.getMessage());
      e.printStackTrace();
    }

    return stringBuilder.toString();
  }

  private static String getPublicKey(String certThumbprint, Boolean encoded)
      throws KeyStoreException, NoSuchProviderException {
    KeyStore keyStore = KeyStore.getInstance("Windows-MY", "SunMSCAPI");
    String encodedPublicKey = "";
    String aliasPublicKey = "";

    try {
      /* Load Windows KeyStore */
      keyStore.load(null, null);
      String selectedLocalPublicKey = "";
      Enumeration<String> al = keyStore.aliases();
      while (al.hasMoreElements()) {
        String alias = al.nextElement();
        X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias);

        if (getThumbPrint(cert).equals(certThumbprint)) {
          selectedLocalPublicKey = new String(Base64.getEncoder()
              .encodeToString((cert.getPublicKey().getEncoded())));
          selectedLocalPublicKey = "-----BEGIN PUBLIC KEY-----\n"
              + selectedLocalPublicKey.replaceAll("(.{64})", "$1\n")
              + "\n-----END PUBLIC KEY-----\n";
          aliasPublicKey = alias;
          log(alias);
        }
      }

      /* Encrypt PK */
      MessageDigest md = MessageDigest.getInstance("SHA-256");
      md.update(selectedLocalPublicKey.getBytes("UTF-8"));
      byte[] byteDigest = md.digest();

      encodedPublicKey = stringHexa(byteDigest);
    } catch (Exception e) {
      log("ERROR ==> Method (getPublicKey)" + e.getMessage());
      e.printStackTrace();
      return "";
    }

    if (encoded) {
      return encodedPublicKey;
    } else {
      return aliasPublicKey;
    }
  }
}