package com.github.k0kubun.github_ranking.worker;

import com.github.k0kubun.github_ranking.config.Config;
import com.github.k0kubun.github_ranking.github.GitHubClient;
import com.github.k0kubun.github_ranking.github.GitHubClientBuilder;
import com.github.k0kubun.github_ranking.model.AccessToken;
import com.github.k0kubun.github_ranking.model.Repository;
import com.github.k0kubun.github_ranking.model.UpdateUserJob;
import com.github.k0kubun.github_ranking.model.User;
import com.github.k0kubun.github_ranking.repository.DatabaseLock;
import com.github.k0kubun.github_ranking.repository.dao.AccessTokenDao;
import com.github.k0kubun.github_ranking.repository.dao.RepositoryDao;
import com.github.k0kubun.github_ranking.repository.dao.UpdateUserJobDao;
import com.github.k0kubun.github_ranking.repository.dao.UserDao;

import java.io.IOException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.sql.DataSource;

import org.skife.jdbi.v2.DBI;
import org.skife.jdbi.v2.Handle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// This job must finish within TIMEOUT_MINUTES (1 min). Otherwise it will be infinitely retried.
public class UpdateUserWorker
        extends Worker
{
    private static final Integer TIMEOUT_MINUTES = 1;
    private static final Integer POLLING_INTERVAL_SECONDS = 1;
    private static final Logger LOG = LoggerFactory.getLogger(UpdateUserWorker.class);

    private final DBI dbi;
    private final GitHubClientBuilder clientBuilder;

    public UpdateUserWorker(DataSource dataSource)
    {
        super();
        clientBuilder = new GitHubClientBuilder(dataSource);
        dbi = new DBI(dataSource);
    }

    // Dequeue a record from update_user_jobs and call updateUser().
    @Override
    public void perform()
            throws Exception
    {
        try (Handle handle = dbi.open()) {
            DatabaseLock lock = new DatabaseLock(handle, this);

            // Poll until it succeeds to acquire a job...
            Timestamp timeoutAt;
            while (acquireUntil(lock, timeoutAt = nextTimeout()) == 0) {
                if (isStopped) {
                    return;
                }
                TimeUnit.SECONDS.sleep(1);
            }

            // Succeeded to acquire a job. Fetch job to execute.
            UpdateUserJobDao dao = handle.attach(UpdateUserJobDao.class);
            UpdateUserJob job = dao.fetchByTimeout(timeoutAt);
            if (job == null) {
                throw new RuntimeException("Failed to fetch a job (timeoutAt = " + timeoutAt + ")");
            }

            // TODO: Log elapsed time
            try {
                lock.withUserUpdate(job.getUserId(), () -> {
                    LOG.info("started to updateUser: (userId = " + job.getUserId() + ")");
                    updateUser(handle, job.getUserId(), job.getTokenUserId());
                    LOG.info("finished to updateUser: (userId = " + job.getUserId() + ")");
                });
            }
            catch (Exception e) {
                LOG.error("Failed to updateUser! (userId = " + job.getUserId() + "): " + e.toString() + ": " + e.getMessage());
                e.printStackTrace();
            }
            finally {
                dao.delete(job.getId());
            }
        }
    }

    // Concurrently executing `dao.acquireUntil` causes deadlock. So this executes it in a lock.
    private long acquireUntil(DatabaseLock lock, Timestamp timeoutAt)
    {
        return lock.withUpdateUserJobs(dao -> dao.acquireUntil(timeoutAt));
    }

    private Timestamp nextTimeout()
    {
        return Timestamp.valueOf(LocalDateTime.now(ZoneId.of("UTC")).plusMinutes(TIMEOUT_MINUTES));
    }

    // Main part of this class. Given enqueued userId, it updates a user and his repositories.
    // * Sync information of all repositories owned by specified user.
    // * Update fetched_at and updated_at, and set total stars to user.
    // TODO: Requeue if GitHub API limit exceeded
    private void updateUser(Handle handle, Integer userId, Integer tokenUserId)
            throws IOException
    {
        LOG.debug("before: clientBuilder.buildForUser");
        GitHubClient client = clientBuilder.buildForUser(tokenUserId);
        LOG.debug("after: clientBuilder.buildForUser");

        try {
            LOG.debug("before: find User");
            User user = handle.attach(UserDao.class).find(userId);
            LOG.debug("after: find User");

            if (!user.isOrganization()) {
                String login = client.getLogin(userId);
                LOG.debug("before: update Login");
                handle.attach(UserDao.class).updateLogin(userId, login);
                LOG.debug("after: update Login");
            }

            LOG.debug("before: getPublicRepos");
            List<Repository> repos = client.getPublicRepos(userId, user.isOrganization());
            LOG.debug("after: getPublicRepos");

            handle.useTransaction((conn, status) -> {
                //conn.attach(RepositoryDao.class).deleteAllOwnedBy(userId); // Delete obsolete ones
                LOG.debug("before: bulkInsert");
                conn.attach(RepositoryDao.class).bulkInsert(repos);
                LOG.debug("after: bulkInsert");
                LOG.debug("before: updateStars");
                conn.attach(UserDao.class).updateStars(userId, calcTotalStars(repos));
                LOG.debug("after: updateStars");
            });
            LOG.info("imported repos: " + repos.size());
        }
        catch (GitHubClient.UserNotFoundException e) {
            LOG.error("UserNotFoundException error: " + e.getMessage());
            LOG.info("delete user: " + userId.toString());
            handle.useTransaction((conn, status) -> {
                conn.attach(UserDao.class).delete(userId);
                conn.attach(RepositoryDao.class).deleteAllOwnedBy(userId);
            });
        }
    }

    private int calcTotalStars(List<Repository> repos)
    {
        int totalStars = 0;
        for (Repository repo : repos) {
            totalStars += repo.getStargazersCount();
        }
        return totalStars;
    }
}

@@ -4218,65 +4218,8 @@
   %7B%0A
-        LOG.debug(%22before: clientBuilder.buildForUser%22);%0A
     
@@ -4304,21 +4304,24 @@
 .debug(%22
-after
+finished
 : client
@@ -4363,52 +4363,8 @@
 y %7B%0A
-            LOG.debug(%22before: find User%22);%0A
     
@@ -4449,21 +4449,24 @@
 .debug(%22
-after
+finished
 : find U
@@ -4575,59 +4575,8 @@
 d);%0A
-                LOG.debug(%22before: update Login%22);%0A
     
@@ -4663,37 +4663,40 @@
      LOG.debug(%22
-after
+finished
 : update Login%22)
@@ -4716,57 +4716,8 @@
  %7D%0A%0A
-            LOG.debug(%22before: getPublicRepos%22);%0A
     
@@ -4826,21 +4826,24 @@
 .debug(%22
-after
+finished
 : getPub
@@ -5014,57 +5014,8 @@
 nes%0A
-                LOG.debug(%22before: bulkInsert%22);%0A
     
@@ -5105,21 +5105,24 @@
 .debug(%22
-after
+finished
 : bulkIn
@@ -5133,58 +5133,8 @@
 %22);%0A
-                LOG.debug(%22before: updateStars%22);%0A
     
@@ -5247,13 +5247,16 @@
 ug(%22
-after
+finished
 : up

Drop lengthy logs