/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2007-2015 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.broad.igv.google;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import org.apache.log4j.Logger;
import org.broad.igv.DirectoryManager;
import org.broad.igv.batch.CommandListener;
import org.broad.igv.ui.util.MessageUtils;
import org.broad.igv.util.FileUtils;
import org.broad.igv.util.HttpUtils;

import java.awt.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.prefs.Preferences;

/**
 * Created by jrobinso on 11/19/14.
 */
public class OAuthUtils {

    private static Logger log = Logger.getLogger(OAuthUtils.class);

    public String authProvider = "Google";
    private String appIdURI = null;
    public static String findString = null;
    public static String replaceString = null;


    private static final String REFRESH_TOKEN_KEY = "oauth_refresh_token";
    private static final String PROPERTIES_URL = "https://s3.amazonaws.com/igv.org.app/desktop_google";
    private String gsScope = "https://www.googleapis.com/auth/devstorage.read_only";
    private String driveScope = "https://www.googleapis.com/auth/drive.readonly";
    private String emailScope = "https://www.googleapis.com/auth/userinfo.email";
    private String state = "%2Fprofile";
    private String redirectURI = "http%3A%2F%2Flocalhost%3A60151%2FoauthCallback";
    private String oobURI = "urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob";
    private String clientId;
    private String clientSecret;
    private String authURI;
    private String tokenURI;

    private String authorizationCode;
    private String accessToken;
    private String refreshToken;
    private long expirationTime;

    private static OAuthUtils theInstance;
    private String currentUserName;

    // by default this is the google scope
    private String scope = driveScope + "%20" + gsScope + "%20" + emailScope;

    public static synchronized OAuthUtils getInstance() throws IOException {

        if (theInstance == null) {
            theInstance = new OAuthUtils();
        }

        return theInstance;
    }

    private OAuthUtils() throws IOException {
        restoreRefreshToken();
        fetchOauthProperties();
    }

    public void fetchOauthProperties() throws IOException {

        String oauthConfig = DirectoryManager.getIgvDirectory() + "/oauth-config.json";
        //IGVPreferences.getInstance().get(IGVPreferences.OAUTH_CONFIG);

        if (oauthConfig == null || !FileUtils.resourceExists(oauthConfig)) {
            String propString = HttpUtils.getInstance().getContentsAsGzippedString(HttpUtils.createURL(PROPERTIES_URL));
            JsonParser parser = new JsonParser();
            JsonObject obj = parser.parse(propString).getAsJsonObject().get("installed").getAsJsonObject();
            authURI = obj.get("auth_uri").getAsString();
            clientSecret = obj.get("client_secret").getAsString();
            tokenURI = obj.get("token_uri").getAsString();
            clientId = obj.get("client_id").getAsString();
        } else {
            // Custom oauth parameters.    -- dwm08
            JsonParser parser = new JsonParser();
            String json = FileUtils.getContents(oauthConfig);
            JsonObject obj = parser.parse(json).getAsJsonObject();
            authURI = obj.get("authorization_endpoint").getAsString();
            clientSecret = obj.get("client_secret").getAsString();
            tokenURI = obj.get("token_endpoint").getAsString();
            clientId = obj.get("client_id").getAsString();
            GoogleUtils.GS_HOST = obj.get("hosts").getAsString();
            appIdURI = obj.get("app_id_uri").getAsString();
            authProvider = obj.get("auth_provider").getAsString();
            String scope = obj.get("scope").getAsString();
            if (scope.equals("none")) {
                this.scope = null;
            }
            JsonElement je = obj.get("find_string");
            if (je != null) {
                findString = je.getAsString();
            }
            je = obj.get("replace_string");
            if (je != null) {
                replaceString = je.getAsString();
            }

        }
    }

