package eu.dnetlib.oauth.repositories;

import com.sun.jersey.api.client.ClientResponse;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.surfnet.oaaas.auth.*;
import org.surfnet.oaaas.auth.OAuth2Validator.*;
import org.surfnet.oaaas.auth.principal.AuthenticatedPrincipal;
import org.surfnet.oaaas.auth.principal.BasicAuthCredentials;
import org.surfnet.oaaas.model.*;
import org.surfnet.oaaas.repository.AccessTokenRepository;
import org.surfnet.oaaas.repository.AuthorizationRequestRepository;
import org.surfnet.oaaas.resource.TokenResource;

import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.UUID;

import static org.surfnet.oaaas.auth.OAuth2Validator.*;

@Named
@Path("/")
public class CustomToken extends TokenResource {

    /**
     * Resource for handling all calls related to tokens. It adheres to <a
     * href="http://tools.ietf.org/html/draft-ietf-oauth-v2"> the OAuth spec</a>.
     */


    public static final String BASIC_REALM = "Basic realm=\"OAuth2 Secure\"";

    public static final String WWW_AUTHENTICATE = "WWW-Authenticate";

    @Inject
    private AuthorizationRequestRepository authorizationRequestRepository;

    @Inject
    private AccessTokenRepository accessTokenRepository;


    private Logger log = LoggerFactory.getLogger(this.getClass().getName());


    // @Inject
    //private OAuth2Validator oAuth2Validator;

    @Inject
    private ResourceOwnerAuthenticator resourceOwnerAuthenticator;

    private static final Logger LOG = LoggerFactory.getLogger(TokenResource.class);

    /**
     * The "authorization endpoint" as described in <a
     * href="http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-3.1">Section 3.1</a> of
     * the OAuth spec.  This provides the optional GET support.  Access to this endpoint requires
     * authentication of the requestor (the resource owner) which must be accomplished via a
     * configured {@link AuthenticationFilter}.
     *
     * @param request the {@link HttpServletRequest}
     * @return the response
     */
    @GET
    @Path("/authorize")
    public Response authorizeCallbackGet(@Context HttpServletRequest request) {
        return authorizeCallback(request);
    }

    /**
     * Entry point for the authorize call which needs to return an authorization
     * code or (implicit grant) an access token.    Access to this endpoint requires
     * authentication of the requestor (the resource owner) which must be accomplished via a
     * configured {@link AuthenticationFilter}.
     *
     * @param request the {@link HttpServletRequest}
     * @return Response the response
     */
    @POST
    @Produces(MediaType.TEXT_HTML)
    @Path("/authorize")
    public Response authorizeCallback(@Context HttpServletRequest request) {
        return doProcess(request);
    }

    /**
     * Called after the user has given consent
     *
     * @param request the {@link HttpServletRequest}
     * @return Response the response
     */
    @POST
    @Produces(MediaType.TEXT_HTML)
    @Path("/consent")
    @Override
    public Response consentCallback(@Context HttpServletRequest request) {
        return doProcess(request);
    }

    private Response doProcess(HttpServletRequest request) {
        log.info("CustomToken: now looking up...");

        if (authorizationRequestRepository == null) {
            log.info("*********************************8authorizationRequestRepository id NULL..");

        }


        AuthorizationRequest authReq = findAuthorizationRequest(request);


        if (authReq == null) {
            return serverError("Not a valid AbstractAuthenticator.AUTH_STATE on the Request");
        }
        processScopes(authReq, request);
        if (authReq.getResponseType().equals(OAuth2Validator.IMPLICIT_GRANT_RESPONSE_TYPE)) {
            AccessToken token = createAccessToken(authReq, true);
            return sendImplicitGrantResponse(authReq, token);
        } else {
            return sendAuthorizationCodeResponse(authReq);
        }
    }

    /*
     * In the user consent filter the scopes are (possible) set on the Request
     */
    private void processScopes(AuthorizationRequest authReq, HttpServletRequest request) {
        if (authReq.getClient().isSkipConsent()) {
            // return the scopes in the authentication request since the requested scopes are stored in the
            // authorizationRequest.
            authReq.setGrantedScopes(authReq.getRequestedScopes());
        } else {
            String[] scopes = (String[]) request.getAttribute(AbstractUserConsentHandler.GRANTED_SCOPES);
            if (!ArrayUtils.isEmpty(scopes)) {
                authReq.setGrantedScopes(Arrays.asList(scopes));
            } else {
                authReq.setGrantedScopes(null);
            }
        }
    }

