/* 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.
 */
 
#include <assert.h>
#include <apr_optional.h>
#include <apr_hash.h>
#include <apr_strings.h>
#include <apr_date.h>

#include <httpd.h>
#include <http_core.h>
#include <http_protocol.h>
#include <http_request.h>
#include <http_log.h>

#include "mod_watchdog.h"

#include "md.h"
#include "md_curl.h"
#include "md_crypt.h"
#include "md_http.h"
#include "md_json.h"
#include "md_status.h"
#include "md_store.h"
#include "md_store_fs.h"
#include "md_log.h"
#include "md_result.h"
#include "md_reg.h"
#include "md_util.h"
#include "md_version.h"
#include "md_acme.h"
#include "md_acme_authz.h"

#include "mod_md.h"
#include "mod_md_private.h"
#include "mod_md_config.h"
#include "mod_md_status.h"
#include "mod_md_drive.h"

/**************************************************************************************************/
/* watchdog based impl. */

#define MD_WATCHDOG_NAME   "_md_"

static APR_OPTIONAL_FN_TYPE(ap_watchdog_get_instance) *wd_get_instance;
static APR_OPTIONAL_FN_TYPE(ap_watchdog_register_callback) *wd_register_callback;
static APR_OPTIONAL_FN_TYPE(ap_watchdog_set_callback_interval) *wd_set_interval;

struct md_drive_ctx {
    apr_pool_t *p;
    server_rec *s;
    md_mod_conf_t *mc;
    ap_watchdog_t *watchdog;
    
    apr_array_header_t *jobs;
};

typedef struct {
    apr_pool_t *p;
    md_job_t *job;
    md_reg_t *reg;
    md_result_t *last;
    apr_time_t last_save;
} md_job_result_ctx;

static void job_result_update(md_result_t *result, void *data)
{
    md_job_result_ctx *ctx = data;
    apr_time_t now;
    const char *msg, *sep;
    
    if (md_result_cmp(ctx->last, result)) {
        now = apr_time_now();
        md_result_assign(ctx->last, result);
        if (result->activity || result->problem || result->detail) {
            msg = sep = "";
            if (result->activity) {
                msg = apr_psprintf(result->p, "%s", result->activity);
                sep = ": ";
            }
            if (result->detail) {
                msg = apr_psprintf(result->p, "%s%s%s", msg, sep, result->detail);
                sep = ", ";
            }
            if (result->problem) {
                msg = apr_psprintf(result->p, "%s%sproblem: %s", msg, sep, result->problem);
                sep = " ";
            }
            md_job_log_append(ctx->job, "progress", NULL, msg);

            if (apr_time_msec(now - ctx->last_save) > 500) {
                md_job_save(ctx->job, ctx->reg, MD_SG_STAGING, result, ctx->p);
                ctx->last_save = now;
            }
        }
    }
}

static void job_result_observation_start(md_job_t *job, md_result_t *result, 
                                         md_reg_t *reg, apr_pool_t *p)
{
    md_job_result_ctx *ctx;

    ctx = apr_pcalloc(p, sizeof(*ctx));
    ctx->p = p;
    ctx->job = job;
    ctx->reg = reg;
    ctx->last = md_result_md_make(p, APR_SUCCESS);
    md_result_assign(ctx->last, result);
    md_result_on_change(result, job_result_update, ctx);
}

static void job_result_observation_end(md_job_t *job, md_result_t *result)
{
    (void)job;
    md_result_on_change(result, NULL, NULL);
} 

static apr_time_t calc_err_delay(int err_count)
{
    apr_time_t delay = 0;
    
    if (err_count > 0) {
        /* back off duration, depending on the errors we encounter in a row */
        delay = apr_time_from_sec(5 << (err_count - 1));
        if (delay > apr_time_from_sec(60*60)) {
            delay = apr_time_from_sec(60*60);
        }
    }
    return delay;
}

