// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.io;

import static org.openstreetmap.josm.tools.I18n.tr;

import java.lang.reflect.InvocationTargetException;
import java.net.Authenticator.RequestorType;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

import javax.swing.SwingUtilities;

import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.oauth.OAuthParameters;
import org.openstreetmap.josm.gui.oauth.OAuthAuthorizationWizard;
import org.openstreetmap.josm.gui.preferences.server.OAuthAccessTokenHolder;
import org.openstreetmap.josm.io.auth.CredentialsAgentException;
import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
import org.openstreetmap.josm.io.auth.CredentialsManager;
import org.openstreetmap.josm.tools.Base64;
import org.openstreetmap.josm.tools.HttpClient;
import org.openstreetmap.josm.tools.Utils;

import oauth.signpost.OAuthConsumer;
import oauth.signpost.exception.OAuthException;

/**
 * Base class that handles common things like authentication for the reader and writer
 * to the osm server.
 *
 * @author imi
 */
public class OsmConnection {
    protected boolean cancel;
    protected HttpClient activeConnection;
    protected OAuthParameters oauthParameters;

    /**
     * Cancels the connection.
     */
    public void cancel() {
        cancel = true;
        synchronized (this) {
            if (activeConnection != null) {
                activeConnection.disconnect();
            }
        }
    }

    /**
     * Adds an authentication header for basic authentication
     *
     * @param con the connection
     * @throws OsmTransferException if something went wrong. Check for nested exceptions
     */
    protected void addBasicAuthorizationHeader(HttpClient con) throws OsmTransferException {
        CredentialsAgentResponse response;
        try {
            synchronized (CredentialsManager.getInstance()) {
                response = CredentialsManager.getInstance().getCredentials(RequestorType.SERVER,
                con.getURL().getHost(), false /* don't know yet whether the credentials will succeed */);
            }
        } catch (CredentialsAgentException e) {
            throw new OsmTransferException(e);
        }
        String token;
        if (response == null) {
            token = ":";
        } else if (response.isCanceled()) {
            cancel = true;
            return;
        } else {
            String username = response.getUsername() == null ? "" : response.getUsername();
            String password = response.getPassword() == null ? "" : String.valueOf(response.getPassword());
            token = username + ':' + password;
            try {
                ByteBuffer bytes = StandardCharsets.UTF_8.newEncoder().encode(CharBuffer.wrap(token));
                con.setHeader("Authorization", "Basic "+Base64.encode(bytes));
            } catch (CharacterCodingException e) {
                throw new OsmTransferException(e);
            }
        }
    }

    /**
     * Signs the connection with an OAuth authentication header
     *
     * @param connection the connection
     *
     * @throws OsmTransferException if there is currently no OAuth Access Token configured
     * @throws OsmTransferException if signing fails
     */
    protected void addOAuthAuthorizationHeader(HttpClient connection) throws OsmTransferException {
        if (oauthParameters == null) {
            oauthParameters = OAuthParameters.createFromPreferences(Main.pref);
        }
        OAuthConsumer consumer = oauthParameters.buildConsumer();
        OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance();
        if (!holder.containsAccessToken()) {
            obtainAccessToken(connection);
        }
        if (!holder.containsAccessToken()) { // check if wizard completed
            throw new MissingOAuthAccessTokenException();
        }
        consumer.setTokenWithSecret(holder.getAccessTokenKey(), holder.getAccessTokenSecret());
        try {
            consumer.sign(connection);
        } catch (OAuthException e) {
            throw new OsmTransferException(tr("Failed to sign a HTTP connection with an OAuth Authentication header"), e);
        }
    }

    /**
     * Obtains an OAuth access token for the connection. Afterwards, the token is accessible via {@link OAuthAccessTokenHolder}.
     * @param connection connection for which the access token should be obtained
     * @throws MissingOAuthAccessTokenException if the process cannot be completec successfully
     */
    protected void obtainAccessToken(final HttpClient connection) throws MissingOAuthAccessTokenException {
        try {
            final URL apiUrl = new URL(OsmApi.getOsmApi().getServerUrl());
            if (!Objects.equals(apiUrl.getHost(), connection.getURL().getHost())) {
                throw new MissingOAuthAccessTokenException();
            }
            final Runnable authTask = new FutureTask<>(new Callable<OAuthAuthorizationWizard>() {
                @Override
                public OAuthAuthorizationWizard call() throws Exception {
                    // Concerning Utils.newDirectExecutor: Main.worker cannot be used since this connection is already
                    // executed via Main.worker. The OAuth connections would block otherwise.
                    final OAuthAuthorizationWizard wizard = new OAuthAuthorizationWizard(
                            Main.parent, apiUrl.toExternalForm(), Utils.newDirectExecutor());
                    wizard.showDialog();
                    OAuthAccessTokenHolder.getInstance().setSaveToPreferences(true);
                    OAuthAccessTokenHolder.getInstance().save(Main.pref, CredentialsManager.getInstance());
                    return wizard;
                }
            });
            // exception handling differs from implementation at GuiHelper.runInEDTAndWait()
            if (SwingUtilities.isEventDispatchThread()) {
                authTask.run();
            } else {
                SwingUtilities.invokeAndWait(authTask);
            }
        } catch (MalformedURLException | InterruptedException | InvocationTargetException e) {
            throw new MissingOAuthAccessTokenException(e);
        }
    }

    protected void addAuth(HttpClient connection) throws OsmTransferException {
        final String authMethod = OsmApi.getAuthMethod();
        if ("basic".equals(authMethod)) {
            addBasicAuthorizationHeader(connection);
        } else if ("oauth".equals(authMethod)) {
            addOAuthAuthorizationHeader(connection);
        } else {
            String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
            Main.warn(msg);
            throw new OsmTransferException(msg);
        }
    }

    /**
     * Replies true if this connection is canceled
     *
     * @return true if this connection is canceled
     */
    public boolean isCanceled() {
        return cancel;
    }
}