    private AccessToken createAccessToken(AuthorizationRequest request, boolean isImplicitGrant) {
        Client client = request.getClient();
        long expireDuration = client.getExpireDuration();
        long expires = (expireDuration == 0L ? 0L : (System.currentTimeMillis() + (1000 * expireDuration)));
        String refreshToken = (client.isUseRefreshTokens() && !isImplicitGrant) ? getTokenValue(true) : null;
        AuthenticatedPrincipal principal = request.getPrincipal();
        AccessToken token = new AccessToken(getTokenValue(false), principal, client, expires, request.getGrantedScopes(), refreshToken);
        return accessTokenRepository.save(token);
    }

    private AuthorizationRequest findAuthorizationRequest(HttpServletRequest request) {

        log.info("*********************************findAuthorizationRequest state " + request.getAttribute(AbstractAuthenticator.AUTH_STATE));


        String authState = (String) request.getAttribute(AbstractAuthenticator.AUTH_STATE);

        log.info("*********************************findAuthorizationRequest state " + authorizationRequestRepository.findByAuthState(authState));

        return authorizationRequestRepository.findByAuthState(authState);
    }

    /**
     * The "token endpoint" as described in <a
     * href="http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-3.2">Section 3.2</a> of
     * the OAuth spec.
     *
     * @param authorization  the HTTP Basic auth header.
     * @param formParameters the request parameters
     * @return the response
     */
    @POST
    @Path("/token")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes("application/x-www-form-urlencoded")
    public Response token(@HeaderParam("Authorization") String authorization,
                          final MultivaluedMap<String, String> formParameters) {
        // Convert incoming parameters into internal form and validate them
        AccessTokenRequest accessTokenRequest =
                AccessTokenRequest.fromMultiValuedFormParameters(formParameters);
        BasicAuthCredentials credentials =
                BasicAuthCredentials.createCredentialsFromHeader(authorization);

        //  ValidationResponse vr = oAuth2Validator.validate(accessTokenRequest, credentials);
        //   if (!vr.valid()) {
        //       return sendErrorResponse(vr);
        //  }

        // The request looks valid, attempt to process
        String grantType = accessTokenRequest.getGrantType();
        AuthorizationRequest request;
        try {
            if (GRANT_TYPE_AUTHORIZATION_CODE.equals(grantType)) {
                request = authorizationCodeToken(accessTokenRequest);
            } else if (GRANT_TYPE_REFRESH_TOKEN.equals(grantType)) {
                request = refreshTokenToken(accessTokenRequest);
            } else if (GRANT_TYPE_CLIENT_CREDENTIALS.equals(grantType)) {
                request = clientCredentialToken(accessTokenRequest);
            } else if (GRANT_TYPE_PASSWORD.equals(grantType)) {
                request = passwordToken(accessTokenRequest);
            } else {
                return sendErrorResponse(ValidationResponse.UNSUPPORTED_GRANT_TYPE);
            }
        } catch (ValidationResponseException e) {
            return sendErrorResponse(e.v);
        }
        AccessToken token = createAccessToken(request, false);

        AccessTokenResponse response = new AccessTokenResponse(token.getToken(), BEARER, token.getExpiresIn(), token.getRefreshToken(), StringUtils.join(token.getScopes(), ' '));

        return Response
                .ok()
                .entity(response)
                .cacheControl(cacheControlNoStore())
                .header("Pragma", "no-cache")
                .build();

    }

    private CacheControl cacheControlNoStore() {
        CacheControl cacheControl = new CacheControl();
        cacheControl.setNoStore(true);
        return cacheControl;
    }

    private AuthorizationRequest authorizationCodeToken(AccessTokenRequest accessTokenRequest) {
        AuthorizationRequest authReq = authorizationRequestRepository.findByAuthorizationCode(accessTokenRequest.getCode());
        if (authReq == null) {
            throw new ValidationResponseException(ValidationResponse.INVALID_GRANT_AUTHORIZATION_CODE);
        }
        String uri = accessTokenRequest.getRedirectUri();
        if (!authReq.getRedirectUri().equalsIgnoreCase(uri)) {
            throw new ValidationResponseException(ValidationResponse.REDIRECT_URI_DIFFERENT);
        }
        authorizationRequestRepository.delete(authReq);
        return authReq;
    }