static apr_status_t send_notification(md_drive_ctx *dctx, md_job_t *job, const md_t *md, 
                                      const char *reason, md_result_t *result, apr_pool_t *ptemp)
{
    const char * const *argv;
    const char *cmdline;
    int exit_code;
    apr_status_t rv;            
    
    if (!strcmp("renewed", reason)) {
        if (dctx->mc->notify_cmd) {
            cmdline = apr_psprintf(ptemp, "%s %s", dctx->mc->notify_cmd, md->name); 
            apr_tokenize_to_argv(cmdline, (char***)&argv, ptemp);
            rv = md_util_exec(ptemp, argv[0], argv, &exit_code);
            
            if (APR_SUCCESS == rv && exit_code) rv = APR_EGENERAL;
            if (APR_SUCCESS != rv) {
                if (!result) result = md_result_make(ptemp, rv);
                md_result_problem_printf(result, rv, MD_RESULT_LOG_ID(APLOGNO(10108)), 
                                         "MDNotifyCmd %s failed with exit code %d.", 
                                         dctx->mc->notify_cmd, exit_code);
                md_result_log(result, MD_LOG_ERR);
                md_job_log_append(job, "notify-error", result->problem, result->detail);
                goto leave;
            }
        }
        ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, dctx->s, APLOGNO(10059) 
                     "The Managed Domain %s has been setup and changes "
                     "will be activated on next (graceful) server restart.", md->name);
    }
    if (dctx->mc->message_cmd) {
        cmdline = apr_psprintf(ptemp, "%s %s %s", dctx->mc->message_cmd, reason, md->name); 
        ap_log_error(APLOG_MARK, APLOG_TRACE2, 0, dctx->s, "Message command: %s", cmdline);
        apr_tokenize_to_argv(cmdline, (char***)&argv, ptemp);
        rv = md_util_exec(ptemp, argv[0], argv, &exit_code);
        
        if (APR_SUCCESS == rv && exit_code) rv = APR_EGENERAL;
        if (APR_SUCCESS != rv) {
            if (!result) result = md_result_make(ptemp, rv);
            md_result_problem_printf(result, rv, MD_RESULT_LOG_ID(APLOGNO(10109)), 
                                     "MDMessageCmd %s failed with exit code %d.", 
                                     dctx->mc->notify_cmd, exit_code);
            md_result_log(result, MD_LOG_ERR);
            md_job_log_append(job, "message-error", reason, result->detail);
            goto leave;
        }
    }
leave:
    return rv;
}

static void check_expiration(md_drive_ctx *dctx, md_job_t *job, const md_t *md, apr_pool_t *ptemp)
{
    md_timeperiod_t since_last;
    
    ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, dctx->s, "md(%s): check expiration", md->name);
    if (!md_reg_should_warn(dctx->mc->reg, md, dctx->p)) return;
    
    /* Sends these out at most once per day */
    since_last.start = md_job_log_get_time_of_latest(job, "message-expiring");
    since_last.end = apr_time_now();

    if (md_timeperiod_length(&since_last) >= apr_time_from_sec(MD_SECS_PER_DAY)) {
        ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, dctx->s, 
                     "md(%s): message expiration warning", md->name);
        send_notification(dctx, job, md, "expiring", NULL, ptemp);
    }
}

static void process_drive_job(md_drive_ctx *dctx, md_job_t *job, apr_pool_t *ptemp)
{
    const md_t *md;
    md_result_t *result;
    int error_run = 0, fatal_run = 0, save = 0;
    apr_status_t rv;
    
    md_job_load(job, dctx->mc->reg, MD_SG_STAGING, ptemp);
    /* Evaluate again on loaded value. Values will change when watchdog switches child process */
    if (apr_time_now() < job->next_run) return;
    
    md = md_get_by_name(dctx->mc->mds, job->name);
    AP_DEBUG_ASSERT(md);

    result = md_result_md_make(ptemp, md);
    if (job->last_result) md_result_assign(result, job->last_result); 
    
    if (md->state == MD_S_MISSING_INFORMATION) {
        /* Missing information, this will not change until configuration
         * is changed and server reloaded. */
        fatal_run = 1;
        goto leave;
    }
    
    while (md_will_renew_cert(md)) {
        if (job->finished) {
            job->next_run = 0;
            /* Finished jobs might take a while before the results become valid.
             * If that is in the future, request to run then */
            if (apr_time_now() < job->valid_from) {
                job->next_run = job->valid_from;
            }
            else if (md_job_log_get_time_of_latest(job, "notified") == 0) {
                rv = send_notification(dctx, job, md, "renewed", result, ptemp);
                if (APR_SUCCESS == rv) {
                    md_job_log_append(job, "notified", NULL, NULL);
                    save = 1;
                }
                else { 
                    /* we treat this as an error that triggers retries */
                    error_run = 1;
                }
            }
            goto leave;
        }
        
        if (!md_reg_should_renew(dctx->mc->reg, md, dctx->p)) {
            ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10053) 
                         "md(%s): no need to renew yet", job->name);
            job->next_run = 0;
            goto leave;
        }

        /* Renew the MDs credentials in a STAGING area. Might be invoked repeatedly 
         * without discarding previous/intermediate results.
         * Only returns SUCCESS when the renewal is complete, e.g. STAGING as a
         * complete set of new credentials.
         */
        ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10052) 
                     "md(%s): state=%d, driving", job->name, md->state);
        md_job_log_append(job, "renewal-start", NULL, NULL);
        /* observe result changes and persist them with limited frequency */
        job_result_observation_start(job, result, dctx->mc->reg, ptemp);
        
        md_reg_renew(dctx->mc->reg, md, dctx->mc->env, 0, result, ptemp);
        
        job_result_observation_end(job, result);
        if (APR_SUCCESS != result->status) {
            ap_log_error( APLOG_MARK, APLOG_ERR, result->status, dctx->s, APLOGNO(10056) 
                         "processing %s: %s", job->name, result->detail);
            error_run = 1;
            md_job_log_append(job, "renewal-error", result->problem, result->detail);
            send_notification(dctx, job, md, "errored", result, ptemp);
            goto leave;
        }
        
        job->finished = 1;
        job->valid_from = result->ready_at;
        job->error_runs = 0;
        md_job_log_append(job, "renewal-finish", NULL, NULL);
        save = 1;
    }
    
