package oasis.web.authn;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import javax.annotation.Priority;
import javax.inject.Inject;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.Provider;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Splitter;

import oasis.model.authn.AccessToken;
import oasis.model.authn.Token;
import oasis.model.authn.TokenRepository;
import oasis.services.authn.TokenHandler;
import oasis.services.authn.TokenSerializer;

/**
 * Implements Bearer Token authentication.
 *
 * @see <a href='http://tools.ietf.org/html/rfc6750'>OAuth 2.0 Bearer Token Usage</a>
 */
@Authenticated @OAuth
@Provider
@Priority(Priorities.AUTHENTICATION)
public class OAuthAuthenticationFilter implements ContainerRequestFilter {
  private static final Logger logger = LoggerFactory.getLogger(OAuthAuthenticationFilter.class);
  private static final String AUTH_SCHEME = "Bearer";
  private static final Splitter AUTH_SCHEME_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults();

  @Context ResourceInfo resourceInfo;
  @Inject TokenRepository tokenRepository;
  @Inject TokenHandler tokenHandler;

  @Override
  public void filter(ContainerRequestContext requestContext) throws IOException {
    String authorization = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
    if (authorization == null) {
      challenge(requestContext);
      return;
    }

    List<String> parts = AUTH_SCHEME_SPLITTER.splitToList(authorization);

    if (parts.isEmpty() || !AUTH_SCHEME.equalsIgnoreCase(parts.get(0))) {
      challenge(requestContext);
      return;
    }
    if (parts.size() != 2) {
      invalidRequest(requestContext);
      return;
    }

    Token token;
    try {
      token = TokenSerializer.unserialize(parts.get(1));
    } catch (Exception e) {
      invalidToken(requestContext);
      return;
    }

    // Get real information for the token
    token = tokenRepository.getToken(token.getId());

    if (!(token instanceof AccessToken) || !tokenHandler.checkTokenValidity(token)) {
      invalidToken(requestContext);
      return;
    }

    /** Check if the resource have an annotation {@link WithScopes} **/
    WithScopes withScopesAnnotation = resourceInfo.getResourceMethod().getAnnotation(WithScopes.class);
    if (withScopesAnnotation == null) {
      withScopesAnnotation = resourceInfo.getResourceClass().getAnnotation(WithScopes.class);
      if (withScopesAnnotation == null) {
        logger.error("Resource requires OAuth token but doesn't declare required scopes: {}", resourceInfo);
        // XXX: should we send a forbidden response rather than let the request go in?
      }
    }
    if (withScopesAnnotation != null && !((AccessToken) token).getScopeIds().containsAll(Arrays.asList(withScopesAnnotation.value()))) {
      insufficientScope(requestContext);
      return;
    }

    final OAuthPrincipal accountPrincipal = new OAuthPrincipal((AccessToken) token);
    final SecurityContext oldSecurityContext = requestContext.getSecurityContext();

    requestContext.setSecurityContext(new SecurityContext() {
      @Override
      public OAuthPrincipal getUserPrincipal() {
        return accountPrincipal;
      }

      @Override
      public boolean isUserInRole(String role) {
        return false;
      }

      @Override
      public boolean isSecure() {
        return oldSecurityContext.isSecure();
      }

      @Override
      public String getAuthenticationScheme() {
        return "OAUTH_BEARER";
      }
    });
  }

  private void challenge(ContainerRequestContext requestContext) {
    requestContext.abortWith(Response
        .status(Response.Status.UNAUTHORIZED)
        .header(HttpHeaders.WWW_AUTHENTICATE, AUTH_SCHEME)
        .build());
  }

  private void invalidRequest(ContainerRequestContext requestContext) {
    requestContext.abortWith(Response
        .status(Response.Status.BAD_REQUEST)
        .header(HttpHeaders.WWW_AUTHENTICATE, AUTH_SCHEME + " error=\"invalid_request\"")
        .build());
  }

  private void insufficientScope(ContainerRequestContext requestContext) {
    requestContext.abortWith(Response
        .status(Response.Status.FORBIDDEN)
        .header(HttpHeaders.WWW_AUTHENTICATE, AUTH_SCHEME + " error=\"insufficient_scope\"")
        .build());
  }

  private void invalidToken(ContainerRequestContext requestContext) {
    requestContext.abortWith(Response
        .status(Response.Status.BAD_REQUEST)
        .header(HttpHeaders.WWW_AUTHENTICATE, AUTH_SCHEME + " error=\"invalid_token\"")
        .build());
  }
}

@@ -672,152 +672,33 @@
 sis.
-model.authn.Token;%0Aimport oasis.model.authn.TokenRepository;%0Aimport oasis.services.authn.TokenHandler;%0Aimport oasis.services.authn.TokenSerializ
+services.authn.TokenHandl
 er;%0A
@@ -1282,51 +1282,8 @@
 fo;%0A
-  @Inject TokenRepository tokenRepository;%0A
   @I
@@ -1881,277 +1881,105 @@
     
-Token token;%0A    try %7B%0A      t
+AccessToken accessT
 oken = 
-T
+t
 oken
-Serializer.unserialize(parts.get(1));%0A    %7D catch (Exception e) %7B%0A      invalidToken(requestContext);%0A      return;%0A    %7D%0A%0A    // Get real information for the token%0A    token = tokenRepository.getToken(token.getId()
+Handler.getCheckedToken(parts.get(1), AccessToken.class
 );%0A
-%0A
     if (
 !(to
@@ -1978,82 +1978,27 @@
 if (
-!(token instanceof AccessToken) %7C%7C !tokenHandler.checkTokenValidity(token)
+accessToken == null
 ) %7B%0A
@@ -2657,19 +2657,17 @@
 ull && !
-((A
+a
 ccessTok
@@ -2668,24 +2668,16 @@
 essToken
-) token)
 .getScop
@@ -2868,18 +2868,17 @@
 incipal(
-(A
+a
 ccessTok
@@ -2884,15 +2884,8 @@
 ken)
- token)
 ;%0A  

Complete move to TokenHandler#getCheckedToken