    private AuthorizationRequest refreshTokenToken(AccessTokenRequest accessTokenRequest) {
        AccessToken accessToken = accessTokenRepository.findByRefreshToken(accessTokenRequest.getRefreshToken());
        if (accessToken == null) {
            throw new ValidationResponseException(ValidationResponse.INVALID_GRANT_REFRESH_TOKEN);
        }
        AuthorizationRequest request = new AuthorizationRequest();
        request.setClient(accessToken.getClient());
        request.setPrincipal(accessToken.getPrincipal());
        request.setGrantedScopes(accessToken.getScopes());
        accessTokenRepository.delete(accessToken);
        return request;

    }

    private AuthorizationRequest clientCredentialToken(AccessTokenRequest accessTokenRequest) {
        AuthorizationRequest request = new AuthorizationRequest();
        request.setClient(accessTokenRequest.getClient());
        // We have to construct a AuthenticatedPrincipal on-the-fly as there is only key-secret authentication
        request.setPrincipal(new AuthenticatedPrincipal(request.getClient().getClientId()));
        // Get scopes (either from request or the client's default set)
        request.setGrantedScopes(accessTokenRequest.getScopeList());
        return request;
    }

    private AuthorizationRequest passwordToken(AccessTokenRequest accessTokenRequest) {
        // Authenticate the resource owner
        AuthenticatedPrincipal principal =
                resourceOwnerAuthenticator.authenticate(accessTokenRequest.getUsername(), accessTokenRequest.getPassword());
        if (principal == null) {
            throw new ValidationResponseException(ValidationResponse.INVALID_GRANT_PASSWORD);
        }

        AuthorizationRequest request = new AuthorizationRequest();
        request.setClient(accessTokenRequest.getClient());
        request.setPrincipal(principal);
        request.setGrantedScopes(accessTokenRequest.getScopeList());
        return request;
    }


    private Response sendAuthorizationCodeResponse(AuthorizationRequest authReq) {
        String uri = authReq.getRedirectUri();
        String authorizationCode = getAuthorizationCodeValue();
        authReq.setAuthorizationCode(authorizationCode);
        authorizationRequestRepository.save(authReq);
        uri = uri + appendQueryMark(uri) + "code=" + authorizationCode + appendStateParameter(authReq);
        return Response
                .seeOther(UriBuilder.fromUri(uri).build())
                .cacheControl(cacheControlNoStore())
                .header("Pragma", "no-cache")
                .build();
    }

    protected String getTokenValue(boolean isRefreshToken) {
        return UUID.randomUUID().toString();
    }

    protected String getAuthorizationCodeValue() {
        return getTokenValue(false);
    }

    private Response sendErrorResponse(String error, String description, ClientResponse.Status status) {
        if (status == ClientResponse.Status.UNAUTHORIZED) {
            return Response.status(ClientResponse.Status.UNAUTHORIZED).header(WWW_AUTHENTICATE, BASIC_REALM).build();
        }
        return Response.status(status).entity(new ErrorResponse(error, description)).build();
    }

    private Response sendErrorResponse(ValidationResponse response) {
        return sendErrorResponse(response.getValue(), response.getDescription(), response.getStatus());
    }

    private Response sendImplicitGrantResponse(AuthorizationRequest authReq, AccessToken accessToken) {
        String uri = authReq.getRedirectUri();
        String fragment = String.format("access_token=%s&token_type=bearer&expires_in=%s&scope=%s"
                + appendStateParameter(authReq), accessToken.getToken(), accessToken.getExpiresIn(), StringUtils.join(authReq.getGrantedScopes(), ','));
        if (authReq.getClient().isIncludePrincipal()) {
            fragment += String.format("&principal=%s", authReq.getPrincipal().getDisplayName());
        }
        return Response
                .seeOther(UriBuilder.fromUri(uri)
                        .fragment(fragment).build())
                .cacheControl(cacheControlNoStore())
                .header("Pragma", "no-cache")
                .build();


    }

    private String appendQueryMark(String uri) {
        return uri.contains("?") ? "&" : "?";
    }

    private String appendStateParameter(AuthorizationRequest authReq) {
        String state = authReq.getState();
        try {
            return StringUtils.isBlank(state) ? "" : "&state=".concat(URLEncoder.encode(state, "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private Response serverError(String msg) {
        LOG.warn(msg);
        return Response.serverError().build();
    }

    /**
     * @param authorizationRequestRepository the authorizationRequestRepository to set
     */
    public void setAuthorizationRequestRepository(AuthorizationRequestRepository authorizationRequestRepository) {
        this.authorizationRequestRepository = authorizationRequestRepository;
    }

    /**
     * @param accessTokenRepository the accessTokenRepository to set
     */
    public void setAccessTokenRepository(AccessTokenRepository accessTokenRepository) {
        this.accessTokenRepository = accessTokenRepository;
    }


}
