package org.github.flytreeleft.nexus3.keycloak.plugin.internal;

import org.github.flytreeleft.nexus3.keycloak.plugin.internal.http.Http;
import org.github.flytreeleft.nexus3.keycloak.plugin.internal.http.HttpMethod;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.util.Time;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.adapters.config.AdapterConfig;

public class KeycloakTokenManager {
    private static final long DEFAULT_MIN_VALIDITY = 30;

    private AccessTokenResponse currentToken;
    private long expirationTime;
    private long minTokenValidity = DEFAULT_MIN_VALIDITY;
    private final AdapterConfig config;
    private final Http http;
    private final String grantType;

    public KeycloakTokenManager(AdapterConfig config, Http http) {
        this.config = config;
        this.http = http;
        this.grantType = OAuth2Constants.CLIENT_CREDENTIALS;

        if (config.isPublicClient()) {
            throw new IllegalArgumentException("Can't use " +
                                               OAuth2Constants.GRANT_TYPE +
                                               "=" +
                                               OAuth2Constants.CLIENT_CREDENTIALS +
                                               " with public client");
        }
    }

    public String getAccessTokenString() {
        return getAccessToken().getToken();
    }

    public synchronized AccessTokenResponse getAccessToken() {
        if (this.currentToken == null) {
            grantToken();
        } else if (tokenExpired()) {
            refreshToken();
        }
        return this.currentToken;
    }

    public AccessTokenResponse grantToken() {
        HttpMethod<AccessTokenResponse> httpMethod = tokenRequest(this.grantType);

        return updateToken(httpMethod);
    }

    public AccessTokenResponse refreshToken() {
        HttpMethod<AccessTokenResponse> httpMethod = tokenRequest(OAuth2Constants.REFRESH_TOKEN);
        httpMethod.param(OAuth2Constants.REFRESH_TOKEN, currentToken.getRefreshToken());

        try {
            return updateToken(httpMethod);
        } catch (Exception e) {
            return grantToken();
        }
    }

    private AccessTokenResponse updateToken(HttpMethod<AccessTokenResponse> httpMethod) {
        int requestTime = Time.currentTime();

        synchronized (this) {
            this.currentToken = httpMethod.response().json(AccessTokenResponse.class).execute();
            this.expirationTime = requestTime + this.currentToken.getExpiresIn();
        }
        return this.currentToken;
    }

    private HttpMethod<AccessTokenResponse> tokenRequest(String grantType) {
        String path = "/realms/%s/protocol/openid-connect/token";
        HttpMethod<AccessTokenResponse> httpMethod = this.http.post(path, this.config.getRealm());
        httpMethod.param(OAuth2Constants.GRANT_TYPE, grantType);

        if (this.config.isPublicClient()) {
            httpMethod.param(OAuth2Constants.CLIENT_ID, this.config.getResource());
        } else {
            httpMethod.authorizationBasic(this.config.getResource(),
                                          this.config.getCredentials().get("secret").toString());
        }

        return httpMethod;
    }

    public synchronized void setMinTokenValidity(long minTokenValidity) {
        this.minTokenValidity = minTokenValidity;
    }

    private synchronized boolean tokenExpired() {
        return (Time.currentTime() + this.minTokenValidity) >= this.expirationTime;
    }

    /**
     * Invalidates the current token, but only when it is equal to the token passed as an argument.
     *
     * @param token
     *         the token to invalidate (cannot be null).
     */
    public void invalidate(String token) {
        if (this.currentToken == null) {
            return; // There's nothing to invalidate.
        }
        if (token.equals(this.currentToken.getToken())) {
            // When used next, this cause a refresh attempt, that in turn will cause a grant attempt if refreshing fails.
            this.expirationTime = -1;
        }
    }
}