package com.sun.identity.saml2.plugins;

import com.sun.identity.saml2.assertion.AuthnContext;
import com.sun.identity.saml2.common.SAML2Exception;
import com.sun.identity.saml2.common.SAML2Utils;
import com.sun.identity.saml2.profile.SPCache;
import com.sun.identity.saml2.profile.SPSSOFederate;
import com.sun.identity.saml2.protocol.ProtocolFactory;
import com.sun.identity.saml2.protocol.RequestedAuthnContext;
import com.sun.identity.shared.encode.URLEncDec;
import com.sun.identity.shared.xml.XMLUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Map.Entry;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

public class DefaultSPAuthnContextMapper implements SPAuthnContextMapper {
   private static final String AUTHN_CONTEXT_NONE = "none";
   private static final String DEFAULT = "default";
   private static final String DEFAULT_CLASS_REF = "defaultClassRef";

   @Override
   public RequestedAuthnContext getRequestedAuthnContext(String realm, String hostEntityID, Map<String, List<String>> paramsMap) throws SAML2Exception {
      if (this.areNoAuthnContextClassrefsRequested(realm, hostEntityID)) {
         return null;
      }

      List<String> authContextClassRef = paramsMap.get("AuthnContextClassRef");
      List<String> authLevelList = paramsMap.get("AuthLevel");
      Integer authLevel = null;
      if (authLevelList != null && !authLevelList.isEmpty()) {
         try {
            authLevel = Integer.valueOf(authLevelList.getFirst());
         } catch (NumberFormatException nfe) {
            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message("not a valid integer", nfe);
            }
         } catch (Exception e) {
            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message("error getting integer object", e);
            }
         }
      }

      if (authLevel == null) {
         authLevel = this.getAuthLevelFromAdvice(paramsMap);
      }

      if (SAML2Utils.DEBUG.messageEnabled()) {
         SAML2Utils.DEBUG.message("authLevel in Query:" + authLevel);
         SAML2Utils.DEBUG.message("authContextClassRef in Query:" + authContextClassRef);
      }

      Map<String, Object> authRefMap = getAuthRefMap(realm, hostEntityID);
      List<String> authCtxList = new ArrayList<>();
      if (authContextClassRef != null && !authContextClassRef.isEmpty()) {
         for (String s : authContextClassRef) {
            String authClassRef = prefixIfRequired(s);
            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message("DefaultSPAuthnContextMapper: authClassRef=" + authClassRef);
            }

            authCtxList.add(authClassRef);
         }
      }

      if (authLevel != null) {
         for (Entry<String, Object> entry : authRefMap.entrySet()) {
            String className = entry.getKey();
            if (!"default".equals(className) && !"defaultClassRef".equals(className)) {
               Integer aLevel = (Integer)entry.getValue();
               if (aLevel != null && aLevel >= authLevel) {
                  authCtxList.add(className);
               }
            }
         }
      }

      if ((authCtxList == null || authCtxList.isEmpty()) && authRefMap != null && !authRefMap.isEmpty()) {
         String defaultClassRef = (String)authRefMap.get("defaultClassRef");
         if (defaultClassRef != null) {
            authCtxList.add(defaultClassRef);
         } else {
            for (String val : authRefMap.keySet()) {
               if (val != null && !val.equals("default")) {
                  authCtxList.add(val);
               }
            }
         }
      }

      if (authCtxList.isEmpty()) {
         authCtxList.add("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport");
      }

      if (SAML2Utils.DEBUG.messageEnabled()) {
         SAML2Utils.DEBUG.message("SPCache.authContextHash is: " + SPCache.authContextHash);
         SAML2Utils.DEBUG.message("authCtxList is: " + authCtxList);
      }

      String authCtxComparison = SPSSOFederate.getParameter(paramsMap, "AuthComparison");
      if (SAML2Utils.DEBUG.messageEnabled()) {
         SAML2Utils.DEBUG.message("AuthComparison in Query:" + authCtxComparison);
      }

      if (authCtxComparison == null || !isValidAuthComparison(authCtxComparison)) {
         authCtxComparison = SAML2Utils.getAttributeValueFromSSOConfig(realm, hostEntityID, "SPRole", "spAuthncontextComparisonType");
         if (authCtxComparison != null && !isValidAuthComparison(authCtxComparison)) {
            authCtxComparison = null;
         }
      }

      RequestedAuthnContext reqCtx = ProtocolFactory.getInstance().createRequestedAuthnContext();
      reqCtx.setAuthnContextClassRef(authCtxList);
      reqCtx.setComparison(authCtxComparison);
      return reqCtx;
   }

   private boolean areNoAuthnContextClassrefsRequested(String realm, String hostEntityID) {
      List<?> authContextClassRefConfig = SAML2Utils.getAllAttributeValueFromSSOConfig(realm, hostEntityID, "SPRole", "spAuthncontextClassrefMapping");
      return authContextClassRefConfig != null && authContextClassRefConfig.size() == 1 && "none".equals(authContextClassRefConfig.getFirst());
   }

   private Integer getAuthLevelFromAdvice(Map<String, List<String>> paramsMap) {
      Integer level = null;
      List<String> advices = paramsMap.get("sunamcompositeadvice");
      if (advices != null && !advices.isEmpty()) {
         String adviceXML = URLEncDec.decode(advices.getFirst());
         if (SAML2Utils.DEBUG.messageEnabled()) {
            SAML2Utils.DEBUG.message("DefaultSPAuthnContextMapper:adviceXML=" + adviceXML);
         }

         Set<String> authLevelvalues = null;
         Document document = XMLUtils.toDOMDocument(adviceXML, SAML2Utils.DEBUG);
         if (document != null) {
            Node adviceNode = XMLUtils.getRootNode(document, "Advices");
            if (adviceNode != null) {
               Map<String, Set<String>> advicePair = XMLUtils.parseAttributeValuePairTags(adviceNode);
               authLevelvalues = advicePair.get("AuthLevelConditionAdvice");
            }
         }

         if (authLevelvalues != null && !authLevelvalues.isEmpty()) {
            for (String authLevelvalue : authLevelvalues) {
               if (authLevelvalue != null && !authLevelvalue.isEmpty()) {
                  int index = authLevelvalue.indexOf(":");
                  String authLevelStr = null;
                  if (index != -1) {
                     authLevelStr = authLevelvalue.substring(index + 1).trim();
                  } else {
                     authLevelStr = authLevelvalue;
                  }

                  try {
                     Integer authLevel = Integer.valueOf(authLevelStr);
                     if (level == null || level.compareTo(authLevel) > 0) {
                        level = authLevel;
                     }
                  } catch (Exception nex) {
                  }
               }
            }
         }
      }

      return level;
   }

   @Override
   public int getAuthLevel(RequestedAuthnContext reqCtx, AuthnContext authnContext, String realm, String hostEntityID, String idpEntityID) throws SAML2Exception {
      Map<String, Object> authRefMap = SPCache.authContextHash.get(hostEntityID + "|" + realm);
      if (authRefMap == null || authRefMap.isEmpty()) {
         authRefMap = getAuthRefMap(realm, hostEntityID);
      }

      int authLevel = 0;
      if (SAML2Utils.DEBUG.messageEnabled()) {
         SAML2Utils.DEBUG.message("DefaultSPAuthnContextMapper:hostEntityID:" + hostEntityID);
         SAML2Utils.DEBUG.message("DefaultSPAuthnContextMapper:realm:" + realm);
         SAML2Utils.DEBUG.message("DefaultSPAuthnContextMapper:MAP:" + authRefMap);
         SAML2Utils.DEBUG.message("DefaultSPAuthnContextMapper:HASH:" + SPCache.authContextHash);
      }

      String authnClassRef = null;
      if (authnContext != null) {
         authnClassRef = authnContext.getAuthnContextClassRef();
      }

      if (reqCtx != null
         && authnClassRef != null
         && !this.isAuthnContextMatching(reqCtx.getAuthnContextClassRef(), authnClassRef, reqCtx.getComparison(), realm, hostEntityID)) {
         throw new SAML2Exception(SAML2Utils.BUNDLE.getString("invalidAuthnContextClassRef"));
      }

      Integer authLevelInt = null;
      if (authnClassRef != null && !authnClassRef.isEmpty()) {
         if (authRefMap != null && !authRefMap.isEmpty()) {
            authLevelInt = (Integer)authRefMap.get(authnClassRef);
         }
      } else if (authRefMap != null && !authRefMap.isEmpty()) {
         authLevelInt = (Integer)authRefMap.get("default");
      }

      if (authLevelInt != null) {
         authLevel = authLevelInt;
      }

      if (SAML2Utils.DEBUG.messageEnabled()) {
         SAML2Utils.DEBUG.message("DefaultSPAuthnContextMapper:authnClRef:" + authnClassRef);
         SAML2Utils.DEBUG.message("DefaultSPAuthnContextMapper:authLevel :" + authLevel);
      }

      return authLevel;
   }

   @Override
   public boolean isAuthnContextMatching(List<String> requestedACClassRefs, String acClassRef, String comparison, String realm, String hostEntityID) {
      Map<String, Object> authRefMap = getAuthRefMap(realm, hostEntityID);
      return SAML2Utils.isAuthnContextMatching(requestedACClassRefs, acClassRef, comparison, authRefMap);
   }

   private static Map<String, Object> getAuthnCtxFromSPConfig(String realm, String hostEntityID) {
      List<String> authContextClassRefConfig = SAML2Utils.getAllAttributeValueFromSSOConfig(realm, hostEntityID, "SPRole", "spAuthncontextClassrefMapping");
      if (SAML2Utils.DEBUG.messageEnabled()) {
         SAML2Utils.DEBUG.message("DefaultSPAuthnContextMapper: List:" + authContextClassRefConfig);
      }

      HashMap<String, Object> authRefMap = new LinkedHashMap<>();
      if (authContextClassRefConfig != null && authContextClassRefConfig.size() == 1 && "none".equals(authContextClassRefConfig.getFirst())) {
         return Collections.unmodifiableMap(authRefMap);
      }

      if (authContextClassRefConfig != null && !authContextClassRefConfig.isEmpty()) {
         for (String authRefVal : authContextClassRefConfig) {
            boolean isDefault = false;
            if (authRefVal.endsWith("|default")) {
               authRefVal = authRefVal.substring(0, authRefVal.length() - "default".length());
               isDefault = true;
            }

            StringTokenizer st = new StringTokenizer(authRefVal, "|");
            String authClass = null;

            try {
               authClass = st.nextToken();
            } catch (Exception e) {
               if (SAML2Utils.DEBUG.messageEnabled()) {
                  SAML2Utils.DEBUG.message("AuthnContextClassRef not found");
               }
            }

            if (st.hasMoreTokens()) {
               Integer authLevel = null;

               try {
                  authLevel = Integer.valueOf(st.nextToken());
               } catch (NumberFormatException nfe) {
                  if (SAML2Utils.DEBUG.messageEnabled()) {
                     SAML2Utils.DEBUG.message("DefaultSPAuthnContextMapper.getAuthnCtxFromSPConfig:", nfe);
                  }
               }

               if (SAML2Utils.DEBUG.messageEnabled()) {
                  SAML2Utils.DEBUG.message("DefaultSPAuthnContextMapper.getAuthnCtxFromSPConfig: AuthLevel is " + authLevel);
               }

               if (authLevel != null) {
                  if (isDefault && !authRefMap.containsKey("default")) {
                     authRefMap.put("default", authLevel);
                  }

                  if (authClass != null) {
                     authRefMap.put(prefixIfRequired(authClass), authLevel);
                  }
               }
            }

            if (isDefault && authClass != null && !authRefMap.containsKey("defaultClassRef")) {
               authRefMap.put("defaultClassRef", prefixIfRequired(authClass));
            }
         }
      }

      return Collections.unmodifiableMap(authRefMap);
   }

   private static boolean isValidAuthComparison(String authComparison) {
      return authComparison.equals("exact") || authComparison.equals("maximum") || authComparison.equals("minimum") || authComparison.equals("better");
   }

   private static Map<String, Object> getAuthRefMap(String realm, String hostEntityID) {
      String key = hostEntityID + "|" + realm;
      Map<String, Object> authRefMap = SPCache.authContextHash.get(key);
      if (authRefMap == null) {
         try {
            authRefMap = getAuthnCtxFromSPConfig(realm, hostEntityID);
            if (authRefMap != null && !authRefMap.isEmpty()) {
               SPCache.authContextHash.put(key, authRefMap);
            }
         } catch (Exception e) {
            if (SAML2Utils.DEBUG.messageEnabled()) {
               SAML2Utils.DEBUG.message("DefaultSPAuthnContextMapper.getAuthRefMap:", e);
            }
         }
      }

      return authRefMap;
   }

   private static String prefixIfRequired(String authClassRef) {
      return authClassRef != null && authClassRef.indexOf(58) == -1 ? "urn:oasis:names:tc:SAML:2.0:ac:classes:" + authClassRef : authClassRef;
   }
}
