/*
 * Copyright (c) 2018, salesforce.com, inc.
 * All rights reserved.
 * Licensed under the BSD 3-Clause license.
 * For full license text, see LICENSE.txt file in the repo root or
 * https://opensource.org/licenses/BSD-3-Clause
 */

package com.salesforce.dockerfileimageupdate.subcommands.impl;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.salesforce.dockerfileimageupdate.SubCommand;
import com.salesforce.dockerfileimageupdate.repository.GitHub;
import com.salesforce.dockerfileimageupdate.subcommands.ExecutableWithNamespace;
import com.salesforce.dockerfileimageupdate.utils.Constants;
import com.salesforce.dockerfileimageupdate.utils.DockerfileGitHubUtil;
import com.salesforce.dockerfileimageupdate.utils.ResultsProcessor;
import net.sourceforge.argparse4j.inf.Namespace;
import org.kohsuke.github.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Paths;
import java.util.*;

@SubCommand(help="updates all repositories' Dockerfiles",
        requiredParams = {Constants.STORE})
public class All implements ExecutableWithNamespace {
    private static final Logger log = LoggerFactory.getLogger(All.class);

    private DockerfileGitHubUtil dockerfileGitHubUtil;

    @Override
    public void execute(final Namespace ns, final DockerfileGitHubUtil dockerfileGitHubUtil) throws IOException, InterruptedException {
        loadDockerfileGithubUtil(dockerfileGitHubUtil);

        Map<String, String> imageToTagMap = new HashMap<>();
        Multimap<String, String> imagesFoundInParentRepo = ArrayListMultimap.create();
        Multimap<String, String> pathToDockerfilesInParentRepo = ArrayListMultimap.create();

        Set<Map.Entry<String, JsonElement>> imageToTagStore = parseStoreToImagesMap(ns.get(Constants.STORE));
        for (Map.Entry<String, JsonElement> imageToTag : imageToTagStore) {
            String image = imageToTag.getKey();
            log.info("Repositories with image {} being forked.", image);
            imageToTagMap.put(image, imageToTag.getValue().getAsString());
            PagedSearchIterable<GHContent> contentsWithImage =
                    this.dockerfileGitHubUtil.findFilesWithImage(image, ns.get(Constants.GIT_ORG));
            forkRepositoriesFound(pathToDockerfilesInParentRepo,
                    imagesFoundInParentRepo, contentsWithImage, image);
        }

        GHMyself currentUser = this.dockerfileGitHubUtil.getMyself();
        if (currentUser == null) {
            throw new IOException("Could not retrieve authenticated user.");
        }

        log.info("Retrieving all the forks...");
        List<GHRepository> listOfCurrUserRepos =
                dockerfileGitHubUtil.getGHRepositories(pathToDockerfilesInParentRepo, currentUser);

        List<IOException> exceptions = new ArrayList<>();
        List<String> skippedRepos = new ArrayList<>();

        for (GHRepository currUserRepo : listOfCurrUserRepos) {
            try {
                changeDockerfiles(ns, pathToDockerfilesInParentRepo, imagesFoundInParentRepo, imageToTagMap, currUserRepo,
                        skippedRepos);
            } catch (IOException e) {
                log.error(String.format("Error changing Dockerfile for %s", currUserRepo.getName()), e);
                exceptions.add(e);
            }
        }

        ResultsProcessor.processResults(skippedRepos, exceptions, log);
    }

    protected void loadDockerfileGithubUtil(DockerfileGitHubUtil _dockerfileGitHubUtil) {
        dockerfileGitHubUtil = _dockerfileGitHubUtil;
    }

