
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.InvalidClaimException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.jackson.io.JacksonDeserializer;

import java.security.spec.InvalidKeySpecException;
import java.security.NoSuchAlgorithmException;

import javax.servlet.http.HttpServletRequestWrapper;
import java.security.Principal;

import java.io.IOException;
import java.util.List; // ArrayList<String>
import java.util.Map;
import java.util.HashMap;
import java.util.*;

import java.util.logging.Logger;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

//import NeaSigningKeyResolver;

public class NeaTokenFilter implements Filter
{
   private static final Logger LOGGER = Logger.getLogger("NeaTokenFilter");
   private static final NeaTokenSettings settings = NeaTokenSettings.getInstance();

   final String resourceId = settings.security.resourceId; //"vlkb"
   //final String realmName = "neanias-production";
   //final String keysUrl = "https://sso.neanias.eu/auth/realms/" + realmName + "/protocol/openid-connect/certs";
   final String keysUrl = settings.security.jwksEndpoint;

   @Override
   public void init(FilterConfig fc) throws ServletException {}

   @Override
   public void destroy() {}

   @Override
   public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
   throws IOException, ServletException
{
   HttpServletRequest request = (HttpServletRequest) req;
   HttpServletResponse response = (HttpServletResponse) res;

   String  qString = request.getQueryString();
   if(qString == null)
      LOGGER.info(request.getRequestURL().toString());
   else
      LOGGER.info(request.getRequestURL() + "    " + qString);

   String authHeader = request.getHeader("Authorization");
   if (authHeader == null)
   {
      boolean non_authenticated_request = (settings.security.non_authn_username != null);
 
      if(non_authenticated_request)
      {
         chain.doFilter(request, response);
      }
      else
      {
         LOGGER.info("Request without Authorization header, no Principal added");
         response.sendError(HttpServletResponse.SC_BAD_REQUEST,
               "No Authorization in HTTP-header. Only authorized requests allowed.");
      }
      return;
   }
   else
   {

      if (authHeader.startsWith("Bearer ") && (authHeader.length() > "Bearer ".length()))
      {
         LOGGER.info("Request with Authorization header and has Bearer entry");

         String jws = authHeader.substring("Bearer ".length());

         try
         {
            VlkbUser user = getUserFromAccessToken(jws);

            HttpServletRequestWrapper requestWithPrincipal
               = new RequestWithPrincipal(request, user);

            chain.doFilter(requestWithPrincipal, response);
            return;

         }
         catch (JwtException | InvalidTokenException ex)
         {
            LOGGER.warning("Token invalid: " + ex.toString());
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Token invalid");
            return;
         }
         catch (Exception ex)
         {
            LOGGER.severe(ex.toString());
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                  "Error during authorization");
            return;
         }

      }
      else
      {
         LOGGER.warning("Request with Authorization header but without Bearer token");

         response.sendError(HttpServletResponse.SC_BAD_REQUEST,
               "Only Bearer authorization supported or token missing");
         return;
      }

   }
}


private boolean isMapStringObject(Object obj)
{
   if(obj instanceof Map)
   {
      Map map = (Map)obj;

      Set<?> s = map.keySet();
      Iterator<?> it = s.iterator();
      while(it.hasNext())
      {
         Object el = it.next();
         if(! (el instanceof String))
         {
            return false;
         }
      }

      return true;
   }
   else
      return false;
}

// validate and parse the token

private List<String> validateAndParseRoles(Claims claims)
{
   Map mapobj = null;

   Object obj = claims.get("resource_access");
   if(isMapStringObject(obj))
      //if(obj instanceof Map<?,?>)
      mapobj = (Map)obj;//claims.get("resource_access");

   //////////////////////////////////////////////////////////////////////

   Map<String,Object> resourceAccess = (Map<String, Object>)claims.get("resource_access");

   if(resourceAccess != null)
   {

      Map<String, Object> resource = (Map<String, Object>)resourceAccess.get(resourceId);
      if (resource != null)
      {
         List<String> roles = (List<String>)resource.get("roles");
         return roles;
      }
      else
      {
         LOGGER.warning("Token invalid: 'resource_access' must have value: " + resourceId);
         throw new InvalidTokenException(
               "missing 'resource_access' must have value: " + resourceId);
      }
   }
   else
   {
      LOGGER.warning("Token invalid: missing 'resource_access' claim");
      throw new InvalidTokenException("missing 'resource_access' claim");
   }
}



VlkbUser getUserFromAccessToken(String jwsString)
   //throws JwtException, InvalidTokenException  <-- FIXME
{
   long clockSkew = 3 * 60; //3 minutes FIXME get from Config file

   Jws<Claims> jws = Jwts
      .parser()
      .setAllowedClockSkewSeconds(clockSkew) // FIXME needed ?
      .setSigningKeyResolver(new NeaSigningKeyResolver(keysUrl))
      .build()
      .parseClaimsJws(jwsString);

   Claims claims = jws.getBody();

   List<String> roles = validateAndParseRoles(claims);

   // set User/Principal

   VlkbUser user = new VlkbUser();
   user.setAccessToken(jwsString);
   user.setExpirationTime(0);//FIXME
   user.setUserId((String) claims.get("sub"));
   user.setUserLabel((String) claims.get("name"));
   user.setGroups(roles);

   return user;
}





private static class RequestWithPrincipal extends HttpServletRequestWrapper
{
   private final VlkbUser user;

   public RequestWithPrincipal(HttpServletRequest request, VlkbUser user)
   {
      super(request);
      this.user = user;
   }

   @Override
   public Principal getUserPrincipal() {
      return user;
   }
}

}

