package com.sun.identity.saml2.profile;

import com.airlock.iam.opensso.jaxb.entityconfig.BaseConfigType;
import com.airlock.iam.opensso.jaxb.metadata.EndpointType;
import com.airlock.iam.opensso.jaxb.metadata.IDPSSODescriptorType;
import com.airlock.iam.opensso.jaxb.metadata.SPSSODescriptorType;
import com.sun.identity.federation.common.FSUtils;
import com.sun.identity.plugin.session.SessionException;
import com.sun.identity.plugin.session.SessionManager;
import com.sun.identity.plugin.session.SessionProvider;
import com.sun.identity.saml2.assertion.Issuer;
import com.sun.identity.saml2.assertion.NameID;
import com.sun.identity.saml2.common.AccountUtils;
import com.sun.identity.saml2.common.NameIDInfoKey;
import com.sun.identity.saml2.common.SAML2Exception;
import com.sun.identity.saml2.common.SAML2SDKUtils;
import com.sun.identity.saml2.common.SAML2Utils;
import com.sun.identity.saml2.logging.LogUtil;
import com.sun.identity.saml2.meta.SAML2MetaException;
import com.sun.identity.saml2.meta.SAML2MetaManager;
import com.sun.identity.saml2.meta.SAML2MetaUtils;
import com.sun.identity.saml2.plugins.SAML2ServiceProviderAdapter;
import com.sun.identity.saml2.protocol.LogoutRequest;
import com.sun.identity.saml2.protocol.LogoutResponse;
import com.sun.identity.saml2.protocol.ProtocolFactory;
import com.sun.identity.saml2.protocol.Status;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.StringTokenizer;
import java.util.logging.Level;

public class SPSingleLogout {
   private static SAML2MetaManager sm;
   private static final Status SUCCESS_STATUS = SAML2Utils.generateStatus(
      "urn:oasis:names:tc:SAML:2.0:status:Success", SAML2Utils.BUNDLE.getString("requestSuccess")
   );
   private static final Status PARTIAL_LOGOUT_STATUS = SAML2Utils.generateStatus(
      "urn:oasis:names:tc:SAML:2.0:status:Responder", SAML2Utils.BUNDLE.getString("partialLogout")
   );
   private static SessionProvider sessionProvider;

   public static void initiateLogoutRequest(HttpServletRequest request, HttpServletResponse response, String binding, Map<String, String> paramsMap) throws SAML2Exception {
      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG.message("SPSingleLogout:initiateLogoutRequest");
         SAML2SDKUtils.DEBUG.message("binding : " + binding);
         SAML2SDKUtils.DEBUG.message("paramsMap : " + paramsMap);
      }

      String metaAlias = paramsMap.get("spMetaAlias");

      try {
         Object session = sessionProvider.getSession(request);
         if (session == null) {
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("nullSSOToken"));
         }

         if (metaAlias == null) {
            String[] values = sessionProvider.getProperty(session, "spMetaAlias");
            if (values != null && values.length > 0) {
               metaAlias = values[0];
            }
         }