    protected void forkRepositoriesFound(Multimap<String, String> pathToDockerfilesInParentRepo,
                                         Multimap<String, String> imagesFoundInParentRepo,
                                         PagedSearchIterable<GHContent> contentsWithImage,
                                         String image) throws IOException {
        log.info("Forking {} repositories...", contentsWithImage.getTotalCount());
        List<String> parentReposForked = new ArrayList<>();
        GHRepository parent;
        String parentRepoName = null;
        for (GHContent c : contentsWithImage) {
            /* Kohsuke's GitHub API library, when retrieving the forked repository, looks at the name of the parent to
             * retrieve. The issue with that is: GitHub, when forking two or more repositories with the same name,
             * automatically fixes the names to be unique (by appending "-#" to the end). Because of this edge case, we
             * cannot save the forks and iterate over the repositories; else, we end up missing/not updating the
             * repositories that were automatically fixed by GitHub. Instead, we save the names of the parent repos
             * in the map above, find the list of repositories under the authorized user, and iterate through that list.
             */
            parent = c.getOwner();
            parentRepoName = parent.getFullName();
            if (parent.isFork()) {
                log.warn("Skipping {} because it's a fork already. Sending a PR to a fork is unsupported at the moment.",
                        parentRepoName);
            } else {
                // fork the parent if not already forked
                if (!parentReposForked.contains(parentRepoName)) {
                    // TODO: Need to close PR!
                    GHRepository fork = dockerfileGitHubUtil.getOrCreateFork(parent);
                    GHPullRequest pr = getPullRequestWithPullReqIdentifier(parent);
                    // Only reason we close the existing PR, delete fork and re-fork, is because there is no way to
                    // determine if the existing fork is compatible with it's parent.
                    if (pr != null) {
                        // close the pull-request since the fork is out of date
                        log.info("closing existing pr: {}", pr.getUrl());
                        try {
                            pr.close();
                        } catch (IOException e) {
                            log.info("Issues closing the pull request '{}'. Moving ahead...", pr.getUrl());
                        }
                    }

                    if (fork == null) {
                        log.info("Could not fork {}", parentRepoName);
                    } else {
                        // Add repos to pathToDockerfilesInParentRepo and imagesFoundInParentRepo only if we forked it successfully.
                        pathToDockerfilesInParentRepo.put(parentRepoName, c.getPath());
                        imagesFoundInParentRepo.put(parentRepoName, image);
                        parentReposForked.add(parentRepoName);
                    }
                }
            }
        }

        log.info("Path to Dockerfiles in repo '{}': {}", parentRepoName, pathToDockerfilesInParentRepo);
        log.info("All images found in repo '{}': {}", parentRepoName, imagesFoundInParentRepo);
    }

    protected Set<Map.Entry<String, JsonElement>> parseStoreToImagesMap(String storeName)
            throws IOException, InterruptedException {
        GHMyself myself = dockerfileGitHubUtil.getMyself();
        String login = myself.getLogin();
        GHRepository store = dockerfileGitHubUtil.getRepo(Paths.get(login, storeName).toString());

        GHContent storeContent = dockerfileGitHubUtil.tryRetrievingContent(store, Constants.STORE_JSON_FILE,
                store.getDefaultBranch());

        if (storeContent == null) {
            return Collections.emptySet();
        }

        JsonElement json;
        try (InputStream stream = storeContent.read(); InputStreamReader streamR = new InputStreamReader(stream)) {
            try {
                json = JsonParser.parseReader(streamR);
            } catch (JsonParseException e) {
                log.warn("Not a JSON format store.");
                return Collections.emptySet();
            }
        }

        JsonElement imagesJson = json.getAsJsonObject().get("images");
        return imagesJson.getAsJsonObject().entrySet();
    }

