package teammates.e2e.util;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.List;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.FileDataStoreFactory;
import com.google.api.services.gmail.Gmail;
import com.google.api.services.gmail.GmailScopes;

/**
 * Class that builds a Gmail service for use in Gmail API.
 */
final class GmailServiceMaker {

    /** Global instance of the JSON factory. */
    private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();

    /** Global instance of the HTTP transport. */
    private static final HttpTransport HTTP_TRANSPORT;

    static {
        try {
            HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
        } catch (GeneralSecurityException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    private final String username;
    private final boolean shouldUseFreshCredentials;

    GmailServiceMaker(String username) {
        this(username, false);
    }

    GmailServiceMaker(String username, boolean shouldUseFreshCredentials) {
        this.username = username;
        this.shouldUseFreshCredentials = shouldUseFreshCredentials;
    }

    /**
     * Builds and returns an authorized Gmail client service.
     */
    Gmail makeGmailService() throws IOException {
        Credential credential = authorizeAndCreateCredentials();
        return new Gmail.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential)
                .setApplicationName("teammates")
                .build();
    }

    /**
     * Authorizes the user and creates an authorized Credential.
     * @return an authorized Credential
     */
    private Credential authorizeAndCreateCredentials() throws IOException {
        GoogleClientSecrets clientSecrets = loadClientSecretFromJson();

        GoogleAuthorizationCodeFlow flow = buildFlow(clientSecrets);

        if (shouldUseFreshCredentials) {
            flow.getCredentialDataStore().delete(username);
        }

        if (flow.getCredentialDataStore().get(username) == null) {
            System.out.println("Please login as: " + username);
        }

        return getCredentialFromFlow(flow);
    }

    private GoogleClientSecrets loadClientSecretFromJson() throws IOException {
        try (InputStream in = Files.newInputStream(Paths.get(TestProperties.TEST_GMAIL_API_FOLDER, "client_secret.json"))) {
            return GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in));
        } catch (FileNotFoundException e) {
            throw new RuntimeException("You need to set up your Gmail API credentials." + System.lineSeparator()
                    + "See docs/development.md section \"Deploying to a staging server\".", e);
        }
    }

    private GoogleAuthorizationCodeFlow buildFlow(GoogleClientSecrets clientSecrets) throws IOException {
        // if the scopes need to change, the user will need to manually delete
        // <TestProperties.TEST_GMAIL_API_FOLDER>/StoredCredential
        List<String> scopes = Arrays.asList(GmailScopes.GMAIL_READONLY, GmailScopes.GMAIL_MODIFY);
        FileDataStoreFactory dataStoreFactory = new FileDataStoreFactory(new File(TestProperties.TEST_GMAIL_API_FOLDER));
        return new GoogleAuthorizationCodeFlow.Builder(
                HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, scopes)
                .setDataStoreFactory(dataStoreFactory)
                .setAccessType("offline")
                .build();
    }

    /**
     * Gets the credential containing the access token from the flow if it exists. Otherwise a local server receiver is used
     * to receive authorization code and then exchanges the code for an access token.
     */
    private Credential getCredentialFromFlow(GoogleAuthorizationCodeFlow flow) throws IOException {
        return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize(username);
    }
}