/*
 * Copyright 2004-2019 the Pacemaker project contributors
 *
 * The version control history for this file may have further details.
 *
 * This source code is licensed under the GNU Lesser General Public License
 * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
 */

#include <crm_internal.h>

#include <crm/crm.h>
#include <crm/msg_xml.h>
#include <crm/common/xml.h>
#include <pacemaker-internal.h>

crm_graph_functions_t *graph_fns = NULL;

static gboolean
update_synapse_ready(synapse_t * synapse, int action_id)
{
    GList *lpc = NULL;
    gboolean updates = FALSE;

    CRM_CHECK(synapse->executed == FALSE, return FALSE);
    CRM_CHECK(synapse->confirmed == FALSE, return FALSE);

    synapse->ready = TRUE;
    for (lpc = synapse->inputs; lpc != NULL; lpc = lpc->next) {
        crm_action_t *prereq = (crm_action_t *) lpc->data;

        crm_trace("Processing input %d", prereq->id);

        if (prereq->id == action_id) {
            crm_trace("Marking input %d of synapse %d confirmed", action_id, synapse->id);
            prereq->confirmed = TRUE;
            updates = TRUE;

        } else if (prereq->confirmed == FALSE) {
            synapse->ready = FALSE;
        }

    }

    if (updates) {
        crm_trace("Updated synapse %d", synapse->id);
    }
    return updates;
}

static gboolean
update_synapse_confirmed(synapse_t * synapse, int action_id)
{
    GList *lpc = NULL;
    gboolean updates = FALSE;
    gboolean is_confirmed = TRUE;

    CRM_CHECK(synapse->executed, return FALSE);
    CRM_CHECK(synapse->confirmed == FALSE, return TRUE);

    is_confirmed = TRUE;
    for (lpc = synapse->actions; lpc != NULL; lpc = lpc->next) {
        crm_action_t *action = (crm_action_t *) lpc->data;

        crm_trace("Processing action %d", action->id);

        if (action->id == action_id) {
            crm_trace("Confirmed: Action %d of Synapse %d", action_id, synapse->id);
            action->confirmed = TRUE;
            updates = TRUE;

        } else if (action->confirmed == FALSE) {
            is_confirmed = FALSE;
            crm_trace("Synapse %d still not confirmed after action %d", synapse->id, action_id);
        }
    }

    if (is_confirmed && synapse->confirmed == FALSE) {
        crm_trace("Confirmed: Synapse %d", synapse->id);
        synapse->confirmed = TRUE;
        updates = TRUE;
    }

    if (updates) {
        crm_trace("Updated synapse %d", synapse->id);
    }
    return updates;
}

gboolean
update_graph(crm_graph_t * graph, crm_action_t * action)
{
    gboolean rc = FALSE;
    gboolean updates = FALSE;
    GList *lpc = NULL;

    for (lpc = graph->synapses; lpc != NULL; lpc = lpc->next) {
        synapse_t *synapse = (synapse_t *) lpc->data;

        if (synapse->confirmed || synapse->failed) {
            crm_trace("Synapse complete");

        } else if (synapse->executed) {
            crm_trace("Synapse executed");
            rc = update_synapse_confirmed(synapse, action->id);

        } else if (action->failed == FALSE || synapse->priority == INFINITY) {
            rc = update_synapse_ready(synapse, action->id);
        }
        updates = updates || rc;
    }

    if (updates) {
        crm_trace("Updated graph with completed action %d", action->id);
    }
    return updates;
}

static gboolean
should_fire_synapse(crm_graph_t * graph, synapse_t * synapse)
{
    GList *lpc = NULL;

    CRM_CHECK(synapse->executed == FALSE, return FALSE);
    CRM_CHECK(synapse->confirmed == FALSE, return FALSE);

    crm_trace("Checking pre-reqs for synapse %d", synapse->id);
    /* lookup prereqs */
    synapse->ready = TRUE;
    for (lpc = synapse->inputs; lpc != NULL; lpc = lpc->next) {
        crm_action_t *prereq = (crm_action_t *) lpc->data;

        crm_trace("Processing input %d", prereq->id);
        if (prereq->confirmed == FALSE) {
            crm_trace("Input %d for synapse %d not satisfied: not confirmed", prereq->id, synapse->id);
            synapse->ready = FALSE;
            break;
        } else if(prereq->failed && prereq->can_fail == FALSE) {
            crm_trace("Input %d for synapse %d not satisfied: failed", prereq->id, synapse->id);
            synapse->ready = FALSE;
            break;
        }
    }

    for (lpc = synapse->actions; synapse->ready && lpc != NULL; lpc = lpc->next) {
        crm_action_t *a = (crm_action_t *) lpc->data;

        if (a->type == action_type_pseudo) {
            /* None of the below applies to pseudo ops */

        } else if (synapse->priority < graph->abort_priority) {
            crm_trace("Skipping synapse %d: abort level %d", synapse->id, graph->abort_priority);
            graph->skipped++;
            return FALSE;

        } else if(graph_fns->allowed && graph_fns->allowed(graph, a) == FALSE) {
            crm_trace("Deferring synapse %d: allowed", synapse->id);
            return FALSE;
        }
    }

    return synapse->ready;
}

