
package de.ivu.elect.jee.security;

import java.io.Serializable;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.jee.faces.FacesContextUtil;


import static de.ivu.elect.jee.session.SessionIdFilter.maskiere;

// ELECT-7324 @SessionScoped - ohne Scope, da nicht immer Session vorhanden
public abstract class AbstractPrincipal<UseCasePerm extends Permission, PK extends Object, LoginEntity extends Login<PK>, C extends PrincipalContext<UseCasePerm, PK>> implements Serializable

/*  MUSS IN DER ANWENDUNG AN EINEM SessionScoped PRINCIPAL IMPLEMENTIERT WERDEN  implements
    PrincipalMarker */ {

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

  private static final long serialVersionUID = 1L;

  public static final String COMPONENT_NAME = "principal";

  public static final String SYSTEM_BENUTZERKENNUNG = "***SYSTEM***";

  public static final long MANDATEN_NUMMER_KEIN_MANDANT = 0L;

  private PK loginPK;

  private String benutzerkennung;

  private IpHostname ipHostname = IpHostname.UNBEKANNT;

  private String sessionId;

  private boolean mustChangePassword;

  private final Set<UseCasePerm> useCasePermissions = new HashSet<>();

  private static final AtomicLong USER_COUNT = new AtomicLong(0L);

  private final String uuid = StringUtils.leftPad(Long.toString(USER_COUNT.addAndGet(1L)), 5, "0");

  private Date letzteErfolgreicheAnmeldung;

  private long anzahlVorherigerFehlversuche;

  @PostConstruct
  public void init() {
    clearLoginEntityState();
  }

  @Deprecated // Mandant verwenden
  public void login(String benutzerkennung, String passwortInClearText) {
    loginMandant(MANDATEN_NUMMER_KEIN_MANDANT, benutzerkennung, passwortInClearText);
  }

  public void loginMandant(Long mandatenNummer, String benutzerkennung, String passwortInClearText) {
    clearLoginEntityState();

    PrincipalLoginFilter filter = getPrincipalContext().getPrincipalLoginFilter();
    Long mandatenNummerFilter = filter.filterMandant(mandatenNummer);
    String benutzerkennungFilter = filter.filterBenutzername(benutzerkennung);
    String passwortInClearTextFilter = filter.filterPasswort(passwortInClearText);

    if (mandatenNummerFilter == null || StringUtils.isBlank(benutzerkennungFilter) || StringUtils.isBlank(passwortInClearTextFilter)) {
      throw new AuthentificationFailedException();
    }

    ipHostname = getPrincipalContext().getLoginIpBestimmer().bestimmeIpAdresseAusRequest();

    LoginData login = callLoginService(mandatenNummerFilter, benutzerkennungFilter, passwortInClearTextFilter);

    login(login);
  }

  private LoginData callLoginService(Long mandantenNummer, String benutzerkennung, String passwortInClearText) {
    LoginData login;
    try {
      LoginService loginService = getPrincipalContext().getLoginServiceFactory().getLoginService(mandantenNummer);
      LOG.info("Longinservice: " + loginService.getServiceName() + " - " + esc(benutzerkennung) + ", mandantenNummer = " + mandantenNummer);
      login = loginService.login(getLoginContext(), mandantenNummer, benutzerkennung, passwortInClearText);
    } catch (Exception e) {
      LOG.info("Anmeldung im LoginService nicht möglich: " + esc(benutzerkennung) + ", mandantenNummer = " + mandantenNummer,
          ", ipHostname=" + getIpHostname() + "," +
              " " + e.getMessage());
      throw e;
    }
    return login;
  }

  protected String esc(String benutzerkennung) {
    return getPrincipalContext().getEscape().encodeUrl(benutzerkennung);
  }

  public void login(LoginData loginData, IpHostname ipHostname) {
    this.ipHostname = ipHostname;
    login(loginData);
  }

  public void login(LoginData loginData) {
    checkLoginData(loginData);

    LoginEntity login = initLoginEntityState(loginData);

    if (isGesperrt(login)) {
      LOG.info("Nutzer gesperrt, Anmeldung nicht möglich: " + esc(loginData.getBenutzerkennung()) + ", mandantenNummer = " + loginData
          .getMandantenNummer() + ", ipHostname=" + getIpHostname());
      logout(false);
      throw new AuthentificationFailedException("login.action.login.user.gesperrt.message",
          loginData.getBenutzerkennung());
    }

    if (getUseCasePermissionFuerAnmeldung() != null && !hasUseCasePermission(getUseCasePermissionFuerAnmeldung())) {
      LOG.info("Nutzer hat keine Rechte: " + esc(loginData.getBenutzerkennung()) + ", mandantenNummer = " + loginData
          .getMandantenNummer() + ", ipHostname=" + getIpHostname());

      logout(false);
      throw new AuthentificationFailedException("login.action.login.error.message.kein.recht");
    }
    handleHttpSession();

    this.anzahlVorherigerFehlversuche = loginData.getAnzahlVorherigerFehlversuche();
    this.letzteErfolgreicheAnmeldung = loginData.getLetzteErfolgreicheAnmeldung();

    fireLoginChangeEvent(login);
    LOG.info("Nutzer angemeldet: " + esc(loginData.getBenutzerkennung()) + ", mandantenNummer = " + loginData.getMandantenNummer());
  }

  protected boolean isGesperrt(LoginEntity login) {
    return login.isGesperrt();
  }

  protected void handleHttpSession() {
    // Session ID nach Login wechseln
    try {
      HttpSession session = (HttpSession) FacesContextUtil.getExternalContext().getSession(false);
      if (session != null) { // WASDE-1959
        LOG.info("Bisherige Session Id: " + maskiere(session.getId()) + ", ipHostname=" + getIpHostname());
        ((HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest()).changeSessionId();
        String newSessionId = session.getId();
        LOG.info("Neue Session Id: " + maskiere(newSessionId) + ", ipHostname=" + getIpHostname());
        this.sessionId = newSessionId;
      }
    } catch (Exception e) {
      LOG.warn("Session Id kann nicht gesetzt werden!" + " IpHostname=" + getIpHostname(), e);
    }

    // ELECTMUC-151 Session timeout anpassen
    try {
      HttpSession session = (HttpSession) FacesContextUtil.getExternalContext().getSession(false);
      if (session != null) { // WASDE-1959
        int maxInactiveInterval = (int) TimeUnit.SECONDS.convert(getSessionTimeoutInMinutes(), TimeUnit.MINUTES);
        session.setMaxInactiveInterval(maxInactiveInterval);
        LOG.info("Session-Timeout gesetzt, Session Id: " + maskiere(session.getId()) + ", maxInactiveInterval=" + maxInactiveInterval);
      }
    } catch (Exception e) {
      LOG.warn("Session timeout kann nicht gesetzt werden!" + " IpHostname=" + getIpHostname(), e);
    }
  }

  private void checkLoginData(LoginData loginData) {
    if (loginData.isGesperrt()) {
      LOG.info("Nutzer gesperrt, Anmeldung nicht möglich: " + esc(loginData.getBenutzerkennung()) + ", mandantenNummer = " + loginData
          .getMandantenNummer() + ", ipHostname=" + getIpHostname());
      throw new AuthentificationFailedException("login.action.login.user.gesperrt.message",
          loginData.getBenutzerkennung());
    }

    if (!loginData.isErfolgreichAuthentifiziert()) {
      LOG.info("Anmeldung nicht möglich: " + esc(loginData.getBenutzerkennung()) + ", mandantenNummer = " + loginData
          .getMandantenNummer() + ", ipHostname=" + getIpHostname());
      throw new AuthentificationFailedException("login.action.login.error.message");
    }
  }

  protected abstract UseCasePerm getUseCasePermissionFuerAnmeldung();

  protected abstract int getSessionTimeoutInMinutes();

  public boolean isLoggedIn() {
    return loginPK != null;
  }

  protected PK getLoginPK() {
    return loginPK;
  }

  public boolean isMustChangePassword() {
    return mustChangePassword;
  }

  public void setMustChangePassword(boolean mustChangePassword) {
    this.mustChangePassword = mustChangePassword;
  }

  public boolean hasUseCasePermission(UseCasePerm permisson) {
    return useCasePermissions.contains(permisson) && getPrincipalContext().getUseCasePermissionFilter().test(permisson, loginPK);
  }

  @Deprecated // hasUseCasePermission verwenden
  public Collection<UseCasePerm> getUseCasePermissions() {
    return useCasePermissions.stream().filter(this::hasUseCasePermission).collect(Collectors.toSet());
  }

  public boolean isUnfilteredHasUseCasePermissionHandleWithCare(UseCasePerm permisson) {
    return useCasePermissions.contains(permisson);
  }

  protected void fireLoginChangeEvent(LoginEntity login) {
    getPrincipalContext().getLoginChangeEvent().fire(new LoginChangeEvent(login, getIpHostname(), sessionId));
  }

  @PreDestroy
  public void logout() {
    try {
      if (StringUtils.isNotBlank(benutzerkennung)) {
        LOG.info("PreDestroy-logout: " + esc(benutzerkennung) + " - " + isLoggedIn());
      }
    } catch (Exception e) {
      // ELECT-14856 WELD NO-CONTEXT (esc)
    }

    try {
      if (isLoggedIn()) {
        logout(true);
      }
    } catch (Exception e) {
      if (StringUtils.isNotBlank(e.getMessage()) && e.getMessage().contains("WELD-001303")) {
        // Kein Kontext, Fehler kommt beim automatischen Logout und kann ignoriert werden
        return;
      }
      LOG.warn("Fehler beim Logout: " + e.getMessage(), e);
    }
  }

  /**
   * Methode zum Ausloggen an der Oberfläche
   */
  public void logoutGUI() {
    LOG.info("logoutGUI: " + esc(benutzerkennung));
    logout();
    try {
      FacesContextUtil.getExternalContext().invalidateSession();
    } catch (Exception e) {
      LOG.warn("Fehler beim Schließen der Session: " + e.getMessage());
    }
  }

  public void logoutMitMeldung(String messageKey, Object... params) {
    LOG.info("logoutMitMeldung: " + esc(benutzerkennung) + " - " + messageKey);
    if (isLoggedIn()) {
      getPrincipalContext().getMessages().addErrorMessage(messageKey, params);
      logout(false);
    }
  }

  public void logoutMitInfoMeldung(String messageKey, Object... params) {
    LOG.info("logoutMitInfoMeldung: " + esc(benutzerkennung) + " - " + messageKey);
    if (isLoggedIn()) {
      getPrincipalContext().getMessages().addInfoMessage(messageKey, params);
      logout(false);
    }
  }

  protected void logout(boolean killSession) {
    if (isLoggedIn()) {
      String benutzerkennungVorLogout = benutzerkennung;
      IpHostname ipHostnameVorLogout = getIpHostname();
      String sessionIdVorLogout = getSessionId();
      clearLoginEntityState();
      fireLogoutChangeEvent(benutzerkennungVorLogout, ipHostnameVorLogout, sessionIdVorLogout);
      if (killSession) {
        clearSession();
      }
      LOG.info("Nutzer abgemeldet: " + esc(benutzerkennungVorLogout));
    }
  }

  private void fireLogoutChangeEvent(String benutzerkennung, IpHostname ipHostname, String sessionId) {
    getPrincipalContext().getLogoutChangeEvent().fire(new LogoutChangeEvent(sessionId, benutzerkennung, ipHostname));
  }

  private LoginEntity initLoginEntityState(LoginData loginData) {
    LoginEntity result = initLoginEntityProperties(loginData);
    initLoginEntityPermissions(result);
    return result;
  }

  protected void clearLoginEntityState() {
    clearLoginEntityProperties();
    clearLoginEntityPermissions();
    clearPrincipal();
  }

  protected void clearPrincipal() {
    ipHostname = IpHostname.UNBEKANNT;
    sessionId = null;
    letzteErfolgreicheAnmeldung = null;
    anzahlVorherigerFehlversuche = 0;
  }

  protected LoginEntity initLoginEntityProperties(LoginData loginData) {
    mustChangePassword = loginData.isMussPasswortWechseln();
    benutzerkennung = loginData.getBenutzerkennung();
    LoginEntity loginEntity = findOrCreateEntity(loginData);
    LOG.info("Login - loginEntity: " + loginEntity);
    flush();
    if (getPrincipalContext().getLoginServiceFactory().isGruppenUebernehmen(loginData.getMandantenNummer())) {
      LOG.info("Übernehme Gruppen: " + loginData.getLoginGruppen());
      updateGruppen(loginEntity, loginData.getLoginGruppen());
      flush();
    }
    loginPK = loginEntity.getPk();
    LOG.info("Login - loginPK: " + loginPK);
    updateLastLoginTime(loginEntity);
    flush();
    return loginEntity;
  }

  protected void updateLastLoginTime(LoginEntity loginEntity) {
    loginEntity.updateLastLoginTime();
  }

  protected abstract void updateGruppen(LoginEntity loginEntity, List<LoginGruppe> loginGruppen);

  protected abstract LoginEntity findOrCreateEntity(LoginData loginData);

  protected abstract void flush();

  private void clearLoginEntityProperties() {
    loginPK = null;
    benutzerkennung = null;
    mustChangePassword = false;
  }

  private void initLoginEntityPermissions(LoginEntity login) {
    useCasePermissions.addAll(loadUseCasePermissions(login));
    useCasePermissions.add(getDefaultPermission());
  }

  protected abstract UseCasePerm getDefaultPermission();

  protected abstract Collection<? extends UseCasePerm> loadUseCasePermissions(LoginEntity login);

  private void clearLoginEntityPermissions() {
    useCasePermissions.clear();
  }

  protected void clearSession() {
    // ELECT-713 Session wird nicht explizit invalidiert...
    // ELECT-1308 ...außer beim Abmelden in der GUI, cf. logoutGUI()
  }

  protected void addPermissions(Collection<UseCasePerm> useCasePerms) {
    this.useCasePermissions.addAll(useCasePerms);
  }

  @Deprecated // Mandant verwenden
  public void changePassword(String benutzerkennung, String passwortAlt, String passwortNeu) {
    changePasswordMandant(MANDATEN_NUMMER_KEIN_MANDANT, benutzerkennung, passwortAlt, passwortNeu);
  }

  public void changePasswordMandant(Long mandantenNummer, String benutzerkennung, String passwortAlt, String passwortNeu) {
    LoginData login = callLoginService(mandantenNummer, benutzerkennung, passwortAlt);
    checkLoginData(login);
    getPrincipalContext().getLoginServiceFactory().getLoginService(mandantenNummer).changePassword(getLoginContext(), mandantenNummer, benutzerkennung,
        passwortAlt,
        passwortNeu);
    mustChangePassword = false;
  }

  protected LoginServiceContext getLoginContext() {
    return new LoginServiceContext();
  }

  public abstract LoginEntity getLogin();

  public String getName() {
    return benutzerkennung;
  }

  public void keepAlive() {
    // ELECTMUC-151 nichts zu tun, Session wird aktiv gehalten
  }

  public IpHostname getIpHostname() {
    return ipHostname;
  }

  public String getSessionId() {
    return sessionId;
  }

  public abstract C getPrincipalContext();

  public String getUuid() {
    return uuid;
  }

  public Date getLetzteErfolgreicheAnmeldung() {
    return letzteErfolgreicheAnmeldung;
  }

  public long getAnzahlVorherigerFehlversuche() {
    return anzahlVorherigerFehlversuche;
  }
}
