/*
 * ProxyAuthorizationCodeFlow.groovy
 *
 * Copyright (c) 2011-2016, Daniel Ellermann
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


package org.amcworld.springcrm.google

import com.google.api.client.auth.oauth2.BearerToken
import com.google.api.client.auth.oauth2.Credential
import com.google.api.client.auth.oauth2.CredentialRefreshListener
import com.google.api.client.auth.oauth2.DataStoreCredentialRefreshListener
import com.google.api.client.auth.oauth2.StoredCredential
import com.google.api.client.auth.oauth2.TokenResponse
import com.google.api.client.auth.oauth2.Credential.AccessMethod
import com.google.api.client.http.BasicAuthentication
import com.google.api.client.http.GenericUrl
import com.google.api.client.http.HttpTransport
import com.google.api.client.json.JsonFactory
import com.google.api.client.util.store.DataStore
import groovy.transform.CompileStatic


/**
 * The class {@code ProxyAuthorizationCodeFlow} encapsulates the code flow by
 * obtaining the access and refresh token for access to Google using the AMC
 * World OAuth2 proxy.
 *
 * @author  Daniel Ellermann
 * @version 2.1
 * @since   1.0
 */
@CompileStatic
class ProxyAuthorizationCodeFlow {

    //-- Instance variables ---------------------

    /**
     * The method to access the authorization data.
     */
    final AccessMethod accessMethod

    /**
     * The underlying data store to load and store credentials.
     */
    final DataStore dataStore

    /**
     * The JSON factory used to create JSON parsers.
     */
    final JsonFactory jsonFactory

    /**
     * The HTTP transport instance.
     */
    final HttpTransport transport


    //-- Constructors ---------------------------

    /**
     * Creates a new instance for the OAuth2 authentication code flow via the
     * AMC World proxy.
     *
     * @param transport     the HTTP transport instance
     * @param jsonFactory   the JSON factory used to create JSON parsers
     */
    ProxyAuthorizationCodeFlow(HttpTransport transport,
                               JsonFactory jsonFactory)
    {
        this.transport = transport
        this.jsonFactory = jsonFactory
        this.accessMethod = BearerToken.authorizationHeaderAccessMethod()
        this.dataStore =
            UserCredentialDataStoreFactory.defaultInstance.createDataStore(
                StoredCredential.DEFAULT_DATA_STORE_ID
            )
    }


    //-- Public methods -------------------------

    /**
     * Creates a credential from the given token response and stores it for the
     * the given user in the underlying store.
     *
     * @param response  the given token request as received from the proxy
     * @param userId    the user name
     * @return          the created credential
     */
    Credential createAndStoreCredential(TokenResponse response, String userId) {
        Credential credential = newCredential(userId)
        credential.setFromTokenResponse response
        dataStore.set userId, new StoredCredential(credential)

        credential
    }

    /**
     * Loads the credential for the given user.
     *
     * @param userId    the given user name
     * @return          the credential; {@code null} if no credential for the
     *                  given user is available
     */
    Credential loadCredential(String userId) {
        StoredCredential storedCredential =
            (StoredCredential) dataStore.get(userId)
        if (!storedCredential) {
            return null
        }

        Credential credential = newCredential(userId)
        credential.accessToken = storedCredential.accessToken
        credential.expirationTimeMilliseconds = storedCredential.expirationTimeMilliseconds
        credential.refreshToken = storedCredential.refreshToken

        credential
    }

    /**
     * Obtains the credential from the proxy and stores it for the given user
     * in the underlying store.
     *
     * @param userId        the user name
     * @param clientId      the client ID generated by the proxy
     * @return              the created credential
     * @throws IOException  if an error occurred while sending the request or
     *                      parsing the response, or if the proxy returned an
     *                      error code
     */
    Credential obtainAndStoreCredential(String userId, String clientId)
        throws IOException
    {
        ProxyRequest req = createProxyRequest(
            'getToken', ['clientId': clientId]
        )
        ProxyResponse response = req.execute()

        createAndStoreCredential response.tokenResponse as TokenResponse, userId
    }

    /**
     * Registers this application as client at the proxy.
     *
     * @param redirectUrl   the URL which is to call when the proxy has
     *                      received the credential
     * @return              a URL to redirect the user to in order to
     *                      authenticate at the server
     * @throws IOException  if an error occurred while sending the request or
     *                      parsing the response, or if the proxy returned an
     *                      error code
     */
    String register(String redirectUrl) throws IOException {
        ProxyRequest req = createProxyRequest(
            'register', ['redirectUrl': redirectUrl]
        )
        ProxyResponse response = req.execute()

        response.get 'url'
    }

    /**
     * Revokes access to the server by sending a revoke request to the proxy
     * and deleting the credential in the underlying store.
     *
     * @param userId        the user name
     * @throws IOException  if an error occurred while sending the request or
     *                      parsing the response, or if the proxy returned an
     *                      error code
     */
    void revoke(String userId) throws IOException {
        Credential credential = loadCredential(userId)
        if (credential != null) {
            ProxyRequest req = createProxyRequest(
                'revoke', ['token': credential.accessToken]
            )
            req.execute()

            dataStore.delete userId
        }
    }


    //-- Non-public methods ---------------------

    /**
     * Creates a proxy request with the given action.
     *
     * @param action    the given action
     * @return          the proxy request instance
     * @since           2.0
     */
    private ProxyRequest createProxyRequest(
        String action, Map<? extends String, ?> params = null
    ) {
        ProxyRequest req = new ProxyRequest(transport, jsonFactory, action)
        if (params != null) {
            req.putAll params
        }

        req
    }

    /**
     * Creates a new credential for the given user name.
     *
     * @param userId    the user name
     * @return          the created credential
     */
    private Credential newCredential(String userId) {
        List<CredentialRefreshListener> listeners =
            new ArrayList<CredentialRefreshListener>(1)
        listeners << new DataStoreCredentialRefreshListener(userId, dataStore)

        /*
         * Implementation notes: the calls setTokenServerUrl() and
         * setClientAuthentication() are needed to build a valid credential
         * even if they are not really used.  The URL used to refresh the token
         * is build in ProxyCredential.executeRefreshToken().
         */
        new ProxyCredential.Builder(accessMethod)
            .setTransport(transport)
            .setJsonFactory(jsonFactory)
            .setTokenServerUrl(new GenericUrl('https://example.com/token'))
            .setClientAuthentication(
                new BasicAuthentication('s6BhdRkqt3', '7Fjfp0ZBr1KtDRbnfVdmIw')
            )
            .setRefreshListeners(listeners)
            .build()
    }
}
