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

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

import java.net.URI;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import org.openstreetmap.josm.io.auth.CredentialsAgent;
import org.openstreetmap.josm.io.auth.CredentialsAgentException;
import org.openstreetmap.josm.io.auth.CredentialsManager;
import org.openstreetmap.josm.spi.preferences.Config;
import org.openstreetmap.josm.tools.CheckParameterUtil;
import org.openstreetmap.josm.tools.Logging;

/**
 * Class holding OAuth access token key and secret.
 * @since 12686 (moved from {@code gui.preferences.server} package)
 */
public class OAuthAccessTokenHolder {
    private static OAuthAccessTokenHolder instance;

    /**
     * Replies the unique instance.
     * @return The unique instance of {@code OAuthAccessTokenHolder}
     */
    public static synchronized OAuthAccessTokenHolder getInstance() {
        if (instance == null) {
            instance = new OAuthAccessTokenHolder();
        }
        return instance;
    }

    private boolean saveToPreferences;

    private final Map<String, Map<OAuthVersion, IOAuthToken>> tokenMap = new HashMap<>();

    /**
     * Replies true if current access token should be saved to the preferences file.
     *
     * @return true if current access token should be saved to the preferences file.
     */
    public boolean isSaveToPreferences() {
        return saveToPreferences;
    }

    /**
     * Sets whether the current access token should be saved to the preferences file.
     * <p>
     * If true, the access token is saved in clear text to the preferences file. The same
     * access token can therefore be used in multiple JOSM sessions.
     * <p>
     * If false, the access token isn't saved to the preferences file. If JOSM is closed,
     * the access token is lost and new token has to be generated by the OSM server the
     * next time JOSM is used.
     *
     * @param saveToPreferences {@code true} to save to preferences file
     */
    public void setSaveToPreferences(boolean saveToPreferences) {
        this.saveToPreferences = saveToPreferences;
    }

    /**
     * Replies the access token.
     * @param api The api the token is for
     * @param version The OAuth version the token is for
     * @return the access token, can be {@code null}
     * @since 18650
     */
    public IOAuthToken getAccessToken(String api, OAuthVersion version) {
        // Sometimes the api might be sent as the host
        api = Optional.ofNullable(URI.create(api).getHost()).orElse(api);
        if (this.tokenMap.containsKey(api) && this.tokenMap.get(api).containsKey(version)) {
            Map<OAuthVersion, IOAuthToken> map = this.tokenMap.get(api);
            return map.get(version);
        }
        try {
            IOAuthToken token = CredentialsManager.getInstance().lookupOAuthAccessToken(api);
            // We *do* want to set the API token to null, if it doesn't exist. Just to avoid unnecessary lookups.
            if (token == null || token.getOAuthType() == version) {
                this.setAccessToken(api, token);
                return token;
            }
        } catch (CredentialsAgentException exception) {
            Logging.trace(exception);
        }
        return null;
    }

    /**
     * Sets the access token hold by this holder.
     *
     * @param api The api the token is for
     * @param token the access token. Can be null to clear the content in this holder.
     * @since 18650
     */
    public void setAccessToken(String api, IOAuthToken token) {
        Objects.requireNonNull(api, "api url");
        // Sometimes the api might be sent as the host
        api = Optional.ofNullable(URI.create(api).getHost()).orElse(api);
        if (token == null) {
            if (this.tokenMap.containsKey(api)) {
                this.tokenMap.get(api).clear();
            }
        } else {
            this.tokenMap.computeIfAbsent(api, key -> new EnumMap<>(OAuthVersion.class)).put(token.getOAuthType(), token);
        }
    }

    /**
     * Initializes the content of this holder from the Access Token managed by the
     * credential manager.
     *
     * @param cm the credential manager. Must not be null.
     * @throws IllegalArgumentException if cm is null
     */
    public void init(CredentialsAgent cm) {
        CheckParameterUtil.ensureParameterNotNull(cm, "cm");
        saveToPreferences = Config.getPref().getBoolean("oauth.access-token.save-to-preferences", true);
    }

    /**
     * Saves the content of this holder to the preferences and a credential store managed
     * by a credential manager.
     *
     * @param cm the credentials manager. Must not be null.
     * @throws IllegalArgumentException if cm is null
     */
    public void save(CredentialsAgent cm) {
        CheckParameterUtil.ensureParameterNotNull(cm, "cm");
        Config.getPref().putBoolean("oauth.access-token.save-to-preferences", saveToPreferences);
        try {
            if (!saveToPreferences) {
                for (String host : this.tokenMap.keySet()) {
                    cm.storeOAuthAccessToken(host, null);
                }
            } else {
                for (Map.Entry<String, Map<OAuthVersion, IOAuthToken>> entry : this.tokenMap.entrySet()) {
                    if (entry.getValue().isEmpty()) {
                        cm.storeOAuthAccessToken(entry.getKey(), null);
                        continue;
                    }
                    for (OAuthVersion version : OAuthVersion.values()) {
                        if (entry.getValue().containsKey(version)) {
                            cm.storeOAuthAccessToken(entry.getKey(), entry.getValue().get(version));
                        }
                    }
                }
            }
        } catch (CredentialsAgentException e) {
            Logging.error(e);
            Logging.warn(tr("Failed to store OAuth Access Token to credentials manager"));
            Logging.warn(tr("Current credential manager is of type ''{0}''", cm.getClass().getName()));
        }
    }

    /**
     * Clears the content of this holder
     */
    public void clear() {
        this.tokenMap.clear();
    }
}