    protected void changeDockerfiles(Namespace ns,
                                     Multimap<String, String> pathToDockerfilesInParentRepo,
                                     Multimap<String, String> imagesFoundInParentRepo,
                                     Map<String, String> imageToTagMap,
                                     GHRepository currUserRepo,
                                     List<String> skippedRepos) throws IOException, InterruptedException {
        /* The Github API does not provide the parent if retrieved through a list. If we want to access its parent,
         * we need to retrieve it once again.
         */
        GHRepository forkedRepo;
        if (currUserRepo.isFork()) {
            try {
                forkedRepo = dockerfileGitHubUtil.getRepo(currUserRepo.getFullName());
            } catch (FileNotFoundException e) {
                /* The edge case here: If a different command calls getGHRepositories, and then this command calls
                 * it again within 60 seconds, it will still have the same list of repositories (because of caching).
                 * However, between the previous and current call, if some of those repositories are deleted, the call
                 * above may cause a FileNotFoundException. This clause prevents that exception from stopping our call;
                 * we do not need to stop because getGHRepositories checks that we have all the repositories we need.
                 *
                 * The integration test calls the testParent -> testAllCommand -> testIdempotency, and the
                 * testIdempotency was failing because of this edge condition.
                 */

                log.warn("This repository does not exist. The list of repositories must be outdated, but the list" +
                        "contains the repositories we need, so we ignore this error.");
                return;
            }
        } else {
            return;
        }
        GHRepository parent = forkedRepo.getParent();

        if (GitHub.shouldNotProcessDockerfilesInRepo(pathToDockerfilesInParentRepo, parent)) return;

        log.info("Fixing Dockerfiles in {}...", forkedRepo.getFullName());
        String parentName = parent.getFullName();
        String branch = (ns.get(Constants.GIT_BRANCH) == null) ? forkedRepo.getDefaultBranch() : ns.get(Constants.GIT_BRANCH);

        String pathToDockerfile;
        String image;
        String tag;
        GHContent content;
        boolean isContentModified = false;
        boolean isRepoSkipped = true;

        Iterator<String> pathToDockerfileInParentRepoIterator = pathToDockerfilesInParentRepo.get(parentName).iterator();
        Iterator<String> imagesFoundInParentRepoIterator = imagesFoundInParentRepo.get(parentName).iterator();

        while (pathToDockerfileInParentRepoIterator.hasNext()) {
            pathToDockerfile = pathToDockerfileInParentRepoIterator.next();
            image = imagesFoundInParentRepoIterator.next();
            tag = imageToTagMap.get(image);
            log.info("pathToDockerfile: {} , image: {}, tag: {}", pathToDockerfile, image, tag);
            content = dockerfileGitHubUtil.tryRetrievingContent(forkedRepo, pathToDockerfile, branch);
            if (content == null) {
                log.info("No Dockerfile found at path: '{}'", pathToDockerfile);
            } else {
                dockerfileGitHubUtil.modifyOnGithub(content, branch, image, tag,
                        ns.get(Constants.GIT_ADDITIONAL_COMMIT_MESSAGE));
                isContentModified = true;
                isRepoSkipped = false;
            }
        }

        if (isRepoSkipped) {
            log.info("Skipping repo '{}' because contents of it's fork could not be retrieved. Moving ahead...",
                    parentName);
            skippedRepos.add(forkedRepo.getFullName());
        }

        if (isContentModified) {
            dockerfileGitHubUtil.createPullReq(parent, branch, forkedRepo, ns.get(Constants.GIT_PR_TITLE));
        }
    }

    private GHPullRequest getPullRequestWithPullReqIdentifier(GHRepository parent) throws IOException {
        List<GHPullRequest> pullRequests;
        GHUser myself;
        try {
            pullRequests = parent.getPullRequests(GHIssueState.OPEN);
            myself = dockerfileGitHubUtil.getMyself();
        } catch (IOException e) {
            log.warn("Error occurred while retrieving pull requests for {}", parent.getFullName());
            return null;
        }

        for (GHPullRequest pullRequest : pullRequests) {
            GHUser user = pullRequest.getHead().getUser();
            if (myself.equals(user) && pullRequest.getBody().equals(Constants.PULL_REQ_ID)) {
                return pullRequest;
            }
        }
        return null;
    }
}