    /**
     * Send request to authorization provider to start the oauth 2.0
     * authorization process. If the listener is up, wait for a callback
     * Otherwise, provide a dialog where user can provide authentication token.
     * This method has been generalized to use any auth provider (originally google only)
     * dwm08
     *
     * @throws IOException
     * @throws URISyntaxException
     */
    public void openAuthorizationPage() throws IOException, URISyntaxException {
        Desktop desktop = Desktop.getDesktop();

        // properties moved to early init dwm08
        //if (clientId == null) fetchOauthProperties();

        String redirect = oobURI;
        // if the listener is active, then set the redirect URI.  dwm08
        if (CommandListener.isListening()) {
            redirect = redirectURI;
        }
        String url;
        // for auth providers that need scope,
        // set the scope parameter
        //if (scope != null) {
        if (appIdURI == null) {
            url = authURI + "?" +
                    "scope=" + scope + "&" +
                    "state=" + state + "&" +
                    "redirect_uri=" + redirect + "&" +
                    "response_type=code&" +
                    "client_id=" + clientId; // Native app
        }
        // for auth providers that need the resource setting
        // the the resource paremeter
        //else if (appIdURI != null) {
        else {

            url = authURI + "?" +
                    "scope=" + scope + "&" +
                    "state=" + state + "&" +
                    "redirect_uri=" + redirect + "&" +
                    "response_type=code&" +
                    "resource=" + appIdURI + "&" +
                    "client_id=" + clientId; // Native app
        }
//        else {
//        	throw new IOException("Either scope or resource must be provided to authenticate.");
//        }

        // check if the "browse" Desktop action is suppported (many Linux DEs cannot directly
        // launch browsers!)
        if (desktop.isSupported(Desktop.Action.BROWSE)) {
            desktop.browse(new URI(url));
        } else { // otherwise, display a dialog box for the user to copy the URL manually.
            MessageUtils.showMessage("Copy this authorization URL into your web browser: " + url);
        }

        // if the listener is not active, prompt the user
        // for the access token
        if (!CommandListener.isListening()) {
            String ac = MessageUtils.showInputDialog("Please paste authorization code here:");
            if (ac != null) {
                setAuthorizationCode(ac, oobURI);
            }
        }

    }

    // Called from port listener upon receiving the oauth request with a "code" parameter
    public void setAuthorizationCode(String ac) throws IOException {
        setAuthorizationCode(ac, redirectURI);
    }

    public void setAuthorizationCode(String ac, String redirect) throws IOException {
        authorizationCode = ac;
        fetchTokens(redirect);
        fetchUserProfile();
    }

    // Called from port listener upon receiving the oauth request with a "token" parameter TODO -- does this ever happen?
    public void setAccessToken(String accessToken) throws IOException {
        this.accessToken = accessToken;
        fetchUserProfile();
    }

    private void fetchTokens(String redirect) throws IOException {

        // properties moved to early init dwm08
        //if (clientId == null) fetchOauthProperties();

        URL url = HttpUtils.createURL(tokenURI);

        Map<String, String> params = new HashMap<String, String>();
        params.put("code", authorizationCode);
        params.put("client_id", clientId);
        params.put("client_secret", clientSecret);
        params.put("redirect_uri", redirect);
        params.put("grant_type", "authorization_code");

        // set the resource if it necessary for the auth provider dwm08
        if (appIdURI != null) {
            params.put("resource", appIdURI);
        }

        String response = HttpUtils.getInstance().doPost(url, params);
        JsonParser parser = new JsonParser();
        JsonObject obj = parser.parse(response).getAsJsonObject();

        accessToken = obj.getAsJsonPrimitive("access_token").getAsString();
        refreshToken = obj.getAsJsonPrimitive("refresh_token").getAsString();
        expirationTime = System.currentTimeMillis() + (obj.getAsJsonPrimitive("expires_in").getAsInt() * 1000);

        // Try to store in java.util.prefs
        saveRefreshToken();
    }

    /**
     * Fetch a new access token from a refresh token.
     *
     * @throws IOException
     */
    private void refreshAccessToken() throws IOException {

        // properties moved to early init dwm08
        //if (clientId == null) fetchOauthProperties();

        URL url = HttpUtils.createURL(tokenURI);

        Map<String, String> params = new HashMap<String, String>();
        params.put("refresh_token", refreshToken);
        params.put("client_id", clientId);
        params.put("client_secret", clientSecret);
        params.put("grant_type", "refresh_token");

        // set the resource if it necessary for the auth provider dwm08
        if (appIdURI != null) {
            params.put("resource", appIdURI);
        }

        String response = HttpUtils.getInstance().doPost(url, params);
        JsonParser parser = new JsonParser();
        JsonObject obj = parser.parse(response).getAsJsonObject();

        JsonPrimitive atprim = obj.getAsJsonPrimitive("access_token");
        if (atprim != null) {
            accessToken = obj.getAsJsonPrimitive("access_token").getAsString();
            expirationTime = System.currentTimeMillis() + (obj.getAsJsonPrimitive("expires_in").getAsInt() * 1000);
            fetchUserProfile();
        } else {
            // Refresh token has failed, reauthorize from scratch
            reauthorize();
        }

    }

