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

import java.util.List;
import java.util.stream.Collectors;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.benutzerverwaltung.boundary.BenutzerDAO;
import de.ivu.elect.business.benutzerverwaltung.entity.Benutzer;
import de.ivu.elect.business.konfiguration.entity.KonfigurationLoader;
import de.ivu.elect.business.mandant.boundary.MandantDAO;
import de.ivu.elect.business.mandant.entity.Mandant;
import de.ivu.elect.business.security.entity.PasswordStoreType;
import de.ivu.elect.jee.security.AuthentificationFailedException;
import de.ivu.elect.jee.security.LoginData;
import de.ivu.elect.jee.security.LoginGruppe;
import de.ivu.elect.jee.security.LoginServiceContext;
import de.ivu.elect.jee.security.PasswordEncryptionService;


import static de.ivu.elect.business.konfiguration.entity.MandantKonfigurationWerte.LOGIN_SERVICE_PASSWORD_STORE_TYPE;

@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public class DatabaseLoginServiceEJB {

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

  public static final String IGNORIERE_GESPERRT_KEY = "DatabaseLoginServiceEJB.IGNORIERE_GESPERRT_KEY"; // ELECTKOM-1713

  public static final String ERHOEHE_FEHLVERSUCHE_NICHT_KEY = "DatabaseLoginServiceEJB.ERHOEHE_FEHLVERSUCHE_NICHT_KEY"; // ELECTKOM-1713

  @Inject
  private transient BenutzerDAO benutzerDAO;

  @Inject
  private transient MandantDAO mandantDAO;

  @Inject
  private transient KonfigurationLoader konfigurationLoader;

  @Inject
  private transient PasswordEncryptionServiceFinder passwordEncryptionServiceFinder;

  @Inject
  private transient DatabaseLoginServiceHelper databaseLoginServiceHelper;

  public LoginData login(LoginServiceContext context, Long mandantenNummer, String benutzername, String kennungImKlartext) throws AuthentificationFailedException {
    try {
      Mandant mandant = mandantDAO.findByNummer(mandantenNummer);
      List<Benutzer> benutzerInDb = benutzerDAO.findByBenutzerkennungUpper(mandant, benutzername);
      if (benutzerInDb.isEmpty()) {
        LOG.warn("Benutzer nicht gefunden");
        throw new AuthentificationFailedException();
      }
      Benutzer benutzer = benutzerInDb.get(0);
      if (benutzerInDb.size() > 1) {
        LOG.error(
            "Mehr als ein Benutzer mit der Benutzerkennung ''" + benutzername + "'' für den Mandanten: " + mandant + " vorhanden: " + benutzerInDb + ", verwende: " + benutzer);
      }

      PasswordStoreType passwordStoreType = konfigurationLoader.resolveEnumValue(mandant, LOGIN_SERVICE_PASSWORD_STORE_TYPE, PasswordStoreType.class);

      boolean mussPasswortWechseln = benutzer.getMussPasswortWechseln() == null || benutzer.getMussPasswortWechseln();
      boolean gesperrt = !Boolean.TRUE.equals(context.get(IGNORIERE_GESPERRT_KEY, Boolean.class)) && isBenutzerGesperrt(benutzer);

      List<LoginGruppe> loginGruppen = bestimmenGruppen(benutzer);

      boolean isErfolgreichAuthentifiziert = false;
      if (!gesperrt) {
        checkPasswortKlartextInDbNeedsEncryption(benutzer, passwordStoreType);
        isErfolgreichAuthentifiziert = checkPasswortIsCorrect(benutzer, kennungImKlartext, passwordStoreType, context);
      }
      return new LoginData(mandantenNummer, benutzer.getBenutzerkennung(), benutzer.getVorname(), benutzer.getFamilienname(), isErfolgreichAuthentifiziert,
          mussPasswortWechseln, gesperrt, loginGruppen);

    } catch (AuthentificationFailedException e) {
      throw e;
    } catch (Exception e) {
      LOG.error("Fehler bei Anmeldung: " + e.getMessage(), e);
      throw new AuthentificationFailedException("login.action.login.error.unknown", e.getMessage());
    }

  }

  protected boolean isBenutzerGesperrt(Benutzer benutzer) {
    if (benutzer.isAdmin()) {
      return databaseLoginServiceHelper.sonderbehandlungAdmin(benutzer);
    }
    return benutzer.getGesperrt() != null && benutzer.getGesperrt();
  }



  public void changePassword(LoginServiceContext context, Long mandantenNummer, String benutzerkennung, String passwortAlt, String passwortNeu) {
    Mandant mandant = mandantDAO.findByNummer(mandantenNummer);
    List<Benutzer> benutzerInDb = benutzerDAO.findByBenutzerkennungUpper(mandant, benutzerkennung);
    if (benutzerInDb.size() == 1) {
      Benutzer benutzer = benutzerInDb.get(0);
      benutzer.setPasswort(passwordEncryptionServiceFinder.getDefaultPasswordEncryptionService(), passwortNeu);
      benutzer.passwortErfolgreichGewechselt();
    }
  }

  private List<LoginGruppe> bestimmenGruppen(Benutzer benutzer) {
    return benutzer.getRolles().stream()
        .map(rolle -> new LoginGruppe(rolle.getName(), rolle.getBeschreibung()))
        .collect(Collectors.toList());

  }

  private void checkPasswortKlartextInDbNeedsEncryption(Benutzer login, PasswordStoreType passwordStoreType) {
    PasswordEncryptionService defaultPasswordEncryptionService = passwordEncryptionServiceFinder.getDefaultPasswordEncryptionService();
    if (!defaultPasswordEncryptionService.isEncrypted(login.getPasswort())) {
      LOG.info("Passwort für Nutzer: " + login.getBenutzerkennung() + " nicht nach aktuellem Verfahren verschlüsselt");

      for (PasswordEncryptionService alternative : passwordEncryptionServiceFinder.gteFallbackPasswordEncryptionServices()) {
        if (alternative.isEncrypted(login.getPasswort())) {
          LOG.info("Passwort für Nutzer: " + login.getBenutzerkennung() + " nach Verfahren " + alternative + " verschlüsselt");
          return;
        }
      }

      if (passwordStoreType.isKlartextUnterstuetzt()) {
        LOG.warn("Passwort für Nutzer: " + login.getBenutzerkennung() + " unverschlüsselt, verschlüssele");
        login.setPasswort(defaultPasswordEncryptionService, login.getPasswort());
      } else {
        LOG.error("Klartextpasswörter in der DB werden nicht unterstützt! Einstellung: " + passwordStoreType);
      }
    }
  }

  protected boolean checkPasswortIsCorrect(Benutzer benutzer, String passwortInClearText, PasswordStoreType passwordStoreType, LoginServiceContext loginServiceContext) {
    PasswordEncryptionService defaultPasswordEncryptionService = passwordEncryptionServiceFinder.getDefaultPasswordEncryptionService();

    if (defaultPasswordEncryptionService.isEncrypted(benutzer.getPasswort())) {
      boolean result = defaultPasswordEncryptionService.authenticate(passwortInClearText, benutzer.getPasswort(), benutzer.getSalt());
      return checkAuthentificationResult(result, benutzer, loginServiceContext);
    }

    for (PasswordEncryptionService alternative : passwordEncryptionServiceFinder.gteFallbackPasswordEncryptionServices()) {
      if (alternative.isEncrypted(benutzer.getPasswort())) {
        if (passwordStoreType.isBisherigeVerfahrenUnterstuetzt()) {

          boolean result = alternative.authenticate(passwortInClearText, benutzer.getPasswort(), benutzer.getSalt());
          if (result) {
            LOG.info("Erfolgreich authentifiziert, speichere Passwort nach aktuellem Verschlüsselungsverfahren");
            // Passwort nach neuem Verfahren in DB speichern
            benutzer.setPasswort(defaultPasswordEncryptionService, passwortInClearText);
            // erneut nach aktuellem Verfahren prüfen
            result = defaultPasswordEncryptionService.authenticate(passwortInClearText, benutzer.getPasswort(), benutzer.getSalt());
            if (!result) {
              // "Cannot happen" Exception werfen, um Probleme zu erkennen und analysieren zu können
              throw new IllegalStateException("Fehler beim Verschlüsseln nach neuem Verfahren! Nutzer: " + benutzer.getBenutzerkennung());
            }
          }

          return checkAuthentificationResult(result, benutzer, loginServiceContext);
        } else {
          LOG.error("Bisherige Verschlüsselungsverfahren werden nicht unterstützt: " + passwordStoreType);
          return false;
        }
      }
    }

    LOG.warn("Kein Verschlüsselungsverfahren gefunden: " + benutzer.getBenutzerkennung());
    return false;
  }

  private boolean checkAuthentificationResult(boolean result, Benutzer benutzer, LoginServiceContext loginServiceContext) {
    if (result) {
      benutzer.erfolgreicherLogin();
      return true;
    }

    if (!Boolean.TRUE.equals(loginServiceContext.get(ERHOEHE_FEHLVERSUCHE_NICHT_KEY, Boolean.class))) {
      benutzer.erhoeheFehlversuche();
    }
    return false;
  }
}
