/*
* RHQ Management Platform
* Copyright (C) 2009 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.enterprise.server.scheduler.jobs;

import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.InterruptableJob;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.StatefulJob;
import org.quartz.UnableToInterruptJobException;

import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.content.Repo;
import org.rhq.enterprise.server.auth.SubjectManagerLocal;
import org.rhq.enterprise.server.content.RepoManagerLocal;
import org.rhq.enterprise.server.util.LookupUtil;

/**
 * Quartz schedule job that handles synchronizing contents of a repo, including:
 * <ul>
 * <li>Package Metadata</li>
 * <li>Package Bits</li>
 * <li>Distribution Tree Metadata</li>
 * <li>Distribution Tree Bits</li>
 * </ul>
 * <p/>
 * Many of the calls out to the plugin are ultimately made from the
 * {@link org.rhq.enterprise.server.plugin.pc.content.ContentProviderManager}. The methods in
 * this job do not call that directly, but rather go through EJB methods to get there.
 * <p/>
 * This implements {@link StatefulJob} (as opposed to {@link org.quartz.Job}) because we do not need
 * nor want this job triggered concurrently. That is, we don't need multiple instances of this job
 * running at the same time.
 * <p/>
 * Much of the functionality in this class was migrated from {@link ContentProviderSyncJob}.
 *
 * @author Jason Dobies
 */
public class RepoSyncJob implements StatefulJob, InterruptableJob {

    public static final String KEY_REPO_NAME = "repoName";

    private final Log log = LogFactory.getLog(this.getClass());

    private Thread executionThread;

    private static ThreadLocal<Boolean> interruptMonitor = new ThreadLocal();

    /**
     * {@inheritDoc}
     */
    public void execute(JobExecutionContext context) throws JobExecutionException {

        executionThread = Thread.currentThread();

        try {
            JobDetail jobDetail = context.getJobDetail();
            if (jobDetail == null) {
                throw new IllegalStateException("The job does not have any details");
            }

            JobDataMap dataMap = jobDetail.getJobDataMap();
            if (dataMap == null) {
                throw new IllegalStateException("The job does not have any data in its details");
            }

            String repoName = dataMap.getString(KEY_REPO_NAME);

            if (repoName == null) {
                throw new IllegalStateException("Missing repo name in details context");
            }

            sync(repoName);
        } catch (InterruptedException ie) {
            log.error("Cancelled job [" + context.getJobDetail() + "]");
        } catch (Exception e) {
            String errorMsg = "Failed to sync repo in job [" + context.getJobDetail() + "]";

            log.error(errorMsg, e);
            JobExecutionException jobExecutionException = new JobExecutionException(errorMsg, e, false);

            // Do not retrigger if we threw IllegalStateException because it'll never work anyway
            if (!(e instanceof IllegalStateException)) {
                jobExecutionException.setUnscheduleAllTriggers(false);
            }

            throw jobExecutionException;
        }
    }

    /**
     * Creates (if necessary) and populates a job details map to contain the necessary data to
     * perform a repo sync. If there is an existing details map in the provided details object,
     * it will be reused, leaving existing data intact.
     *
     * @param details may not be <code>null</code>
     * @param repo    may not be <code>null</code>
     * @return populated map used to drive a repo sync job; this will be the same map as what
     *         exists in the {@link JobDetail#getJobDataMap()} call of the details object if
     *         that call does not return <code>null</code>
     */
    public static JobDataMap createJobDataMap(JobDetail details, Repo repo) {
        JobDataMap dataMap;

        if (details != null) {
            dataMap = details.getJobDataMap();
        } else {
            dataMap = new JobDataMap();
        }

        dataMap.put(KEY_REPO_NAME, repo.getName());

        return dataMap;
    }

    /**
     * Creates a name to use when scheduling a repo sync job. Calling this method multiple times
     * on the same parameters will always produce the <em>same</em> name.
     *
     * @param repo may not be <code>null</code>
     * @return name to use to schedule the job; will not be <code>null</code>
     */
    public static String createJobName(Repo repo) {
        // The quartz table has a limited column width of 80 - but we need to use the names to make
        // jobs unique so encode the names' hashcodes to ensure we fit into the quartz job name.
        String jobName = Integer.toHexString(repo.getName().hashCode());

        if (jobName.length() > 80) {
            throw new IllegalArgumentException("Job names max size is 80 chars due to DB column "
                + "size restrictions: " + jobName);
        }

        return jobName;
    }

    /**
     * Performs similar to {#createJobName} except adds uniqueness to the name regardless of the
     * parameters. In other words, calling this method multiple times on the same parameters
     * will always (almost, it's based on system time) produce a <em>unique</em> name.
     *
     * @param repo may not be <code>null</code>
     * @return name to use to schedule the job; will not be <code>null</code>
     */
    public static String createUniqueJobName(Repo repo) {
        // Append current time to add uniqueness to exising job name algorithm
        String jobName = createJobName(repo);

        String uniquifier = Long.toHexString(System.currentTimeMillis());
        jobName = jobName + "-" + uniquifier;

        if (jobName.length() > 80) {
            throw new IllegalArgumentException("Job names max size is 80 chars due to DB column "
                + "size restrictions: " + jobName);
        }

        return jobName;
    }

    /**
     * Performs the repo synchronization.
     * <p/>
     * Note that this method executes outside of any transaction. This is very important since this
     * job is potentially very long running (on the order of hours potentially). We do our processing
     * in here with this in mind. We make sure we never do any one thing that potentially could
     * timeout a transaction.
     *
     * @param repoName may not be <code>null</code>
     * @throws InterruptedException 
     * @throws  
     * @throws Exception if there is an error in the sync
     */
    private void sync(String repoName) throws InterruptedException {
        log.debug("Syncing repo [" + repoName + "]...");

        SubjectManagerLocal subjectManager = LookupUtil.getSubjectManager();
        RepoManagerLocal repoManager = LookupUtil.getRepoManagerLocal();

        // Note that we will keep calling getOverlord on this subject manager - the overlord
        // has a very short session lifespan so we need to keep asking for a new one, due to
        // the possibility that some of the methods we call here take longer than its lifespan
        Subject overlord;

        overlord = subjectManager.getOverlord();
        List<Repo> repoList = repoManager.getRepoByName(repoName);

        if (repoList.size() != 1) {
            throw new RuntimeException("Unexpected number of repos found for name [" + repoName + "]. " + "Found ["
                + repoList.size() + "] repos");
        }
        Repo repoToSync = repoList.get(0);

        // This call executes all of the logic associated with synchronizing the given repo
        repoManager.internalSynchronizeRepos(overlord, new Integer[] { repoToSync.getId() });
    }

    public void interrupt() throws UnableToInterruptJobException {
        if (executionThread == null) {
            log.error("execution thread is null, cant interrupt", new IllegalStateException());

        } else {
            log.debug("exeThread : [" + executionThread.getName() + "]");
            executionThread.interrupt();
        }
    }

}
