
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.OutputStreamWriter;
import java.io.PrintWriter;
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 javax.servlet.ServletOutputStream;

public class IamTokenFilter implements Filter
{
   private static final Logger LOGGER = Logger.getLogger(IamTokenFilter.class.getName());
   private static final IamTokenSettings settings = IamTokenSettings.getInstance();

   final String RESPONSE_ENCODING = "utf-8";

   final String keysUrl = settings.security.jwksEndpoint;
   final String INTROSPECT_URL = settings.getIntrospectUrl();
   final String CLIENT_PASS = settings.getClientName() + ":" + settings.getClientPassword();


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

   @Override
   public void destroy() {}


   @Override
   public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
      throws IOException, ServletException
   {
      String authHeader = ((HttpServletRequest)req).getHeader("Authorization");

      ServletOutputStream  respOutputStream = resp.getOutputStream();
      PrintWriter writer = new PrintWriter(new OutputStreamWriter(respOutputStream, RESPONSE_ENCODING));

      if(authHeader==null)
      {
         final String AUTH_ERR = "Request without Authorization header. Only authenticated requests allowed.";
         LOGGER.warning(AUTH_ERR);
         sendAuthenticationError((HttpServletResponse)resp, writer, AUTH_ERR);
      }
      else
      {
         authHeader = authHeader.trim();

         if (authHeader.startsWith("Bearer ") && (authHeader.length() > "Bearer ".length()))
         {
            LOGGER.warning("Request with Authorization header and has Bearer entry");
            String token = authHeader.substring("Bearer ".length()).trim();

            doFilterBearer(req, token, resp, chain);
         }
         else
         {
            final String AUTH_ERR = "Authorization header with Bearer-token expected, but it starts with : "
               + authHeader.substring(0, "Bearer ".length()) + "...";
            LOGGER.warning(AUTH_ERR);
            sendUsageError((HttpServletResponse)resp, writer, AUTH_ERR);
         }
      }
   }




   private void doFilterBearer(ServletRequest req, String token, ServletResponse resp, FilterChain chain)
         throws IOException, ServletException
      {
         LOGGER.fine("trace");

         HttpServletRequest  request  = (HttpServletRequest) req;
         HttpServletResponse response = (HttpServletResponse)resp;

         ServletOutputStream  respOutputStream = response.getOutputStream();
         PrintWriter writer = new PrintWriter(new OutputStreamWriter(respOutputStream, RESPONSE_ENCODING));

         try
         {
            IntrospectResponse insResp = new IntrospectResponse(CLIENT_PASS, INTROSPECT_URL, token);

            if(insResp.isTokenActive())
            {
               String idString = request.getParameter("ID");
               Ivoid ivoid = new Ivoid(idString);

               String ivoidPath = ivoid.getLocalPart();
               String tokenPath = insResp.getPathFromStorageReadScope();

               LOGGER.finest("Path from IVOID: " + ivoidPath);
               LOGGER.finest("Path from token: " + tokenPath);

               if(tokenPath.endsWith(ivoidPath))
               {
                  LOGGER.finest("Access authorized.");
                  chain.doFilter(request, response);
               }
               else
               {
                  final String AUTH_ERR = "Bearer token does not authorize access to : " + ivoidPath;
                  LOGGER.finer(AUTH_ERR);
                  sendAuthorizationError(response, writer, AUTH_ERR);
               }
            }
            else
            {
               final String AUTH_ERR = "Bearer-token is inactive.";
               LOGGER.finer(AUTH_ERR);
               sendAuthorizationError(response, writer, AUTH_ERR);
            }

         }
         catch(IndexOutOfBoundsException ex)
         {
            LOGGER.warning("IndexOutOfBoundsException: " + ex.getMessage());
            sendUsageError(response, writer, ex.getMessage());
         }
         catch(IllegalArgumentException ex)
         {
            LOGGER.warning("IllegalArgumentException: " + ex.getMessage());
            sendUsageError(response, writer, ex.getMessage());
         }
         catch(Exception ex)
         {
            LOGGER.severe("Exception: " + ex.getMessage());
            ex.printStackTrace();
            sendError(response, writer, ex.toString());
         }
         finally
         {
            writer.close();
            respOutputStream.close();
         }
      }



