/**
   * Validates the authentication request according to IdP Initiated SAML SSO Web Browser
   * Specification
   *
   * @return SAMLSSOSignInResponseDTO
   * @throws org.wso2.carbon.identity.base.IdentityException
   */
  public SAMLSSOReqValidationResponseDTO validate() throws IdentityException {

    SAMLSSOReqValidationResponseDTO validationResponse = new SAMLSSOReqValidationResponseDTO();
    try {

      // spEntityID MUST NOT be null
      if (StringUtils.isNotBlank(spEntityID)) {
        validationResponse.setIssuer(spEntityID);
      } else {
        String errorResp =
            SAMLSSOUtil.buildErrorResponse(
                SAMLSSOConstants.StatusCodes.REQUESTOR_ERROR,
                "spEntityID parameter not found in request",
                null);
        if (log.isDebugEnabled()) {
          log.debug("spEntityID parameter not found in request");
        }
        validationResponse.setResponse(errorResp);
        validationResponse.setValid(false);
        return validationResponse;
      }

      if (!SAMLSSOUtil.isSAMLIssuerExists(
          spEntityID, SAMLSSOUtil.getTenantDomainFromThreadLocal())) {
        String message =
            "A Service Provider with the Issuer '"
                + spEntityID
                + "' is not registered. Service "
                + "Provider should be registered in advance";
        log.error(message);
        String errorResp =
            SAMLSSOUtil.buildErrorResponse(
                SAMLSSOConstants.StatusCodes.REQUESTOR_ERROR, message, null);
        validationResponse.setResponse(errorResp);
        validationResponse.setValid(false);
        return validationResponse;
      }

      // If SP has multiple ACS
      if (StringUtils.isNotBlank(acs)) {
        validationResponse.setAssertionConsumerURL(acs);
      }

      if (StringUtils.isBlank(SAMLSSOUtil.getTenantDomainFromThreadLocal())) {
        SAMLSSOUtil.setTenantDomainInThreadLocal(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME);
      }

      validationResponse.setValid(true);

      if (log.isDebugEnabled()) {
        log.debug("IdP Initiated SSO request validation is successful");
      }
      return validationResponse;
    } catch (Exception e) {
      throw IdentityException.error("Error validating the IdP Initiated SSO request", e);
    }
  }
  private void handleLogoutResponseFromFramework(
      HttpServletRequest request, HttpServletResponse response, SAMLSSOSessionDTO sessionDTO)
      throws ServletException, IOException, IdentityException {

    SAMLSSOReqValidationResponseDTO validationResponseDTO = sessionDTO.getValidationRespDTO();

    if (validationResponseDTO != null) {
      // sending LogoutRequests to other session participants
      LogoutRequestSender.getInstance()
          .sendLogoutRequests(validationResponseDTO.getLogoutRespDTO());
      SAMLSSOUtil.removeSession(sessionDTO.getSessionId(), validationResponseDTO.getIssuer());
      removeSessionDataFromCache(
          CharacterEncoder.getSafeText(request.getParameter("sessionDataKey")));

      if (validationResponseDTO.isIdPInitSLO()) {
        // redirecting to the return URL or IS logout page
        response.sendRedirect(validationResponseDTO.getReturnToURL());
      } else {
        // sending LogoutResponse back to the initiator
        sendResponse(
            request,
            response,
            sessionDTO.getRelayState(),
            validationResponseDTO.getLogoutResponse(),
            validationResponseDTO.getAssertionConsumerURL(),
            validationResponseDTO.getSubject(),
            null,
            sessionDTO.getTenantDomain());
      }
    } else {
      try {
        samlSsoService.doSingleLogout(request.getSession().getId());
      } catch (IdentityException e) {
        log.error("Error when processing the logout request!", e);
      }

      String errorResp =
          SAMLSSOUtil.buildErrorResponse(
              SAMLSSOConstants.StatusCodes.REQUESTOR_ERROR,
              "Invalid request",
              sessionDTO.getAssertionConsumerURL());
      sendNotification(
          errorResp,
          SAMLSSOConstants.Notification.INVALID_MESSAGE_STATUS,
          SAMLSSOConstants.Notification.INVALID_MESSAGE_MESSAGE,
          sessionDTO.getAssertionConsumerURL(),
          request,
          response);
    }
  }
  /**
   * This method handles authentication and sends authentication Response message back to the
   * Service Provider after successful authentication. In case of authentication failure the user is
   * prompted back for authentication.
   *
   * @param req
   * @param resp
   * @param sessionId
   * @throws IdentityException
   * @throws IOException
   * @throws ServletException
   */
  private void handleAuthenticationReponseFromFramework(
      HttpServletRequest req,
      HttpServletResponse resp,
      String sessionId,
      SAMLSSOSessionDTO sessionDTO)
      throws UserStoreException, IdentityException, IOException, ServletException {

    String sessionDataKey = CharacterEncoder.getSafeText(req.getParameter("sessionDataKey"));
    AuthenticationResult authResult = getAuthenticationResultFromCache(sessionDataKey);

    if (log.isDebugEnabled() && authResult == null) {
      log.debug("Session data is not found for key : " + sessionDataKey);
    }
    SAMLSSOReqValidationResponseDTO reqValidationDTO = sessionDTO.getValidationRespDTO();
    SAMLSSOAuthnReqDTO authnReqDTO = new SAMLSSOAuthnReqDTO();

    if (authResult == null || !authResult.isAuthenticated()) {

      if (log.isDebugEnabled() && authResult != null) {
        log.debug("Unauthenticated User");
      }

      if (reqValidationDTO.isPassive()) { // if passive

        List<String> statusCodes = new ArrayList<String>();
        statusCodes.add(SAMLSSOConstants.StatusCodes.NO_PASSIVE);
        statusCodes.add(SAMLSSOConstants.StatusCodes.IDENTITY_PROVIDER_ERROR);
        String destination = reqValidationDTO.getDestination();
        reqValidationDTO.setResponse(
            SAMLSSOUtil.buildErrorResponse(
                reqValidationDTO.getId(),
                statusCodes,
                "Cannot authenticate Subject in Passive Mode",
                destination));

        sendResponse(
            req,
            resp,
            sessionDTO.getRelayState(),
            reqValidationDTO.getResponse(),
            reqValidationDTO.getAssertionConsumerURL(),
            reqValidationDTO.getSubject(),
            null,
            sessionDTO.getTenantDomain());
        return;

      } else { // if forceAuthn or normal flow
        // TODO send a saml response with a status message.
        if (!authResult.isAuthenticated()) {
          String destination = reqValidationDTO.getDestination();
          String errorResp =
              SAMLSSOUtil.buildErrorResponse(
                  SAMLSSOConstants.StatusCodes.AUTHN_FAILURE,
                  "User authentication failed",
                  destination);
          sendNotification(
              errorResp,
              SAMLSSOConstants.Notification.EXCEPTION_STATUS,
              SAMLSSOConstants.Notification.EXCEPTION_MESSAGE,
              reqValidationDTO.getAssertionConsumerURL(),
              req,
              resp);
          return;
        } else {
          throw new IdentityException("Session data is not found for authenticated user");
        }
      }
    } else {
      populateAuthnReqDTO(req, authnReqDTO, sessionDTO, authResult);
      req.setAttribute(SAMLSSOConstants.AUTHENTICATION_RESULT, authResult);
      String relayState = null;

      if (req.getParameter(SAMLSSOConstants.RELAY_STATE) != null) {
        relayState = req.getParameter(SAMLSSOConstants.RELAY_STATE);
      } else {
        relayState = sessionDTO.getRelayState();
      }

      startTenantFlow(authnReqDTO.getTenantDomain());

      if (sessionId == null) {
        sessionId = UUIDGenerator.generateUUID();
      }

      SAMLSSOService samlSSOService = new SAMLSSOService();
      SAMLSSORespDTO authRespDTO =
          samlSSOService.authenticate(
              authnReqDTO,
              sessionId,
              authResult.isAuthenticated(),
              authResult.getAuthenticatedAuthenticators(),
              SAMLSSOConstants.AuthnModes.USERNAME_PASSWORD);

      if (authRespDTO.isSessionEstablished()) { // authenticated

        storeTokenIdCookie(sessionId, req, resp, authnReqDTO.getTenantDomain());
        removeSessionDataFromCache(
            CharacterEncoder.getSafeText(req.getParameter("sessionDataKey")));

        sendResponse(
            req,
            resp,
            relayState,
            authRespDTO.getRespString(),
            authRespDTO.getAssertionConsumerURL(),
            authRespDTO.getSubject().getAuthenticatedSubjectIdentifier(),
            authResult.getAuthenticatedIdPs(),
            sessionDTO.getTenantDomain());
      } else { // authentication FAILURE
        String errorResp = authRespDTO.getRespString();
        sendNotification(
            errorResp,
            SAMLSSOConstants.Notification.EXCEPTION_STATUS,
            SAMLSSOConstants.Notification.EXCEPTION_MESSAGE,
            authRespDTO.getAssertionConsumerURL(),
            req,
            resp);
      }
    }
  }
  /**
   * All requests are handled by this handleRequest method. In case of SAMLRequest the user will be
   * redirected to commonAuth servlet for authentication. Based on successful authentication of the
   * user a SAMLResponse is sent back to service provider. In case of logout requests, the IDP will
   * send logout requests to the other session participants and then send the logout response back
   * to the initiator.
   *
   * @param req
   * @param resp
   * @throws ServletException
   * @throws IOException
   */
  private void handleRequest(HttpServletRequest req, HttpServletResponse resp, boolean isPost)
      throws ServletException, IOException {
    String sessionId = null;
    Cookie ssoTokenIdCookie = getTokenIdCookie(req);

    if (ssoTokenIdCookie != null) {
      sessionId = ssoTokenIdCookie.getValue();
    }

    String queryString = req.getQueryString();
    if (log.isDebugEnabled()) {
      log.debug("Query string : " + queryString);
    }
    // if an openid authentication or password authentication
    String authMode = CharacterEncoder.getSafeText(req.getParameter("authMode"));
    if (!SAMLSSOConstants.AuthnModes.OPENID.equals(authMode)) {
      authMode = SAMLSSOConstants.AuthnModes.USERNAME_PASSWORD;
    }
    String relayState =
        CharacterEncoder.getSafeText(req.getParameter(SAMLSSOConstants.RELAY_STATE));
    String spEntityID =
        CharacterEncoder.getSafeText(
            req.getParameter(SAMLSSOConstants.QueryParameter.SP_ENTITY_ID.toString()));
    String samlRequest = CharacterEncoder.getSafeText(req.getParameter("SAMLRequest"));
    String sessionDataKey = CharacterEncoder.getSafeText(req.getParameter("sessionDataKey"));
    String slo =
        CharacterEncoder.getSafeText(
            req.getParameter(SAMLSSOConstants.QueryParameter.SLO.toString()));

    boolean isExpFired = false;
    try {

      String tenantDomain = CharacterEncoder.getSafeText(req.getParameter("tenantDomain"));
      SAMLSSOUtil.setTenantDomainInThreadLocal(tenantDomain);

      if (sessionDataKey != null) { // Response from common authentication framework.
        SAMLSSOSessionDTO sessionDTO = getSessionDataFromCache(sessionDataKey);

        if (sessionDTO != null) {
          SAMLSSOUtil.setTenantDomainInThreadLocal(sessionDTO.getTenantDomain());
          if (sessionDTO.isInvalidLogout()) {
            log.warn("Redirecting to default logout page due to an invalid logout request");
            String serverUrl = CarbonUIUtil.getAdminConsoleURL(req);
            resp.sendRedirect(
                serverUrl.replace(
                    SAMLSSOConstants.SAML_ENDPOINT, SAMLSSOConstants.DEFAULT_LOGOUT_LOCATION));
          } else if (sessionDTO.isLogoutReq()) {
            handleLogoutResponseFromFramework(req, resp, sessionDTO);
          } else {
            handleAuthenticationReponseFromFramework(req, resp, sessionId, sessionDTO);
          }

          removeAuthenticationResultFromCache(sessionDataKey);

        } else {
          log.error("Failed to retrieve sessionDTO from the cache for key " + sessionDataKey);
          String errorResp =
              SAMLSSOUtil.buildErrorResponse(
                  SAMLSSOConstants.StatusCodes.IDENTITY_PROVIDER_ERROR,
                  SAMLSSOConstants.Notification.EXCEPTION_STATUS,
                  null);
          sendNotification(
              errorResp,
              SAMLSSOConstants.Notification.EXCEPTION_STATUS,
              SAMLSSOConstants.Notification.EXCEPTION_MESSAGE,
              null,
              req,
              resp);
          return;
        }
      } else if (spEntityID != null || slo != null) { // idp initiated SSO/SLO
        handleIdPInitSSO(
            req, resp, relayState, queryString, authMode, sessionId, isPost, (slo != null));
      } else if (samlRequest != null) { // SAMLRequest received. SP initiated SSO
        handleSPInitSSO(
            req, resp, queryString, relayState, authMode, samlRequest, sessionId, isPost);
      } else {
        log.debug("Invalid request message or single logout message ");

        if (sessionId == null) {
          String errorResp =
              SAMLSSOUtil.buildErrorResponse(
                  SAMLSSOConstants.StatusCodes.REQUESTOR_ERROR, "Invalid request message", null);
          sendNotification(
              errorResp,
              SAMLSSOConstants.Notification.INVALID_MESSAGE_STATUS,
              SAMLSSOConstants.Notification.INVALID_MESSAGE_MESSAGE,
              null,
              req,
              resp);
        } else {
          // Non-SAML request are assumed to be logout requests
          sendToFrameworkForLogout(req, resp, null, null, sessionId, true, false);
        }
      }
    } catch (UserStoreException e) {
      if (log.isDebugEnabled()) {
        log.debug("Error occurred while handling SAML2 SSO request", e);
      }
      String errorResp = null;
      try {
        errorResp =
            SAMLSSOUtil.buildErrorResponse(
                SAMLSSOConstants.StatusCodes.IDENTITY_PROVIDER_ERROR,
                "Error occurred while handling SAML2 SSO request",
                null);
      } catch (IdentityException e1) {
        log.error("Error while building SAML response", e1);
      }
      sendNotification(
          errorResp,
          SAMLSSOConstants.Notification.EXCEPTION_STATUS,
          SAMLSSOConstants.Notification.EXCEPTION_MESSAGE,
          null,
          req,
          resp);
    } catch (IdentityException e) {
      log.error("Error when processing the authentication request!", e);
      String errorResp = null;
      try {
        errorResp =
            SAMLSSOUtil.buildErrorResponse(
                SAMLSSOConstants.StatusCodes.IDENTITY_PROVIDER_ERROR,
                "Error when processing the authentication request",
                null);
      } catch (IdentityException e1) {
        log.error("Error while building SAML response", e1);
      }
      sendNotification(
          errorResp,
          SAMLSSOConstants.Notification.EXCEPTION_STATUS,
          SAMLSSOConstants.Notification.EXCEPTION_MESSAGE,
          null,
          req,
          resp);
    }
  }