
package de.ivu.elect.business.security.boundary;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.util.Date;
import java.util.LinkedHashMap;

import javax.ejb.Stateless;
import javax.inject.Inject;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.CurrentCustomer;
import de.ivu.elect.business.gebietsbaum.boundary.GebietDAO;
import de.ivu.elect.business.gebietsbaum.boundary.NlGebietDarstellungNummerFormatter;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.NlGebietsart;
import de.ivu.elect.business.konfiguration.entity.ElectKonfigurationWerte;
import de.ivu.elect.business.konfiguration.entity.KonfigurationLoader;
import de.ivu.elect.business.security.control.NlKeyGeneratorConfig;
import de.ivu.elect.business.security.control.NlKeystoreHandlerConfig;
import de.ivu.elect.business.system.boundary.SystemLogWriter;
import de.ivu.elect.business.system.control.ElectPasswordGenerator;
import de.ivu.elect.business.system.entity.SystemLogQuelle;
import de.ivu.elect.business.wahl.boundary.WahlDAO;
import de.ivu.elect.business.wahl.entity.NlWahlCustomData;
import de.ivu.elect.business.wahl.entity.NlWahlart;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.WahlParameter;
import de.ivu.elect.business.wahl.entity.WahlParameterLoader;
import de.ivu.elect.business.wahl.entity.WahlParameterWerte;
import de.ivu.elect.encrypt.KeyGenerator;
import de.ivu.elect.encrypt.KeystoreHandler;
import de.ivu.elect.jee.faces.Messages;
import de.ivu.elect.jee.faces.UserLocale;
import de.ivu.elect.nl.x509.NlX509Metadata;


import static org.apache.commons.lang3.StringUtils.defaultIfBlank;

@Stateless
public class EmlSignKeyService implements Serializable {

  private static final Logger LOG = LoggerFactory.getLogger(EmlSignKeyService.class);

  private static final long serialVersionUID = 1L;

  private static final String KESTORE_FILENAME = "eml.p12"; // ELECTVAPP-1368

  private static final String ALIAS = "EML";

  public static final int PW_LAENGE = 20;

  private static final String[] CHARACTER_CLASSES = {
      "abcdefghkmnpqrstuvwxyz",
      "ABCDEFGHJKLMNPQRSTUVWXYZ",
      "23456789",
      "~!@#$%&*-+=()?/"
  };

  private static final int MAX_TRIES = 10;

  public static final String EML_CERTIFICATE_ID_HSB = "eml.certificate.id.hsb";

  public static final String EML_CERTIFICATE_ID_PSB = "eml.certificate.id.psb";

  @Inject
  private transient KonfigurationLoader konfigurationLoader;

  @Inject
  private transient WahlParameterLoader wahlParameterLoader;

  @Inject
  private transient SystemLogWriter systemLogWriter;

  @Inject
  private transient Messages messages;

  @Inject
  private transient WahlDAO wahlDAO;

  @Inject
  private transient GebietDAO gebietDAO;

  @Inject
  private transient CurrentCustomer currentCustomer;

  @Inject
  private transient NlGebietDarstellungNummerFormatter nlGebietDarstellungNummerFormatter;

  public PrivateKey loadPrivateKey(Wahl wahl) {
    try {
      return getKeystoreHandler()
          .readKeyPairFromKeyStore(ladeKeyStore(wahl), getConfigPassword(wahl).toCharArray(), ALIAS)
          .getPrivate();
    } catch (Exception e) {
      throw new RuntimeException(e.getMessage(), e);
    }
  }

  public boolean isPrivateKeyValid(Wahl wahl) {
    try {
      loadPrivateKey(wahl);
      return true;
    } catch (Exception e) {
      LOG.warn("Invalid: " + e.getMessage(), e);
    }
    return false;
  }

  private KeyStore ladeKeyStore(Wahl wahl) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException {
    try (FileInputStream fis = new FileInputStream(getKeyStoreFile(wahl))) {
      return getKeystoreHandler().loadOrCreateKeyStore(fis, getConfigPassword(wahl).toCharArray());
    }
  }

  public PublicKey loadPublicKey(Wahl wahl) {
    try {
      return getKeystoreHandler()
          .readKeyPairFromKeyStore(ladeKeyStore(wahl), getConfigPassword(wahl).toCharArray(), ALIAS).getPublic();
    } catch (Exception e) {
      throw new RuntimeException(e.getMessage(), e);
    }
  }