   // 5. SODA sunc Responses [Table 6]
   // Success: 200 (Ok) or 204 (NO Content) and set HTTP-Headers: Content-Type & Content_encoding (if applicable)
   //Error Code          Description
   //===================================================================================
   //Error               General error (not covered below)
   //AuthenticationError Not authenticated
   //AuthorizationError  Not authorized to access the resource
   //ServiceUnavailable  Transient error (could succeed with retry)
   //UsageError          Permanent error (retry pointless)
   //MultiValuedParamNotSupported  request included multiple values for a parameter
   //                              but the service only supports a single value 


   protected void sendError(HttpServletResponse response, PrintWriter printWriter, String message)
   {
      response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
      response.setContentType("text/plain");
      printWriter.println("Error : " + message);
   }


   protected void sendAuthenticationError(HttpServletResponse response, PrintWriter printWriter, String message)
   {
      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
      response.setContentType("text/plain");
      printWriter.println("AuthenticationError : " + message);
   }


   protected void sendAuthorizationError(HttpServletResponse response, PrintWriter printWriter, String message)
   {
      response.setStatus(HttpServletResponse.SC_FORBIDDEN);
      response.setContentType("text/plain");
      printWriter.println("AuthorizationError : " + message);
   }


   protected void sendServiceUnavailable(HttpServletResponse response, PrintWriter printWriter, String message)
   {
      response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
      response.setContentType("text/plain");
      printWriter.println("ServiceUnavailable : " + message);
   }


   protected void sendUsageError(HttpServletResponse response, PrintWriter printWriter, String message)
   {
      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      response.setContentType("text/plain");
      printWriter.println("UsageError : " + message);
   }


   protected void sendMultiValuedParamNotSupported(HttpServletResponse response, PrintWriter printWriter, String message)
   {
      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      response.setContentType("text/plain");
      printWriter.println("MultiValuedParamNotSupported : " + message);
   }






   // Implementation with JWKs endpoint (explicit signiture verification):

   //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";

   /*/@Override
     public void OLD_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.finest(request.getRequestURL().toString());
     else
     LOGGER.finest(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.finest("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.finest("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;
     }

     }
     }
*/


   /*/ validate and parse the token

     private List<String> parseScopes(Claims claims)
     {
     String scopeStr = (String)claims.get("scope");
     List<String> scopes = new ArrayList<String>(Arrays.asList(scopeStr.split(" ")));

     if (scopes.stream().anyMatch(s -> s.startsWith("storage.read:")))
     {
     return scopes;
     }
     else
     {
     final String AUTH_ERR = "Invalid token: missing storage.read scope: " + scopeStr;
     LOGGER.warning(AUTH_ERR);
     throw new InvalidTokenException(AUTH_ERR);
     }
     }



     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 IamSigningKeyResolver(keysUrl))
   .build()
   .parseClaimsJws(jwsString);

   Claims claims = jws.getBody();

   LOGGER.finest("scope: " + (String)claims.get("scope"));

   List<String> scopes = parseScopes(claims);

   String storageReadScope = "";

   for(int i=0;i<scopes.size();i++)
   {
   if(scopes.get(i).startsWith("storage.read:"))
   {
   storageReadScope = scopes.get(i);
   }
   }

   LOGGER.finest("storage.read: " + storageReadScope);

   String path = storageReadScope.substring(storageReadScope.lastIndexOf(":") + 1);

   LOGGER.finest("path: " + path);

   // 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(scopes); // FIXME temp store scopes where roles were in neanias

   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;
      }
   }
*/




   /*
      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;
      }
      */


}