         if (metaAlias == null) {
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("nullSPMetaAlias"));
         }

         paramsMap.put("metaAlias", metaAlias);
         String realm = SAML2Utils.getRealm(SAML2MetaUtils.getRealmByMetaAlias(metaAlias));
         SAML2SDKUtils.DEBUG.message("realm : " + realm);
         String spEntityID = sm.getEntityByMetaAlias(metaAlias);
         if (spEntityID == null) {
            SAML2SDKUtils.DEBUG.error("Service Provider ID is missing");
            String[] data = new String[]{spEntityID};
            LogUtil.error(Level.INFO, "INVALID_SP", data, null);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("nullSPEntityID"));
         }

         SAML2SDKUtils.DEBUG.message("spEntityID : " + spEntityID);
         String tokenID = sessionProvider.getSessionID(session);
         String infoKeyString = null;

         try {
            String[] values = sessionProvider.getProperty(session, AccountUtils.getNameIDInfoKeyAttribute());
            if (values != null && values.length > 0) {
               infoKeyString = values[0];
            }
         } catch (SessionException se) {
            SAML2SDKUtils.DEBUG.error("Unable to get infoKeyString from session.", se);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("errorInfoKeyString"));
         }

         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("tokenID : " + tokenID);
            SAML2SDKUtils.DEBUG.message("infoKeyString : " + infoKeyString);
         }

         SPSSODescriptorType spsso = sm.getSPSSODescriptor(realm, spEntityID);
         if (spsso == null) {
            String[] data = new String[]{spEntityID};
            LogUtil.error(Level.INFO, "SP_METADATA_ERROR", data, null);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }

         List<String> extensionsList = LogoutUtil.getExtensionsList(paramsMap);
         String relayState = SAML2Utils.getParameter(paramsMap, "RelayState");
         if (relayState == null || relayState.isEmpty()) {
            relayState = SAML2Utils.getAttributeValueFromSSOConfig(realm, spEntityID, "SPRole", "defaultRelayState");
         }

         if (infoKeyString == null) {
            SAML2SDKUtils.DEBUG.warning("SPSingleLogout.initiateLogoutRequest : Unable to get infoKeyString from session.");
            sessionProvider.invalidateSession(session, request, response);
            if (relayState != null && !relayState.isEmpty()) {
               try {
                  response.sendRedirect(relayState);
               } catch (IOException e) {
                  SAML2SDKUtils.DEBUG.error("SPSingleLogout.initiateLogoutRequest: Error in send redirect to " + relayState, e);
               }
            } else {
               RequestDispatcher dispatcher = request.getRequestDispatcher("saml2/jsp/default.jsp?message=spSloSuccess");

               try {
                  dispatcher.forward(request, response);
               } catch (IOException | ServletException e) {
                  SAML2SDKUtils.DEBUG.error("SPSingleLogout.initiateLogoutRequest: Error in forwarding to default.jsp", e);
               }
            }
         } else {
            StringTokenizer st = new StringTokenizer(infoKeyString, ";");
            if (st != null && st.hasMoreTokens()) {
               while (st.hasMoreTokens()) {
                  String tmpInfoKeyString = st.nextToken();
                  prepareForLogout(realm, tokenID, metaAlias, extensionsList, binding, relayState, request, response, paramsMap, tmpInfoKeyString);
               }
            }

            if (binding.equals("urn:oasis:names:tc:SAML:2.0:bindings:SOAP")) {
               sessionProvider.invalidateSession(session, request, response);
            }
         }
      } catch (SAML2MetaException sme) {
         SAML2SDKUtils.DEBUG.error("Error retreiving metadata", sme);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      } catch (SessionException ssoe) {
         SAML2SDKUtils.DEBUG.error("Session exception: ", ssoe);
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
      }
   }

   private static void prepareForLogout(
      String realm,
      String tokenID,
      String metaAlias,
      List<String> extensionsList,
      String binding,
      String relayState,
      HttpServletRequest request,
      HttpServletResponse response,
      Map<String, String> paramsMap,
      String infoKeyString
   ) throws SAML2Exception, SessionException {
      NameIDInfoKey nameIdInfoKey = NameIDInfoKey.parse(infoKeyString);
      String sessionIndex = null;
      NameID nameID = null;
      SPFedSession fedSession = null;
      List<SPFedSession> list = SPCache.fedSessionListsByNameIDInfoKey().get(infoKeyString);
      if (list != null) {
         synchronized (list) {
            for (ListIterator<SPFedSession> iter = list.listIterator(); iter.hasNext(); fedSession = null) {
               fedSession = iter.next();
               if (tokenID.equals(fedSession.spTokenID)) {
                  iter.remove();
                  if (list.isEmpty()) {
                     SPCache.fedSessionListsByNameIDInfoKey().remove(infoKeyString);
                  }
                  break;
               }
            }
         }
      }

      if (fedSession == null) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("No session partner, just do local logout.");
         }
      } else {
         sessionIndex = fedSession.idpSessionIndex;
         nameID = fedSession.info.getNameID();
         IDPSSODescriptorType idpsso = sm.getIDPSSODescriptor(realm, nameIdInfoKey.getRemoteEntityID());
         if (idpsso == null) {
            String[] data = new String[]{nameIdInfoKey.getRemoteEntityID()};
            LogUtil.error(Level.INFO, "IDP_METADATA_ERROR", data, null);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }

         List<EndpointType> slosList = idpsso.getSingleLogoutService();
         if (slosList == null) {
            String[] data = new String[]{nameIdInfoKey.getRemoteEntityID()};
            LogUtil.error(Level.INFO, "SLO_NOT_FOUND", data, null);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("sloServiceListNotfound"));
         }

         BaseConfigType idpConfig = null;
         if (binding.equals("urn:oasis:names:tc:SAML:2.0:bindings:SOAP")) {
            idpConfig = sm.getIDPSSOConfig(realm, nameIdInfoKey.getRemoteEntityID());
         }

         LogoutUtil.doLogout(
            metaAlias,
            nameIdInfoKey.getRemoteEntityID(),
            slosList,
            extensionsList,
            binding,
            relayState,
            sessionIndex,
            nameID,
            request,
            response,
            paramsMap,
            idpConfig
         );
      }
   }

   public static void processLogoutResponse(HttpServletRequest request, HttpServletResponse response, String samlResponse, String relayState) throws SAML2Exception, SessionException {
      String method = "SPSingleLogout:processLogoutResponse : ";
      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG.message(method + "samlResponse : " + samlResponse);
         SAML2SDKUtils.DEBUG.message(method + "relayState : " + relayState);
      }

      String rmethod = request.getMethod();
      LogoutResponse logoutRes = null;
      String binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect";
      if (rmethod.equals("POST")) {
         binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
         logoutRes = LogoutUtil.getLogoutResponseFromPost(samlResponse);
      } else if (rmethod.equals("GET")) {
         String decodedStr = SAML2Utils.decodeFromRedirect(samlResponse);
         if (decodedStr == null) {
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("nullDecodedStrFromSamlResponse"));
         }

         logoutRes = ProtocolFactory.getInstance().createLogoutResponse(decodedStr);
      }

      if (logoutRes == null) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("SSingleLogout:processLogoutResponse: logoutRes is null");
         }
      } else {
         String metaAlias = SAML2MetaUtils.getMetaAliasByUri(request.getRequestURI());
         if (metaAlias != null && !metaAlias.isEmpty()) {
            String realm = SAML2Utils.getRealm(SAML2MetaUtils.getRealmByMetaAlias(metaAlias));
            String spEntityID = sm.getEntityByMetaAlias(metaAlias);
            String idpEntityID = logoutRes.getIssuer().getValue();
            Issuer resIssuer = logoutRes.getIssuer();
            String inResponseTo = logoutRes.getInResponseTo();
            LogoutRequest logoutReq = SPCache.logoutRequestIDHash().remove(inResponseTo);
            String userId = null;
            userId = preSingleLogoutProcess(spEntityID, realm, request, response, null, logoutReq, logoutRes, binding);
            SAML2Utils.verifyResponseIssuer(realm, spEntityID, resIssuer, inResponseTo);
            boolean needToVerify = SAML2Utils.getWantLogoutResponseSigned(realm, spEntityID, "SPRole");
            if (SAML2SDKUtils.DEBUG.messageEnabled()) {
               SAML2SDKUtils.DEBUG.message(method + "metaAlias : " + metaAlias);
               SAML2SDKUtils.DEBUG.message(method + "realm : " + realm);
               SAML2SDKUtils.DEBUG.message(method + "idpEntityID : " + idpEntityID);
               SAML2SDKUtils.DEBUG.message(method + "spEntityID : " + spEntityID);
            }

            if (needToVerify) {
               boolean valid;
               if (rmethod.equals("GET")) {
                  String queryString = request.getQueryString();
                  valid = SAML2Utils.verifyQueryString(queryString, realm, "SPRole", idpEntityID);
               } else {
                  valid = LogoutUtil.verifySLOResponse(logoutRes, realm, idpEntityID, spEntityID, "SPRole");
               }

               if (!valid) {
                  SAML2SDKUtils.DEBUG.error("SPSingleLogout.processLogoutResponse: Invalid signature in SLO Response.");
                  throw new SAML2Exception(SAML2Utils.BUNDLE.getString("invalidSignInResponse"));
               }

               SPSSODescriptorType spsso = sm.getSPSSODescriptor(realm, spEntityID);
               String loc = getSLOResponseLocationOrLocation(spsso, binding);
               if (!SAML2Utils.verifyDestination(logoutRes.getDestination(), loc)) {
                  throw new SAML2Exception(SAML2Utils.BUNDLE.getString("invalidDestination"));
               }
            }

            if (inResponseTo != null && !inResponseTo.isEmpty()) {
               if (logoutReq != null) {
                  if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                     SAML2SDKUtils.DEBUG.message("LogoutResponse inResponseTo matches LogoutRequest ID.");
                  }

                  SessionProvider sessionProvider = SessionManager.getProvider();
                  Object session = sessionProvider.getSession(request);
                  if (session != null && sessionProvider.isValid(session)) {
                     sessionProvider.invalidateSession(session, request, response);
                  }

                  if (isSuccess(logoutRes)) {
                     postSingleLogoutSuccess(spEntityID, realm, request, response, userId, logoutReq, logoutRes, binding);
                  } else {
                     throw new SAML2Exception(SAML2Utils.BUNDLE.getString("sloFailed"));
                  }
               } else {
                  if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                     SAML2SDKUtils.DEBUG.message("LogoutResponse inResponseTo does not match LogoutRequest ID.");
                  }

                  throw new SAML2Exception(SAML2Utils.BUNDLE.getString("LogoutRequestIDandInResponseToDoNotMatch"));
               }
            } else {
               if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                  SAML2SDKUtils.DEBUG.message("LogoutResponse inResponseTo is null");
               }

               throw new SAML2Exception(SAML2Utils.BUNDLE.getString("nullInResponseToFromSamlResponse"));
            }
         } else {
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("nullSPEntityID"));
         }
      }
   }

   static String preSingleLogoutProcess(
      String hostedEntityID,
      String realm,
      HttpServletRequest request,
      HttpServletResponse response,
      String userID,
      LogoutRequest logoutRequest,
      LogoutResponse logoutResponse,
      String binding
   ) throws SAML2Exception {
      SAML2ServiceProviderAdapter spAdapter = null;

      try {
         spAdapter = SAML2Utils.getSPAdapterClass(hostedEntityID, realm);
      } catch (SAML2Exception e) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("SPACSUtils.invokeSPAdapterForPreSLOProcess", e);
         }
      }

      if (spAdapter != null) {
         if (userID == null) {
            try {
               Object session = SessionManager.getProvider().getSession(request);
               if (session != null) {
                  userID = SessionManager.getProvider().getPrincipalName(session);
               }
            } catch (SessionException ex) {
               if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                  SAML2SDKUtils.DEBUG.message("SPACSUtils.invokeSPAdapterForPreSLOProcess2: " + ex.getMessage());
               }
            }
         }

         spAdapter.preSingleLogoutProcess(hostedEntityID, realm, request, response, userID, logoutRequest, logoutResponse, binding);
      }

      return userID;
   }

   static void postSingleLogoutSuccess(
      String hostedEntityID,
      String realm,
      HttpServletRequest request,
      HttpServletResponse response,
      String userID,
      LogoutRequest logoutRequest,
      LogoutResponse logoutResponse,
      String binding
   ) {
      SAML2ServiceProviderAdapter spAdapter = null;

      try {
         spAdapter = SAML2Utils.getSPAdapterClass(hostedEntityID, realm);
      } catch (SAML2Exception e) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("SPACSUtils.invokeSPAdapterForPostSLOProcess", e);
         }
      }

      if (spAdapter != null) {
         spAdapter.postSingleLogoutSuccess(hostedEntityID, realm, request, response, userID, logoutRequest, logoutResponse, binding);
      }
   }

   public static Optional<LogoutRequestDataContainer> validateLogoutRequest(HttpServletRequest request, String samlRequest, String relayState) throws SAML2Exception, SessionException {
      String method = "processLogoutRequest : ";
      if (SAML2SDKUtils.DEBUG.messageEnabled()) {
         SAML2SDKUtils.DEBUG.message(method + "samlRequest : " + samlRequest);
         SAML2SDKUtils.DEBUG.message(method + "relayState : " + relayState);
      }

      String rmethod = request.getMethod();
      String binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect";
      LogoutRequest logoutReq = null;
      if (rmethod.equals("POST")) {
         binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
         logoutReq = LogoutUtil.getLogoutRequestFromPost(samlRequest);
      } else if (rmethod.equals("GET")) {
         String decodedStr = SAML2Utils.decodeFromRedirect(samlRequest);
         if (decodedStr == null) {
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("nullDecodedStrFromSamlRequest"));
         }

         logoutReq = ProtocolFactory.getInstance().createLogoutRequest(decodedStr);
      }

      if (logoutReq == null) {
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("SPSingleLogout:processLogoutRequest: logoutReq is null");
         }

         return Optional.empty();
      } else {
         String metaAlias = SAML2MetaUtils.getMetaAliasByUri(request.getRequestURI());
         String realm = SAML2Utils.getRealm(SAML2MetaUtils.getRealmByMetaAlias(metaAlias));
         String spEntityID = sm.getEntityByMetaAlias(metaAlias);
         String location = null;
         String idpEntityID = logoutReq.getIssuer().getValue();
         boolean needToVerify = SAML2Utils.getWantLogoutRequestSigned(realm, spEntityID, "SPRole");
         if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message(method + "metaAlias : " + metaAlias);
            SAML2SDKUtils.DEBUG.message(method + "realm : " + realm);
            SAML2SDKUtils.DEBUG.message(method + "idpEntityID : " + idpEntityID);
            SAML2SDKUtils.DEBUG.message(method + "spEntityID : " + spEntityID);
         }

         if (needToVerify) {
            boolean valid = false;
            if (rmethod.equals("POST")) {
               valid = LogoutUtil.verifySLORequest(logoutReq, realm, idpEntityID, spEntityID, "SPRole");
            } else {
               String queryString = request.getQueryString();
               valid = SAML2Utils.verifyQueryString(queryString, realm, "SPRole", idpEntityID);
            }

            if (!valid) {
               SAML2SDKUtils.DEBUG.error("SPSingleLogout.processLogoutRequest: Invalid signature in SLO Request.");
               throw new SAML2Exception(SAML2Utils.BUNDLE.getString("invalidSignInRequest"));
            }

            SPSSODescriptorType spsso = sm.getSPSSODescriptor(realm, spEntityID);
            String loc = getSLOResponseLocationOrLocation(spsso, binding);
            if (!SAML2Utils.verifyDestination(logoutReq.getDestination(), loc)) {
               throw new SAML2Exception(SAML2Utils.BUNDLE.getString("invalidDestination"));
            }
         }

         IDPSSODescriptorType idpsso = sm.getIDPSSODescriptor(realm, idpEntityID);
         if (idpsso == null) {
            String[] data = new String[]{idpEntityID};
            LogUtil.error(Level.INFO, "IDP_METADATA_ERROR", data, null);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("metaDataError"));
         }

         List<EndpointType> slosList = idpsso.getSingleLogoutService();
         if (slosList == null) {
            String[] data = new String[]{idpEntityID};
            LogUtil.error(Level.INFO, "SLO_NOT_FOUND", data, null);
            throw new SAML2Exception(SAML2Utils.BUNDLE.getString("sloServiceListNotfound"));
         }

         location = LogoutUtil.getSLOResponseServiceLocation(slosList, binding);
         if (location == null || location.isEmpty()) {
            location = LogoutUtil.getSLOServiceLocation(slosList, binding);
            if (location == null || location.isEmpty()) {
               SAML2SDKUtils.DEBUG.error("Unable to find the IDP's single logout response service with the HTTP-Redirect binding");
               throw new SAML2Exception(SAML2Utils.BUNDLE.getString("sloResponseServiceLocationNotfound"));
            }

            if (SAML2SDKUtils.DEBUG.messageEnabled()) {
               SAML2SDKUtils.DEBUG.message("SP's single logout response service location = " + location);
            }
         } else if (SAML2SDKUtils.DEBUG.messageEnabled()) {
            SAML2SDKUtils.DEBUG.message("IDP's single logout response service location = " + location);
         }

         return Optional.of(new LogoutRequestDataContainer(logoutReq, spEntityID, realm, location, idpEntityID, binding, relayState));
      }
   }

   public static void handleLogoutRequest(HttpServletRequest request, HttpServletResponse response, LogoutRequestDataContainer logoutRequestDataContainer) throws SAML2Exception {
      LogoutResponse logoutRes = processLogoutRequest(
         logoutRequestDataContainer.getLogoutReq(),
         logoutRequestDataContainer.getSpEntityID(),
         logoutRequestDataContainer.getRealm(),
         request,
         response,
         true,
         logoutRequestDataContainer.getBinding(),
         true
      );
      logoutRes.setDestination(logoutRequestDataContainer.getLocation());
      LogoutUtil.sendSLOResponse(
         request,
         response,
         logoutRes,
         logoutRequestDataContainer.getLocation(),
         logoutRequestDataContainer.getRelayState(),
         logoutRequestDataContainer.getRealm(),
         logoutRequestDataContainer.getSpEntityID(),
         "SPRole",
         logoutRequestDataContainer.getIdpEntityID(),
         logoutRequestDataContainer.getBinding()
      );
   }

   private static LogoutResponse processLogoutRequest(
      LogoutRequest logoutReq,
      String spEntityID,
      String realm,
      HttpServletRequest request,
      HttpServletResponse response,
      boolean isLBReq,
      String binding,
      boolean isVerified
   ) {
      return processLogoutRequest(logoutReq, spEntityID, realm, request, response, isLBReq, true, binding, isVerified);
   }

   private static LogoutResponse processLogoutRequest(
      LogoutRequest logoutReq,
      String spEntityID,
      String realm,
      HttpServletRequest request,
      HttpServletResponse response,
      boolean isLBReq,
      boolean destroySession,
      String binding,
      boolean isVerified
   ) {
      String method = "processLogoutRequest : ";
      NameID nameID = null;
      Status status = null;
      Issuer issuer = null;
      String idpEntity = logoutReq.getIssuer().getValue();
      String userId = null;

      try {
         issuer = logoutReq.getIssuer();
         String requestId = logoutReq.getID();
         SAML2Utils.verifyRequestIssuer(realm, spEntityID, issuer, requestId);
         issuer = SAML2Utils.createIssuer(spEntityID);
         List<String> siList = logoutReq.getSessionIndex();
         int numSI = 0;
         if (siList != null) {
            numSI = siList.size();
            if (SAML2SDKUtils.DEBUG.messageEnabled()) {
               SAML2SDKUtils.DEBUG.message("processLogoutRequest : Number of session indices in the logout request is " + numSI);
            }
         }

         nameID = LogoutUtil.getNameIDFromSLORequest(logoutReq, realm, spEntityID, "SPRole");
         if (nameID == null) {
            SAML2SDKUtils.DEBUG.error("processLogoutRequest : LogoutRequest does not contain Name ID");
            status = SAML2Utils.generateStatus("urn:oasis:names:tc:SAML:2.0:status:Responder", SAML2Utils.BUNDLE.getString("missing_name_identifier"));
         } else {
            String infoKeyString = null;
            infoKeyString = new NameIDInfoKey(nameID.getValue(), spEntityID, idpEntity).toValueString();
            if (SAML2SDKUtils.DEBUG.messageEnabled()) {
               SAML2SDKUtils.DEBUG.message("processLogoutRequest : infokey=" + infoKeyString);
            }

            List<SPFedSession> list = SPCache.fedSessionListsByNameIDInfoKey().get(infoKeyString);
            if (SAML2SDKUtils.DEBUG.messageEnabled()) {
               SAML2SDKUtils.DEBUG.message("processLogoutRequest : SPFedsessions=" + list);
            }

            if (list == null || list.isEmpty()) {
               String spQ = nameID.getSPNameQualifier();
               if (spQ == null || spQ.isEmpty()) {
                  infoKeyString = new NameIDInfoKey(nameID.getValue(), spEntityID, nameID.getNameQualifier()).toValueString();
                  list = SPCache.fedSessionListsByNameIDInfoKey().get(infoKeyString);
               }
            }

            boolean foundPeer = false;
            List<String> remoteServiceURLs = null;
            if (isLBReq) {
               remoteServiceURLs = FSUtils.getRemoteServiceURLs(request);
               foundPeer = !remoteServiceURLs.isEmpty();
            }

            if (SAML2SDKUtils.DEBUG.messageEnabled()) {
               SAML2SDKUtils.DEBUG.message("processLogoutRequest : isLBReq = " + isLBReq + ", foundPeer = " + foundPeer);
            }

            if (list != null && !list.isEmpty()) {
               if (!isVerified && !LogoutUtil.verifySLORequest(logoutReq, realm, logoutReq.getIssuer().getValue(), spEntityID, "SPRole")) {
                  throw new SAML2Exception(SAML2Utils.BUNDLE.getString("invalidSignInRequest"));
               }

               try {
                  userId = sessionProvider.getPrincipalName(sessionProvider.getSession(request));
                  if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                     SAML2SDKUtils.DEBUG.message("SPSingleLogout.processLogoutRequest, user = " + userId);
                  }
               } catch (SessionException ex) {
                  if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                     SAML2SDKUtils.DEBUG.message("SPSingleLogout: Session not found or not authenticated.");
                  }
               }

               userId = preSingleLogoutProcess(spEntityID, realm, request, response, userId, logoutReq, null, binding);
               BaseConfigType spConfig = Objects.requireNonNull(SAML2Utils.getSAML2MetaManager().getSPSSOConfig(realm, spEntityID));
               List<String> appLogoutURL = SAML2MetaUtils.getAttributes(spConfig).get("appLogoutUrl");
               if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                  SAML2SDKUtils.DEBUG.message("IDPLogoutUtil.processLogoutRequest: external app logout URL= " + appLogoutURL);
               }

               if (numSI == 0) {
                  List<String> tokenIDsToBeDestroyed = new ArrayList<>();
                  synchronized (list) {
                     Iterator<SPFedSession> iter = list.listIterator();

                     while (iter.hasNext()) {
                        SPFedSession fedSession = iter.next();
                        tokenIDsToBeDestroyed.add(fedSession.spTokenID);
                        iter.remove();
                     }
                  }

                  for (String spFedSession : tokenIDsToBeDestroyed) {
                     Object token;
                     try {
                        token = sessionProvider.getSession(spFedSession);
                     } catch (SessionException se) {
                        SAML2SDKUtils.DEBUG.error("processLogoutRequest : Could not create session from token ID = " + spFedSession);
                        continue;
                     }

                     if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                        SAML2SDKUtils.DEBUG.message("processLogoutRequest : destroy token " + spFedSession);
                     }

                     if (appLogoutURL != null && !appLogoutURL.isEmpty()) {
                        SAML2Utils.postToAppLogout(request, appLogoutURL.getFirst(), token);
                     }

                     if (destroySession) {
                        sessionProvider.invalidateSession(token, request, response);
                     }
                  }

                  if (foundPeer) {
                     boolean peerError = false;

                     for (String remoteServiceURL : remoteServiceURLs) {
                        String remoteLogoutURL = getRemoteLogoutURL(remoteServiceURL, request);
                        LogoutResponse logoutRes = LogoutUtil.forwardToRemoteServer(logoutReq, remoteLogoutURL);
                        if (logoutRes == null || !isSuccess(logoutRes) && !isNameNotFound(logoutRes)) {
                           peerError = true;
                        }
                     }

                     if (peerError) {
                        status = PARTIAL_LOGOUT_STATUS;
                     } else {
                        status = SUCCESS_STATUS;
                     }
                  }
               } else {
                  String sessionIndex = null;
                  List<String> siNotFound = new ArrayList<>();

                  for (int i = 0; i < numSI; i++) {
                     sessionIndex = siList.get(i);
                     String tokenIDToBeDestroyed = null;
                     synchronized (list) {
                        Iterator<SPFedSession> iter = list.listIterator();

                        while (iter.hasNext()) {
                           SPFedSession fedSession = iter.next();
                           if (sessionIndex.equals(fedSession.idpSessionIndex)) {
                              if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                                 SAML2SDKUtils.DEBUG.message("processLogoutRequest :  found si + " + sessionIndex);
                              }

                              tokenIDToBeDestroyed = fedSession.spTokenID;
                              iter.remove();
                              break;
                           }
                        }
                     }

                     if (tokenIDToBeDestroyed != null) {
                        try {
                           Object token = sessionProvider.getSession(tokenIDToBeDestroyed);
                           if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                              SAML2SDKUtils.DEBUG.message("processLogoutRequest : destroy token (2) " + tokenIDToBeDestroyed);
                           }

                           if (appLogoutURL != null && !appLogoutURL.isEmpty()) {
                              SAML2Utils.postToAppLogout(request, appLogoutURL.getFirst(), token);
                           }

                           if (destroySession) {
                              sessionProvider.invalidateSession(token, request, response);
                           }
                        } catch (SessionException se) {
                           SAML2SDKUtils.DEBUG.error("processLogoutRequest : Could not create session from token ID = " + tokenIDToBeDestroyed);
                        }
                     } else {
                        siNotFound.add(sessionIndex);
                     }
                  }

                  if (isLBReq) {
                     if (foundPeer && !siNotFound.isEmpty()) {
                        boolean peerError = false;
                        LogoutRequest lReq = copyAndMakeMutable(logoutReq);

                        for (String remoteServiceURL : remoteServiceURLs) {
                           lReq.setSessionIndex(siNotFound);
                           String remoteLogoutURL = getRemoteLogoutURL(remoteServiceURL, request);
                           LogoutResponse logoutRes = LogoutUtil.forwardToRemoteServer(lReq, remoteLogoutURL);
                           if (logoutRes != null && !isNameNotFound(logoutRes)) {
                              if (isSuccess(logoutRes)) {
                                 siNotFound = LogoutUtil.getSessionIndex(logoutRes);
                              } else {
                                 peerError = true;
                              }
                           }

                           if (SAML2SDKUtils.DEBUG.messageEnabled()) {
                              SAML2SDKUtils.DEBUG.message("processLogoutRequest : siNotFound = " + siNotFound);
                           }

                           if (siNotFound == null || siNotFound.isEmpty()) {
                              peerError = false;
                              break;
                           }
                        }

                        if (!peerError && (siNotFound == null || siNotFound.isEmpty())) {
                           status = SUCCESS_STATUS;
                        } else {
                           status = PARTIAL_LOGOUT_STATUS;
                        }
                     } else {
                        status = SUCCESS_STATUS;
                     }
                  } else if (siNotFound.isEmpty()) {
                     status = SUCCESS_STATUS;
                  } else {
                     status = SAML2Utils.generateStatus("urn:oasis:names:tc:SAML:2.0:status:Success", SAML2Utils.BUNDLE.getString("requestSuccess"));
                     LogoutUtil.setSessionIndex(status, siNotFound);
                  }
               }
            } else if (foundPeer) {
               boolean peerError = false;

               for (String remoteServiceURL : remoteServiceURLs) {
                  String remoteLogoutURL = getRemoteLogoutURL(remoteServiceURL, request);
                  LogoutResponse logoutRes = LogoutUtil.forwardToRemoteServer(logoutReq, remoteLogoutURL);
                  if (logoutRes != null && !isNameNotFound(logoutRes)) {
                     if (!isSuccess(logoutRes)) {
                        peerError = true;
                     } else if (numSI > 0) {
                        siList = LogoutUtil.getSessionIndex(logoutRes);
                        if (siList == null || siList.isEmpty()) {
                           peerError = false;
                           break;
                        }
                     }
                  }
               }

               if (!peerError && (siList == null || siList.isEmpty())) {
                  status = SUCCESS_STATUS;
               } else {
                  status = PARTIAL_LOGOUT_STATUS;
               }
            } else {
               SAML2SDKUtils.DEBUG.error("processLogoutRequest : invalid Name ID received");
               status = SAML2Utils.generateStatus("urn:oasis:names:tc:SAML:2.0:status:Responder", SAML2Utils.BUNDLE.getString("invalid_name_identifier"));
            }
         }
      } catch (SessionException se) {
         SAML2SDKUtils.DEBUG.error("processLogoutRequest: ", se);
         status = SAML2Utils.generateStatus("urn:oasis:names:tc:SAML:2.0:status:Responder", se.toString());
      } catch (SAML2Exception e) {
         SAML2SDKUtils.DEBUG.error("processLogoutRequest: failed to create response", e);
         status = SAML2Utils.generateStatus("urn:oasis:names:tc:SAML:2.0:status:Responder", e.toString());
      }

      if (spEntityID == null) {
         spEntityID = nameID.getSPNameQualifier();
      }

      LogoutResponse logResponse = LogoutUtil.generateResponse(status, logoutReq.getID(), issuer);
      if (isSuccess(logResponse)) {
         postSingleLogoutSuccess(spEntityID, realm, request, response, userId, logoutReq, logResponse, binding);
      }

      return logResponse;
   }

   private static boolean isSuccess(LogoutResponse logoutRes) {
      return logoutRes.getStatus().getStatusCode().getValue().equals("urn:oasis:names:tc:SAML:2.0:status:Success");
   }

   private static boolean isNameNotFound(LogoutResponse logoutRes) {
      Status status = logoutRes.getStatus();
      String statusMessage = status.getStatusMessage();
      return status.getStatusCode().getValue().equals("urn:oasis:names:tc:SAML:2.0:status:Responder")
         && statusMessage != null
         && statusMessage.equals(SAML2Utils.BUNDLE.getString("invalid_name_identifier"));
   }

   private static LogoutRequest copyAndMakeMutable(LogoutRequest src) {
      LogoutRequest dest = ProtocolFactory.getInstance().createLogoutRequest();

      try {
         dest.setNotOnOrAfter(src.getNotOnOrAfter());
         dest.setReason(src.getReason());
         dest.setEncryptedID(src.getEncryptedID());
         dest.setNameID(src.getNameID());
         dest.setBaseID(src.getBaseID());
         dest.setSessionIndex(src.getSessionIndex());
         dest.setIssuer(src.getIssuer());
         dest.setExtensions(src.getExtensions());
         dest.setID(src.getID());
         dest.setVersion(src.getVersion());
         dest.setIssueInstant(src.getIssueInstant());
         dest.setDestination(src.getDestination());
         dest.setConsent(src.getConsent());
      } catch (SAML2Exception ex) {
         SAML2SDKUtils.DEBUG.error("SPLogoutUtil.copyAndMakeMutable:", ex);
      }

      return dest;
   }

   private static String getSLOResponseLocationOrLocation(SPSSODescriptorType spsso, String binding) {
      String location = null;
      if (spsso != null) {
         List<EndpointType> sloList = spsso.getSingleLogoutService();
         if (sloList != null && !sloList.isEmpty()) {
            location = LogoutUtil.getSLOResponseServiceLocation(sloList, binding);
            if (location == null || location.isEmpty()) {
               location = LogoutUtil.getSLOServiceLocation(sloList, binding);
            }
         }
      }

      return location;
   }

   private static String getRemoteLogoutURL(String serverURL, HttpServletRequest request) {
      if (serverURL != null && request != null) {
         String queryString = request.getQueryString();
         return queryString == null
            ? serverURL + SAML2Utils.removeDeployUri(request.getRequestURI()) + "?isLBReq=false"
            : serverURL + SAML2Utils.removeDeployUri(request.getRequestURI()) + "?" + queryString + "&isLBReq=false";
      } else {
         return null;
      }
   }

   static {
      try {
         sm = new SAML2MetaManager();
      } catch (SAML2MetaException sme) {
         SAML2SDKUtils.DEBUG.error("Error retrieving metadata.", sme);
      }

      try {
         sessionProvider = SessionManager.getProvider();
      } catch (SessionException se) {
         SAML2SDKUtils.DEBUG.error("Error retrieving session provider.", se);
      }
   }
}