  public void createPrivateKey(Wahl wahlNotAttached) {
    try {
      LOG.info("Create EML PK" + wahlNotAttached);
      Wahl wahl = wahlDAO.attach(wahlNotAttached);
      String path = getConfigPath(wahl);

      if (StringUtils.isBlank(path)) {
        WahlParameter parameter = wahlParameterLoader.getParameter(wahl, WahlParameterWerte.NL_EML_KEYSTORE_PATH);
        if (parameter == null) {
          LOG.info("PK nicht benoetigt?");
          return;
        }
        path = getDefaultKeyStorePath(wahl);
        parameter.setWert(path);
      }

      File keystoreFile = new File(new File(path), KESTORE_FILENAME);
      LOG.info("KS-File: " + keystoreFile);

      String password = getConfigPassword(wahl);
      if (StringUtils.isBlank(password) || !keystoreFile.isFile()) {
        WahlParameter parameter = wahlParameterLoader.getParameter(wahl, WahlParameterWerte.NL_EML_KEYSTORE_PASSWORD);
        if (parameter == null) {
          LOG.info("PK nicht benoetigt?");
          return;
        }
        password = generatePasword();
        parameter.setWert(password);
        LOG.info("PW created");
      }

      KeystoreHandler keystoreHandler = getKeystoreHandler();
      KeyStore keyStore = null;
      if (keystoreFile.isFile()) {
        try (FileInputStream fis = new FileInputStream(keystoreFile)) {
          keyStore = keystoreHandler.loadOrCreateKeyStore(fis, password.toCharArray());
          KeyPair keyPair = keystoreHandler.readKeyPairFromKeyStore(keyStore, password.toCharArray(), ALIAS);
          if (keyPair != null) {
            LOG.info("Keystore valid");
            return; // alles gut, Keystore mit Passwort gültig
          }
        } catch (Exception e) {
          LOG.error("Keystore: " + path + ", " + e.getMessage(), e);
          systemLogWriter.writeErrorLogMessageFuerSystemMandant(messages.getMessageForMessageKey("nl.eml.keystore.error"), SystemLogQuelle.SYSTEM);
          return;
        }
      } else {
        LOG.info("Keystore file not found " + keystoreFile);
      }

      if (keyStore == null) {
        keyStore = keystoreHandler.loadOrCreateKeyStore(null, password.toCharArray());
      }
      new File(path).mkdirs(); // Verzeichnisse anlegen

      Date now = new Date();
      wahl.getCustomData(NlWahlCustomData.class).setPrivateKeyGenerationDate(now); // ELECTVAPP-1177
      KeyPair keyPair = getKeyGenerator().generateKeyPair();
      keystoreHandler.saveKeyPairToKeyStore(keyPair, keyStore, password.toCharArray(), ALIAS);
      try (FileOutputStream fos = new FileOutputStream(keystoreFile)) {
        keystoreHandler.saveKeyStore(keyStore, password.toCharArray(), fos);
      }
      LOG.info("Keystore created: " + now);
      loadPrivateKey(wahl);
      LOG.info("Keystore created and accessible");
    } catch (Exception e) {
      LOG.error("Wahl: " + wahlNotAttached + ", " + e.getMessage(), e);
      systemLogWriter.writeErrorLogMessageFuerSystemMandant(messages.getMessageForMessageKey("eml.private.key.creation.failed"), SystemLogQuelle.SYSTEM);
    }
  }

  private String getConfigPassword(Wahl wahl) {
    return wahlParameterLoader.resolveStringValue(wahl, WahlParameterWerte.NL_EML_KEYSTORE_PASSWORD);
  }

  private String getConfigPath(Wahl wahl) {
    return wahlParameterLoader.resolveStringValue(wahl, WahlParameterWerte.NL_EML_KEYSTORE_PATH);
  }

  private String generatePasword() {
    return new ElectPasswordGenerator(PW_LAENGE, CHARACTER_CLASSES).createPasswort(MAX_TRIES);
  }

  private String getDefaultKeyStorePath(Wahl wahl) {
    String ablage = konfigurationLoader.resolveStringValue(ElectKonfigurationWerte.ELECT_COMMON_DATEI_ABLAGE_VERZEICHNIS_PFAD);
    String systemId = konfigurationLoader.resolveStringValue(ElectKonfigurationWerte.SYSTEM_ID);
    String path = ablage + File.separator + "keystore" + File.separator + systemId + File.separator + wahl.getId() + File.separator + "EML" + File.separator;
    LOG.info("Default Keystore path: " + path);
    return path;
  }

  public File getKeyStoreFile(Wahl wahl) {
    File file = new File(new File(getConfigPath(wahl)), KESTORE_FILENAME);
    LOG.info("Keystore-File is: " + file + ", " + file.isFile());
    return file;
  }

  private KeystoreHandler getKeystoreHandler() {
    return new KeystoreHandler(new NlKeystoreHandlerConfig());
  }

  private KeyGenerator getKeyGenerator() {
    return new KeyGenerator(new NlKeyGeneratorConfig());
  }

  public void deleteKeystore(Wahl wahlNotAttched) {
    try {
      boolean result = FileUtils.deleteQuietly(getKeyStoreFile(wahlNotAttched));
      LOG.info("Keystore deleted: " + wahlNotAttched.getId() + ", " + result);
    } catch (Exception e) {
      LOG.warn("Keystore not deleted: " + wahlNotAttched.getId());
    }
  }

