/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.deltaspike.scheduler.impl;

import org.apache.deltaspike.core.api.config.ConfigResolver;
import org.apache.deltaspike.core.api.provider.BeanProvider;
import org.apache.deltaspike.core.spi.activation.Deactivatable;
import org.apache.deltaspike.scheduler.spi.Scheduler;
import org.quartz.CronScheduleBuilder;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.PersistJobDataAfterExecution;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;

import jakarta.inject.Inject;
import java.util.logging.Logger;

/**
 * This job is only active, if configurable cron-expressions are used - e.g.: @Scheduled(cronExpression = "{myKey}").
 * It observes jobs with configurable cron-expressions and updates their job-triggers once a config-change was detected.
 * Per default this job gets executed once per minute. That can be changed via config-entry:
 * deltaspike.scheduler.dynamic-expression.observer-interval=[any valid cron-expression]
 */
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public class DynamicExpressionObserverJob implements Deactivatable, Job
{
    static final String CONFIG_EXPRESSION_KEY = "ds_configExpression";
    static final String ACTIVE_CRON_EXPRESSION_KEY = "ds_activeCronExpression";
    static final String TRIGGER_ID_KEY = "ds_triggerKey";
    static final String OBSERVER_POSTFIX = "_observer";

    private static final Logger LOG = Logger.getLogger(DynamicExpressionObserverJob.class.getName());

    @Inject
    private Scheduler<Job> scheduler;

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException
    {
        JobDataMap jobDataMap = context.getMergedJobDataMap();
        String configExpression = jobDataMap.getString(CONFIG_EXPRESSION_KEY);
        String triggerId = jobDataMap.getString(TRIGGER_ID_KEY);
        String activeCronExpression = jobDataMap.getString(ACTIVE_CRON_EXPRESSION_KEY);

        String configKey = configExpression.substring(1, configExpression.length() - 1);
        String configuredValue = ConfigResolver.getPropertyAwarePropertyValue(configKey, activeCronExpression);

        if (!activeCronExpression.equals(configuredValue))
        {
            //both #put calls are needed currently
            context.getJobDetail().getJobDataMap().put(ACTIVE_CRON_EXPRESSION_KEY, configuredValue);
            context.getTrigger().getJobDataMap().put(ACTIVE_CRON_EXPRESSION_KEY, configuredValue);

            BeanProvider.injectFields(this);

            JobKey observerJobKey = context.getJobDetail().getKey();
            String observedJobName = observerJobKey.getName()
                .substring(0, observerJobKey.getName().length() - OBSERVER_POSTFIX.length());
            JobKey observedJobKey = new JobKey(observedJobName, observerJobKey.getGroup());

            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(triggerId)
                    .forJob(observedJobName, observedJobKey.getGroup())
                    .withSchedule(CronScheduleBuilder.cronSchedule(configuredValue))
                    .build();

            //use rescheduleJob instead of delete + add
            //(unwrap is ok here, because this class will only get active in case of a quartz-scheduler)
            org.quartz.Scheduler quartzScheduler = scheduler.unwrap(org.quartz.Scheduler.class);
            try
            {
                quartzScheduler.rescheduleJob(trigger.getKey(), trigger);
            }
            catch (SchedulerException e)
            {
                LOG.warning("failed to updated cron-expression for " + observedJobKey);
            }
        }
    }
}