    private void reauthorize() throws IOException {
        logout();
        try {
            openAuthorizationPage();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    /**
     * Check if the username is in the claim information. If so, extract it.
     * Otherwise call out to the server to get the current user name.
     * dwm08
     *
     * @throws IOException
     */
    public JsonObject fetchUserProfile() throws IOException {

        try {

            URL url = new URL("https://www.googleapis.com/oauth2/v1/userinfo?access_token=" + accessToken);
            String response = HttpUtils.getInstance().getContentsAsJSON(url);
            JsonParser parser = new JsonParser();
            JsonObject obj = parser.parse(response).getAsJsonObject();

            currentUserName = obj.get("name").getAsString();
            //currentUserEmail = obj.get("email").getAsString();
            //currentUserID = obj.get("id").getAsString();

            return obj;
        } catch (Throwable exception) {
            log.error(exception);
            return null;
        }
    }

    public String getAccessToken() {

        // Check expiration time, with 1 minute cushion
        if (accessToken == null || (System.currentTimeMillis() > (expirationTime - 60 * 1000))) {
            if (refreshToken != null) {
                try {
                    this.refreshAccessToken();
                } catch (IOException e) {
                    log.error("Error fetching access token", e);
                }
            }
        }

        return accessToken;
    }

    public boolean isLoggedIn() {
        return getAccessToken() != null;
    }

    public String getCurrentUserName() {
        return currentUserName;
    }

    public void logout() {
        accessToken = null;
        refreshToken = null;
        expirationTime = -1;
        currentUserName = null;
        removeRefreshToken();
    }

    private void saveRefreshToken() {
        try {
            Preferences.userRoot().put(REFRESH_TOKEN_KEY, refreshToken);
        } catch (Exception e) {
            log.error("Error storing refresh token", e);
        }
    }

    private void restoreRefreshToken() {
        try {
            refreshToken = Preferences.userRoot().get(REFRESH_TOKEN_KEY, null);
        } catch (Exception e) {
            log.error("Error fetching oauth refresh token", e);
        }
    }

    private void removeRefreshToken() {
        try {
            Preferences.userRoot().remove(REFRESH_TOKEN_KEY);
        } catch (Exception e) {
            log.error("Error removing oauth refresh token", e);
        }
    }

    public void updateSaveOption(boolean aBoolean) {
        if (aBoolean) {
            if (refreshToken != null) {
                saveRefreshToken();
            }
        } else {
            removeRefreshToken();

        }
    }

    /**
     * Try to login to secure server. dwm08
     */
    public void doSecureLogin() {
        // if user is not currently logged in, attempt to
        // log in user if not logged in dwm08
        if (!isLoggedIn()) {
            try {
                OAuthUtils.getInstance().openAuthorizationPage();
            } catch (Exception ex) {
                MessageUtils.showErrorMessage("Error fetching oAuth tokens.  See log for details", ex);
                log.error("Error fetching oAuth tokens", ex);
            }

        }
        // wait until authentication successful or 1 minute -
        // dwm08
        int i = 0;
        while (!isLoggedIn() && i < 600) {
            ++i;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        }
    }

    /**
     * Generate a set of all urls in the sessino file
     *
     * @param sessionPath
     * @return list of urls
     */
    public static Set<String> findUrlsInSessionFile(String sessionPath) {
        BufferedReader br = null;
        HashSet<String> urlSet = new HashSet<>();
        try {
            br = new BufferedReader(new FileReader(new File(sessionPath)));
            String line;
            while ((line = br.readLine()) != null) {
                int start = line.indexOf("http");
                if (start != -1) {
                    int mid = line.indexOf("://", start);
                    int end = line.indexOf("/", mid + 3);
                    String url = line.substring(start, end);
                    urlSet.add(url);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                }
            }
        }
        return urlSet;
    }

    /**
     * Check if any reference in the session file refers to a server protected
     * by the oauth protocol. If so, check to see if the user is logged in. If
     * user is not logged in, put up login prompt.
     *
     * @param sessionPath
     */
    public void checkServerLogin(String sessionPath) {
        Set<String> urlSet = findUrlsInSessionFile(sessionPath);
        if (urlSet.size() > 0) {
            for (String url : urlSet) {
                if (GoogleUtils.isGoogleCloud(url)) {

                    doSecureLogin();

                    // user is logged in. Can proceed with the load
                    return;
                }
            }
        }
    }
}
