package com.satalyst.powerbi.impl;

import com.microsoft.aad.adal4j.AuthenticationContext;
import com.satalyst.powerbi.AuthenticationFailureException;
import com.satalyst.powerbi.Authenticator;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static com.google.common.base.Preconditions.checkNotNull;


/**
 * https://msdn.microsoft.com/en-US/library/dn877545.aspx
 *
 * @author Aidan Morgan
 */
public class Office365Authenticator implements Authenticator {
    private static final String DEFAULT_AUTHORITY = "https://login.windows.net/common/oauth2/authorize";
    private static final String DEFAULT_POWER_BI_RESOURCE_ID = "https://analysis.windows.net/powerbi/api";
    private static final boolean DEFAULT_VALIDATE_AUTHORITY = false;

    private String authority = DEFAULT_AUTHORITY;
    private String powerBiResourceId = DEFAULT_POWER_BI_RESOURCE_ID;
    private boolean validateAuthority = DEFAULT_VALIDATE_AUTHORITY;

    private String nativeClientId;
    private String tenant;
    private String username;
    private String password;

    private ExecutorService executor;


    public Office365Authenticator(String nativeClientId, String tenant, String username, String password) {
        this(nativeClientId, tenant, username, password, Executors.newSingleThreadExecutor());
    }

    public Office365Authenticator(String nativeClientId, String tenant, String username, String password, ExecutorService executor) {
        this.nativeClientId = nativeClientId;
        this.tenant = tenant;
        this.username = username;
        this.password = password;
        this.executor = executor;
    }

    public Office365Authenticator setAuthority(String authority) {
        this.authority = checkNotNull(authority);
        return this;
    }

    public Office365Authenticator setPowerBiResourceId(String powerBiResourceId) {
        this.powerBiResourceId = checkNotNull(powerBiResourceId);
        return this;
    }

    public Office365Authenticator setValidateAuthority(boolean validateAuthority) {
        this.validateAuthority = validateAuthority;
        return this;
    }

    private ReadWriteLock tokenLock = new ReentrantReadWriteLock();
    private String cachedToken;

    /**
     * Performs the authentication.
     *
     * Thread-safe implementation that will cache the bearer token for multiple calls to ensure that we don't make
     * repeated, unnecessary calls to the authentication service.
     *
     * @return the bearer token to use for authenticating service requests.
     * @throws AuthenticationFailureException
     */
    public String authenticate() throws AuthenticationFailureException {
        try {
            tokenLock.readLock().lock();

            if (cachedToken == null) {
                try {
                    // release the read lock and acquire the write lock to call the implementation
                    tokenLock.readLock().unlock();
                    tokenLock.writeLock().lock();

                    // check again, it may have been set in the time it took us to acquire the write lock
                    if (cachedToken == null) {
                        cachedToken = _authenticate();
                    }

                    // Downgrade by acquiring read lock before releasing write lock
                    tokenLock.readLock().lock();
                } finally {
                    tokenLock.writeLock().unlock();
                }
            }
        } finally {
            // TODO: in theory, if there has been an exception in the authenticate method then this unlock method
            // TODO: should fail as the downgrade of the lock was never performed. Haven't seen this issue in practice yet
            // TODO: however it looks theoretically possible.
            try {
                tokenLock.readLock().unlock();
            } catch (IllegalMonitorStateException e) {
                // ignore - see TODO above for reasoning....
            }
        }


        return cachedToken;
    }

    @Override
    public void reset() {
        try {
            tokenLock.writeLock().lock();
            cachedToken = null;
        }
        finally {
            tokenLock.writeLock().unlock();
        }
    }

    private String _authenticate() throws AuthenticationFailureException {
        try {
            AuthenticationContext authenticationContext = new AuthenticationContext(
                    authority,
                    validateAuthority,
                    executor
            );

            String result = getAccessToken(
                    authenticationContext,
                    powerBiResourceId,
                    nativeClientId,
                    username + "@" + tenant,
                    password
            );

            if (StringUtils.isEmpty(result)) {
                throw new AuthenticationFailureException("Returned access token is null.");
            }

            return result;
        } catch (ExecutionException | InterruptedException | IOException e) {
            throw new AuthenticationFailureException(e);
        }
    }

    private String getAccessToken(AuthenticationContext authenticationContext, String resourceId, String clientId,
                                  String username, String password) throws ExecutionException, InterruptedException {
        return authenticationContext.acquireToken(
                resourceId,
                clientId,
                username,
                password,
                null
        ).get().getAccessToken();
    }
}