  public void downloadPublicKey(Wahl wahlNotAttached) {
    Wahl wahl = wahlDAO.attach(wahlNotAttached);
    wahl.getCustomData(NlWahlCustomData.class).setPublicKeyExported(true); // ELECTVAPP-1143
  }

  public static LinkedHashMap<String, String> collectNlX509MetadataFormatted(Messages messages, UserLocale userLocale, NlX509Metadata nlX509Metadata, String defaultBlank) {
    LinkedHashMap<String, String> result = new LinkedHashMap<>();

    result.put(messages.getMessageForMessageKey("eml.certificate.label.country.name"), defaultIfBlank(nlX509Metadata.getCountryName(), defaultBlank));
    result.put(messages.getMessageForMessageKey("eml.certificate.label.locality.name"), defaultIfBlank(nlX509Metadata.getLocalityName(), defaultBlank));
    result.put(messages.getMessageForMessageKey("eml.certificate.label.organisation.name"), defaultIfBlank(nlX509Metadata.getOrganisationName(), defaultBlank));
    result.put(messages.getMessageForMessageKey("eml.certificate.label.organisation.unit"), defaultIfBlank(nlX509Metadata.getOrganisationUnit(), defaultBlank));
    result.put(messages.getMessageForMessageKey("eml.certificate.label.type.region"), defaultIfBlank(nlX509Metadata.getOrganisationTypeAndRegionName(), defaultBlank));
    result.put(messages.getMessageForMessageKey("eml.certificate.label.uid"), defaultIfBlank(nlX509Metadata.getIdentifier(), defaultBlank));
    result.put(messages.getMessageForMessageKey("eml.certificate.label.valid.from"), nlX509Metadata.getValidFrom() == null ? defaultBlank : userLocale.formatDate(nlX509Metadata.getValidFrom()));
    result.put(messages.getMessageForMessageKey("eml.certificate.label.valid"), nlX509Metadata.getValidTill() == null ? defaultBlank : userLocale.formatDate(nlX509Metadata.getValidTill()));
    result.put(messages.getMessageForMessageKey("eml.certificate.label.signAlgorithm"), defaultIfBlank(nlX509Metadata.getSignAlgorithm(), defaultBlank));

    return result;
  }

  public NlX509Metadata getNlX509Metadata(Wahl wahlNotAttached) {
    Wahl wahl = wahlDAO.attach(wahlNotAttached);
    Gebiet wurzelgebiet = gebietDAO.findByWahlWurzelgebiet(wahl);

    // ELECTVAPP-973
    String countryName = messages.getMessageForMessageKey("eml.certificate.country");
    String localityName = wurzelgebiet.getName();
    String organisationName = wahl.getKennzeichen();
    String organisationUnit = messages.getMessageForMessageKey("eml.certificate.organisation.unit");

    String organisationTypeAndRegionName = bestimmeTypeAndName(wahl, wurzelgebiet);
    String identifier = bestimmeIdentifier(wahl, wurzelgebiet);
    Date validFrom = wahl.getCustomData(NlWahlCustomData.class).getPrivateKeyGenerationDate(); // ELECTVAPP-1177
    if (validFrom == null) {
      // legacy
      validFrom = DateUtils.addWeeks(wahl.getWahldatum(), -4 /* ELECTVAPP-978 */);
    }
    Date validTill = DateUtils.addMonths(wahl.getWahldatum(), 3 /* ELECTVAPP-973 */);
    String signAlgorithm = "SHA256withRSA";

    return new NlX509Metadata(countryName,
        localityName,
        organisationName,
        organisationUnit,
        organisationTypeAndRegionName,
        identifier,
        validFrom,
        validTill,
        signAlgorithm);
  }

  private String bestimmeIdentifier(Wahl wahl, Gebiet wurzelgebiet) {
    String nummer = nlGebietDarstellungNummerFormatter.formatiereNummer(wurzelgebiet);
    if (NlWahlart.isWasHSB(wahl, currentCustomer.getCustomer())) {
      return messages.getMessageForMessageKey(EML_CERTIFICATE_ID_HSB, nummer);
    }
    if (NlWahlart.isWasPSB(wahl, currentCustomer.getCustomer())) {
      return messages.getMessageForMessageKey(EML_CERTIFICATE_ID_PSB, nummer);
    }

    return "";
  }

  private String bestimmeTypeAndName(Wahl wahl, Gebiet wurzelgebiet) {
    // TK...
    if (NlWahlart.isWasHSB(wahl, currentCustomer.getCustomer()) && NlGebietsart.from(wurzelgebiet).isKieskring()) {
      return messages.getMessageForMessageKey("eml.certificate.type.name.hsb.kieskring", wurzelgebiet.getName());
    }
    if (NlWahlart.isWasPSB(wahl, currentCustomer.getCustomer()) && NlGebietsart.from(wurzelgebiet).isGemeinde()) {
      return messages.getMessageForMessageKey("eml.certificate.type.name.psb.gemeinde", wurzelgebiet.getName());
    }

    return "";
  }

}