static gboolean
initiate_action(crm_graph_t * graph, crm_action_t * action)
{
    const char *id = NULL;

    CRM_CHECK(action->executed == FALSE, return FALSE);

    id = ID(action->xml);
    CRM_CHECK(id != NULL, return FALSE);

    action->executed = TRUE;
    if (action->type == action_type_pseudo) {
        crm_trace("Executing pseudo-event: %s (%d)", id, action->id);
        return graph_fns->pseudo(graph, action);

    } else if (action->type == action_type_rsc) {
        crm_trace("Executing rsc-event: %s (%d)", id, action->id);
        return graph_fns->rsc(graph, action);

    } else if (action->type == action_type_crm) {
        const char *task = NULL;

        task = crm_element_value(action->xml, XML_LRM_ATTR_TASK);
        CRM_CHECK(task != NULL, return FALSE);

        if (pcmk__str_eq(task, CRM_OP_FENCE, pcmk__str_casei)) {
            crm_trace("Executing STONITH-event: %s (%d)", id, action->id);
            return graph_fns->stonith(graph, action);
        }

        crm_trace("Executing crm-event: %s (%d)", id, action->id);
        return graph_fns->crmd(graph, action);
    }

    crm_err("Failed on unsupported command type: %s (id=%s)", crm_element_name(action->xml), id);
    return FALSE;
}

static gboolean
fire_synapse(crm_graph_t * graph, synapse_t * synapse)
{
    GList *lpc = NULL;

    CRM_CHECK(synapse != NULL, return FALSE);
    CRM_CHECK(synapse->ready, return FALSE);
    CRM_CHECK(synapse->confirmed == FALSE, return TRUE);

    crm_trace("Synapse %d fired", synapse->id);
    synapse->executed = TRUE;
    for (lpc = synapse->actions; lpc != NULL; lpc = lpc->next) {
        crm_action_t *action = (crm_action_t *) lpc->data;

        /* allow some leeway */
        gboolean passed = FALSE;

        /* Invoke the action and start the timer */
        passed = initiate_action(graph, action);
        if (passed == FALSE) {
            crm_err("Failed initiating <%s id=%d> in synapse %d",
                    crm_element_name(action->xml), action->id, synapse->id);
            synapse->confirmed = TRUE;
            action->confirmed = TRUE;
            action->failed = TRUE;
            return FALSE;
        }
    }

    return TRUE;
}

int
run_graph(crm_graph_t * graph)
{
    GList *lpc = NULL;
    int stat_log_level = LOG_DEBUG;
    int pass_result = transition_active;

    const char *status = "In-progress";

    if (graph_fns == NULL) {
        set_default_graph_functions();
    }
    if (graph == NULL) {
        return transition_complete;
    }

    graph->fired = 0;
    graph->pending = 0;
    graph->skipped = 0;
    graph->completed = 0;
    graph->incomplete = 0;
    crm_trace("Entering graph %d callback", graph->id);

    /* Pre-calculate the number of completed and in-flight operations */
    for (lpc = graph->synapses; lpc != NULL; lpc = lpc->next) {
        synapse_t *synapse = (synapse_t *) lpc->data;

        if (synapse->confirmed) {
            crm_trace("Synapse %d complete", synapse->id);
            graph->completed++;

        } else if (synapse->failed == FALSE && synapse->executed) {
            crm_trace("Synapse %d: confirmation pending", synapse->id);
            graph->pending++;
        }
    }

    /* Now check if there is work to do */
    for (lpc = graph->synapses; lpc != NULL; lpc = lpc->next) {
        synapse_t *synapse = (synapse_t *) lpc->data;

        if (graph->batch_limit > 0 && graph->pending >= graph->batch_limit) {
            crm_debug("Throttling output: batch limit (%d) reached", graph->batch_limit);
            break;
        } else if (synapse->failed) {
            graph->skipped++;
            continue;

        } else if (synapse->confirmed || synapse->executed) {
            /* Already handled */
            continue;
        }

        if (should_fire_synapse(graph, synapse)) {
            crm_trace("Synapse %d fired", synapse->id);
            graph->fired++;
            if(fire_synapse(graph, synapse) == FALSE) {
                crm_err("Synapse %d failed to fire", synapse->id);
                stat_log_level = LOG_ERR;
                graph->abort_priority = INFINITY;
                graph->incomplete++;
                graph->fired--;
            }

            if (synapse->confirmed == FALSE) {
                graph->pending++;
            }

        } else {
            crm_trace("Synapse %d cannot fire", synapse->id);
            graph->incomplete++;
        }
    }

    if (graph->pending == 0 && graph->fired == 0) {
        graph->complete = TRUE;
        stat_log_level = LOG_NOTICE;
        pass_result = transition_complete;
        status = "Complete";

        if (graph->incomplete != 0 && graph->abort_priority <= 0) {
            stat_log_level = LOG_WARNING;
            pass_result = transition_terminated;
            status = "Terminated";

        } else if (graph->skipped != 0) {
            status = "Stopped";
        }

    } else if (graph->fired == 0) {
        pass_result = transition_pending;
    }

    do_crm_log(stat_log_level,
               "Transition %d (Complete=%d, Pending=%d,"
               " Fired=%d, Skipped=%d, Incomplete=%d, Source=%s): %s",
               graph->id, graph->completed, graph->pending, graph->fired,
               graph->skipped, graph->incomplete, graph->source, status);

    return pass_result;
}