leave:
    if (!job->finished) {
        check_expiration(dctx, job, md, ptemp);
    }
    
    if (fatal_run) {
        save = 1;
        job->next_run = 0;
    }
    if (error_run) {
        ++job->error_runs;
        save = 1;
        job->next_run = apr_time_now() + calc_err_delay(job->error_runs);
        ap_log_error(APLOG_MARK, APLOG_INFO, 0, dctx->s, APLOGNO(10057) 
                     "%s: encountered error for the %d. time, next run in %s",
                     job->name, job->error_runs, 
                     md_duration_print(ptemp, job->next_run - apr_time_now()));
    }
    if (save) {
        apr_status_t rv2 = md_job_save(job, dctx->mc->reg, MD_SG_STAGING, result, ptemp);
        ap_log_error(APLOG_MARK, APLOG_TRACE1, rv2, dctx->s, "%s: saving job props", job->name);
    }
}

int md_will_renew_cert(const md_t *md)
{
    if (md->renew_mode == MD_RENEW_MANUAL) {
        return 0;
    }
    else if (md->renew_mode == MD_RENEW_AUTO && md->cert_file) {
        return 0;
    } 
    return 1;
}

static apr_time_t next_run_default(void)
{
    /* we'd like to run at least twice a day by default */
    return apr_time_now() + apr_time_from_sec(MD_SECS_PER_DAY / 2);
}

static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp)
{
    md_drive_ctx *dctx = baton;
    md_job_t *job;
    apr_time_t next_run, wait_time;
    int i;
    
    /* mod_watchdog invoked us as a single thread inside the whole server (on this machine).
     * This might be a repeated run inside the same child (mod_watchdog keeps affinity as
     * long as the child lives) or another/new child.
     */
    switch (state) {
        case AP_WATCHDOG_STATE_STARTING:
            ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10054)
                         "md watchdog start, auto drive %d mds", dctx->jobs->nelts);
            break;
            
        case AP_WATCHDOG_STATE_RUNNING:
            ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10055)
                         "md watchdog run, auto drive %d mds", dctx->jobs->nelts);
                         
            /* Process all drive jobs. They will update their next_run property
             * and we schedule ourself at the earliest of all. A job may specify 0
             * as next_run to indicate that it wants to participate in the normal
             * regular runs. */
            next_run = next_run_default();
            for (i = 0; i < dctx->jobs->nelts; ++i) {
                job = APR_ARRAY_IDX(dctx->jobs, i, md_job_t *);
                
                if (apr_time_now() >= job->next_run) {
                    process_drive_job(dctx, job, ptemp);
                }
                
                if (job->next_run && job->next_run < next_run) {
                    next_run = job->next_run;
                }
            }

            wait_time = next_run - apr_time_now();
            if (APLOGdebug(dctx->s)) {
                ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10107)
                             "next run in %s", md_duration_print(ptemp, wait_time));
            }
            wd_set_interval(dctx->watchdog, wait_time, dctx, run_watchdog);
            break;
            
        case AP_WATCHDOG_STATE_STOPPING:
            ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10058)
                         "md watchdog stopping");
            break;
    }
    
    return APR_SUCCESS;
}

