package com.intellij.util.net.ssl;

import java.util.HashMap;
import org.apache.commons.codec.digest.DigestUtils;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;

import javax.security.auth.x500.X500Principal;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Date;
import java.util.Map;

/**
 * @author Mikhail Golubev
 */
@SuppressWarnings("UnusedDeclaration")
public class CertificateWrapper {
  @NonNls public static final String NOT_AVAILABLE = "N/A";

  private final X509Certificate myCertificate;
  private final Map<String, String> myIssuerFields;
  private final Map<String, String> mySubjectFields;

  public CertificateWrapper(@NotNull X509Certificate certificate) {
    myCertificate = certificate;
    myIssuerFields = extractFields(certificate.getIssuerX500Principal());
    mySubjectFields = extractFields(certificate.getSubjectX500Principal());
  }

  /**
   * @param name - Common name of desired issuer field
   * @return field value of {@link #NOT_AVAILABLE}. if it doesn't exist
   */
  @NotNull
  public String getIssuerField(@NotNull CommonField name) {
    String field = myIssuerFields.get(name.getShortName());
    return field == null ? NOT_AVAILABLE : field;
  }

  /**
   * @param name - Common name of desired subject field
   * @return field value of {@link #NOT_AVAILABLE}, if it doesn't exist
   */
  @NotNull
  public String getSubjectField(@NotNull CommonField name) {
    String field = mySubjectFields.get(name.getShortName());
    return field == null ? NOT_AVAILABLE : field;
  }

  /**
   * Returns SHA-256 fingerprint of the certificate.
   *
   * @return SHA-256 fingerprint or {@link #NOT_AVAILABLE} in case of any error
   */
  @NotNull
  public String getSha256Fingerprint() {
    try {
      return DigestUtils.sha256Hex(myCertificate.getEncoded());
    }
    catch (CertificateEncodingException e) {
      return NOT_AVAILABLE;
    }
  }

  /**
   * Returns SHA-1 fingerprint of the certificate.
   *
   * @return SHA-1 fingerprint or {@link #NOT_AVAILABLE} in case of any error
   */
  public String getSha1Fingerprint() {
    try {
      return DigestUtils.sha1Hex(myCertificate.getEncoded());
    }
    catch (Exception e) {
      return NOT_AVAILABLE;
    }
  }

  /**
   * Check whether certificate is valid. It's considered invalid it it either expired or
   * not yet valid.
   *
   * @see #isExpired()
   * @see #isNotYetValid()
   */
  public boolean isValid() {
    try {
      myCertificate.checkValidity();
      return true;
    }
    catch (Exception e) {
      return false;
    }
  }

  public boolean isExpired() {
    return new Date().getTime() > myCertificate.getNotAfter().getTime();
  }

  public boolean isNotYetValid() {
    return new Date().getTime() < myCertificate.getNotBefore().getTime();
  }

  /**
   * Check whether certificate is self-signed. It's considered self-signed if
   * its issuer and subject are the same.
   */
  public boolean isSelfSigned() {
    return myCertificate.getIssuerX500Principal().equals(myCertificate.getSubjectX500Principal());
  }

  public int getVersion() {
    return myCertificate.getVersion();
  }

  @NotNull
  public String getSerialNumber() {
    return myCertificate.getSerialNumber().toString();
  }

  public X509Certificate getCertificate() {
    return myCertificate;
  }

  public X500Principal getIssuerX500Principal() {
    return myCertificate.getIssuerX500Principal();
  }

  public X500Principal getSubjectX500Principal() {
    return myCertificate.getSubjectX500Principal();
  }

  public Date getNotBefore() {
    return myCertificate.getNotBefore();
  }

  public Date getNotAfter() {
    return myCertificate.getNotAfter();
  }

  public Map<String, String> getIssuerFields() {
    return myIssuerFields;
  }

  public Map<String, String> getSubjectFields() {
    return mySubjectFields;
  }

  // E.g. CN=*.github.com,O=GitHub\, Inc.,L=San Francisco,ST=California,C=US
  private static Map<String, String> extractFields(X500Principal principal) {
    Map<String, String> fields = new HashMap<>();
    for (String field : principal.getName().split("(?<!\\\\),")) {
      String[] parts = field.trim().split("=", 2);
      if (parts.length != 2) {
        continue;
      }
      fields.put(parts[0], parts[1].replaceAll("\\\\,", ","));
    }
    return Collections.unmodifiableMap(fields);
  }

  @Override
  public final boolean equals(Object other) {
    return other instanceof CertificateWrapper && myCertificate.equals(((CertificateWrapper)other).getCertificate());
  }

  @Override
  public final int hashCode() {
    return myCertificate.hashCode();
  }

  /**
   * Find out full list of names from specification.
   * See http://tools.ietf.org/html/rfc5280#section-4.1.2.2 for details.
   */
  public enum CommonField {
    COMMON_NAME("CN", "Common Name"),
    ORGANIZATION("O", "Organization"),
    ORGANIZATION_UNIT("OU", "Organizational Unit"),
    LOCATION("L", "Locality"),
    COUNTRY("C", "Country"),
    STATE("ST", "State or Province");

    private final String myShortName;
    private final String myLongName;

    CommonField(@NotNull String shortName, @NotNull String longName) {
      myShortName = shortName;
      myLongName = longName;
    }

    public String getShortName() {
      return myShortName;
    }

    public String getLongName() {
      return myLongName;
    }
  }
}