apr_status_t md_start_watching(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p)
{
    apr_allocator_t *allocator;
    md_drive_ctx *dctx;
    apr_pool_t *dctxp;
    apr_status_t rv;
    const char *name;
    md_t *md;
    md_job_t *job;
    int i;
    
    /* We use mod_watchdog to run a single thread in one of the child processes
     * to monitor the MDs in mc->watched_names, using the const data in the list
     * mc->mds of our MD structures.
     *
     * The data in mc cannot be changed, as we may spawn copies in new child processes
     * of the original data at any time. The child which hosts the watchdog thread
     * may also die or be recycled, which causes a new watchdog thread to run
     * in another process with the original data.
     * 
     * Instead, we use our store to persist changes in group STAGING. This is
     * kept writable to child processes, but the data stored there is not live.
     * However, mod_watchdog makes sure that we only ever have a single thread in
     * our server (on this machine) that writes there. Other processes, e.g. informing
     * the user about progress, only read from there.
     *
     * All changes during driving an MD are stored as files in MG_SG_STAGING/<MD.name>.
     * All will have "md.json" and "job.json". There may be a range of other files used
     * by the protocol obtaining the certificate/keys.
     * 
     * 
     */
    wd_get_instance = APR_RETRIEVE_OPTIONAL_FN(ap_watchdog_get_instance);
    wd_register_callback = APR_RETRIEVE_OPTIONAL_FN(ap_watchdog_register_callback);
    wd_set_interval = APR_RETRIEVE_OPTIONAL_FN(ap_watchdog_set_callback_interval);
    
    if (!wd_get_instance || !wd_register_callback || !wd_set_interval) {
        ap_log_error(APLOG_MARK, APLOG_CRIT, 0, s, APLOGNO(10061) "mod_watchdog is required");
        return !OK;
    }
    
    /* We want our own pool with own allocator to keep data across watchdog invocations.
     * Since we'll run in a single watchdog thread, using our own allocator will prevent 
     * any confusion in the parent pool. */
    apr_allocator_create(&allocator);
    apr_allocator_max_free_set(allocator, 1);
    rv = apr_pool_create_ex(&dctxp, p, NULL, allocator);
    if (rv != APR_SUCCESS) {
        ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10062) "md_drive_ctx: create pool");
        return rv;
    }
    apr_allocator_owner_set(allocator, dctxp);
    apr_pool_tag(dctxp, "md_drive_ctx");

    dctx = apr_pcalloc(dctxp, sizeof(*dctx));
    dctx->p = dctxp;
    dctx->s = s;
    dctx->mc = mc;
    
    dctx->jobs = apr_array_make(dctx->p, mc->watched_names->nelts, sizeof(md_job_t *));
    for (i = 0; i < mc->watched_names->nelts; ++i) {
        name = APR_ARRAY_IDX(mc->watched_names, i, const char *);
        md = md_get_by_name(mc->mds, name);
        if (!md) continue;
        
        job = md_job_make(p, md->name);
        APR_ARRAY_PUSH(dctx->jobs, md_job_t*) = job;
        ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, dctx->s,  
                     "md(%s): state=%d, created drive job", name, md->state);
        
        md_job_load(job, mc->reg, MD_SG_STAGING, dctx->p);
        if (job->error_runs) {
            /* Server has just restarted. If we encounter an MD job with errors
             * on a previous driving, we purge its STAGING area.
             * This will reset the driving for the MD. It may run into the same
             * error again, or in case of race/confusion/our error/CA error, it
             * might allow the MD to succeed by a fresh start.
             */
            ap_log_error( APLOG_MARK, APLOG_NOTICE, 0, dctx->s, APLOGNO(10064) 
                         "md(%s): previous drive job showed %d errors, purging STAGING "
                         "area to reset.", name, job->error_runs);
            md_store_purge(md_reg_store_get(dctx->mc->reg), p, MD_SG_STAGING, md->name);
            md_store_purge(md_reg_store_get(dctx->mc->reg), p, MD_SG_CHALLENGES, md->name);
            job->error_runs = 0;
        }
    }

    if (!dctx->jobs->nelts) {
        ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10065)
                     "no managed domain to drive, no watchdog needed.");
        apr_pool_destroy(dctx->p);
        return APR_SUCCESS;
    }
    
    if (APR_SUCCESS != (rv = wd_get_instance(&dctx->watchdog, MD_WATCHDOG_NAME, 0, 1, dctx->p))) {
        ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, APLOGNO(10066) 
                     "create md watchdog(%s)", MD_WATCHDOG_NAME);
        return rv;
    }
    rv = wd_register_callback(dctx->watchdog, 0, dctx, run_watchdog);
    ap_log_error(APLOG_MARK, rv? APLOG_CRIT : APLOG_DEBUG, rv, s, APLOGNO(10067) 
                 "register md watchdog(%s)", MD_WATCHDOG_NAME);
    return rv;
}
