#include "slapi-plugin.h"
#include "nspr.h"
#include <string.h>

#include "posix-wsp-ident.h"
#include "posix-group-func.h"

typedef struct _task_data
{
    char *dn; /* search base */
    char *filter_str; /* search filter */
} task_data;

typedef struct _cb_data
{
    char *dn;
    void *txn;
} cb_data;
/*
 typedef struct _posix_group_task_data
 {
 POSIX_WinSync_Config *config;
 Slapi_Value *memberdn_val;
 Slapi_ValueSet **uidvals;
 void *txn;
 } posix_group_data_data;
 */

Slapi_Value **
valueset_get_valuearray(const Slapi_ValueSet *vs); /* stolen from proto-slap.h */

/* interface function */
int
posix_group_task_add(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eAfter, int *returncode,
    char *returntext, void *arg);

Slapi_Entry *
getEntry(const char *udn, char **attrs);

static void
posix_group_task_destructor(Slapi_Task *task);
static void
posix_group_fixup_task_thread(void *arg);
static int
posix_group_fix_memberuid_callback(Slapi_Entry *e, void *callback_data);

/* extract a single value from the entry (as a string) -- if it's not in the
 * entry, the default will be returned (which can be NULL).
 * you do not need to free anything returned by this.
 */
static const char *
fetch_attr(Slapi_Entry *e, const char *attrname, const char *default_val)
{
    Slapi_Attr *attr;
    Slapi_Value *val = NULL;

    if (slapi_entry_attr_find(e, attrname, &attr) != 0)
        return default_val;
    slapi_attr_first_value(attr, &val);
    return slapi_value_get_string(val);
}

/* e configEntry */
int
posix_group_task_add(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eAfter, int *returncode,
    char *returntext, void *arg)
{
    PRThread *thread = NULL;
    int rv = SLAPI_DSE_CALLBACK_OK;
    task_data *mytaskdata = NULL;
    Slapi_Task *task = NULL;
    const char *filter;
    const char *dn = 0;

    *returncode = LDAP_SUCCESS;


    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_task_add: ==>\n");

    /* get arg(s) */
    /* default: set replication basedn */
    if ((dn = fetch_attr(e, "basedn", slapi_sdn_get_dn(posix_winsync_config_get_suffix()))) == NULL) {
        *returncode = LDAP_OBJECT_CLASS_VIOLATION;
        rv = SLAPI_DSE_CALLBACK_ERROR;
        goto out;
    }

    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_task_add: retrieved basedn: %s\n", dn);

    if ((filter = fetch_attr(e, "filter", "(&(objectclass=ntGroup)(|(uniquemember=*)(memberuid=*)))")) == NULL) {
        *returncode = LDAP_OBJECT_CLASS_VIOLATION;
        rv = SLAPI_DSE_CALLBACK_ERROR;
        goto out;
    }

    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_task_add: retrieved filter: %s\n", filter);

    /* setup our task data */
    mytaskdata = (task_data*) slapi_ch_malloc(sizeof(task_data));
    if (mytaskdata == NULL) {
        *returncode = LDAP_OPERATIONS_ERROR;
        rv = SLAPI_DSE_CALLBACK_ERROR;
        goto out;
    }
    mytaskdata->dn = slapi_ch_strdup(dn);
    mytaskdata->filter_str = slapi_ch_strdup(filter);

    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_task_add: task data allocated\n");

    /* allocate new task now */
    char * ndn = slapi_entry_get_ndn(e);

    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_task_add: creating task object: %s\n",
                    ndn);

    task = slapi_new_task(ndn);

    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_task_add: task object created\n");

    /* register our destructor for cleaning up our private data */
    slapi_task_set_destructor_fn(task, posix_group_task_destructor);

    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_task_add: task destructor set\n");

    /* Stash a pointer to our data in the task */
    slapi_task_set_data(task, mytaskdata);

    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_task_add: task object initialized\n");

    /* start the sample task as a separate thread */
    thread = PR_CreateThread(PR_USER_THREAD, posix_group_fixup_task_thread, (void *) task,
                             PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_UNJOINABLE_THREAD,
                             SLAPD_DEFAULT_THREAD_STACKSIZE);

    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_task_add: thread created\n");

    if (thread == NULL) {
        slapi_log_error(SLAPI_LOG_FATAL, POSIX_WINSYNC_PLUGIN_NAME,
                        "unable to create task thread!\n");
        *returncode = LDAP_OPERATIONS_ERROR;
        rv = SLAPI_DSE_CALLBACK_ERROR;
        slapi_task_finish(task, *returncode);
    } else {
        rv = SLAPI_DSE_CALLBACK_OK;
    }

out: 
    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_task_add: <==\n");

    return rv;
}

static void
posix_group_task_destructor(Slapi_Task *task)
{
    if (task) {
        task_data *mydata = (task_data *) slapi_task_get_data(task);
        while (slapi_task_get_refcount(task) > 0) {
            /* Yield to wait for the fixup task finishes. */
            DS_Sleep (PR_MillisecondsToInterval(100));
        }
        if (mydata) {
            slapi_ch_free_string(&mydata->dn);
            slapi_ch_free_string(&mydata->filter_str);
            /* Need to cast to avoid a compiler warning */
            slapi_ch_free((void **) &mydata);
        }
    }
    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_task_destructor <--\n");
}

#if 0 /* NOT USED */
static int
posix_group_del_memberuid_callback(Slapi_Entry *e, void *callback_data)
{
    int rc = 0;
    LDAPMod mod;
    LDAPMod *mods[2];
    char *val[2];
    Slapi_PBlock *mod_pb = 0;
    cb_data *the_cb_data = (cb_data *) callback_data;

    mod_pb = slapi_pblock_new();

    mods[0] = &mod;
    mods[1] = 0;

    val[0] = 0; /* all */
    val[1] = 0;

    mod.mod_op = LDAP_MOD_DELETE;
    mod.mod_type = "memberuid";
    mod.mod_values = val;

    slapi_modify_internal_set_pb_ext(mod_pb, slapi_entry_get_sdn(e), mods, 0, 0,
                                     posix_winsync_get_plugin_identity(), 0);

    slapi_pblock_set(mod_pb, SLAPI_TXN, the_cb_data->txn);
    slapi_modify_internal_pb(mod_pb);

    slapi_pblock_get(mod_pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);

    slapi_pblock_destroy(mod_pb);

    return rc;
}
#endif

static int
posix_group_fix_memberuid(char *dn, char *filter_str, void *txn)
{
    int rc = 0;
    struct _cb_data callback_data = { dn, txn };
    Slapi_PBlock *search_pb = slapi_pblock_new();

    /* char *attrs[]={"uniquemember","memberuid",NULL}; */

    slapi_search_internal_set_pb(search_pb, dn, LDAP_SCOPE_SUBTREE, filter_str, 0, 0, 0, 0,
                                 posix_winsync_get_plugin_identity(), 0);

    slapi_pblock_set(search_pb, SLAPI_TXN, txn); /* set transaction id */
    rc = slapi_search_internal_callback_pb(search_pb, &callback_data, 0,
                                           posix_group_fix_memberuid_callback, 0);

    slapi_pblock_destroy(search_pb);

    return rc;
}

/* posix_group_fix_memberuid_callback()
 * Add initial and/or fix up broken group list in entry
 *
 * 1. forall uniquemember search if posixAccount ? add uid : ""
 */
static int
posix_group_fix_memberuid_callback(Slapi_Entry *e, void *callback_data)
{
    int i;
    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "_fix_memberuid ==>\n");
    cb_data *the_cb_data = (cb_data *) callback_data;

    int rc = 0;
    Slapi_Attr *muid_attr = NULL;
    Slapi_Value *v = NULL;

    Slapi_Mods *smods = NULL;
    char *dn = NULL;
    Slapi_DN *sdn = NULL;
    LDAPMod **mods = NULL;
    int is_posix_group = 0;

    /* 
     * If the server is ordered to shutdown, stop the fixup and return an error.
     */
    if (slapi_is_shutting_down()) {
        rc = -1;
        goto bail;
    }

    smods = slapi_mods_new();
    dn = slapi_entry_get_dn(e);
    sdn = slapi_entry_get_sdn(e);

    if (hasObjectClass(e, "posixGroup")) {
        is_posix_group = 1;
    }
/* Clean out memberuids and dsonlymemberuids without a valid referant */
    rc = slapi_entry_attr_find(e, "memberuid", &muid_attr);
    if (rc == 0 && muid_attr) {
        Slapi_PBlock *search_pb = slapi_pblock_new();

        Slapi_Attr *dsmuid_attr = NULL;
        Slapi_ValueSet *dsmuid_vs = NULL;

        char *attrs[] = { "uid", NULL };

        rc = slapi_entry_attr_find(e, "dsonlymemberuid", &dsmuid_attr);
        if (rc == 0 && dsmuid_attr) {
            slapi_attr_get_valueset(dsmuid_attr, &dsmuid_vs);
        }

        slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                        "_fix_memberuid scan for orphaned memberuids\n");

        for (i = slapi_attr_first_value(muid_attr, &v); i != -1;
             i = slapi_attr_next_value(muid_attr, i, &v)) {
            char *muid = (char *)slapi_value_get_string(v);

            slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                            "_fix_memberuid iterating memberuid: %s\n",
                            muid);

            size_t vallen = muid ? strlen(muid) : 0;
            char *filter_escaped_value = slapi_escape_filter_value(muid, vallen);
            char *filter = slapi_ch_smprintf("(uid=%s)", filter_escaped_value);
            slapi_ch_free_string(&filter_escaped_value);

            Slapi_Entry **search_entries = NULL;

            slapi_search_internal_set_pb(search_pb,
                                         the_cb_data->dn,
                                         LDAP_SCOPE_SUBTREE,
                                         filter,
                                         attrs, 0, NULL, NULL,
                                         posix_winsync_get_plugin_identity(), 0);

            slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                            "_fix_memberuid searching %s with filter: %s\n",
                            the_cb_data->dn, filter);

            rc = slapi_search_internal_pb(search_pb);

            slapi_pblock_get(search_pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &search_entries);

            if (!search_entries || !search_entries[0]) {
                slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                                "_fix_memberuid Adding bad memberuid %s\n",
                                slapi_value_get_string(v));

                slapi_mods_add_string(smods, LDAP_MOD_DELETE, "memberuid", slapi_value_get_string(v));

                if (dsmuid_vs && slapi_valueset_find(dsmuid_attr, dsmuid_vs, v)) {
                    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                                    "_fix_memberuid Adding bad dsonlymemberuid %s\n",
                                    slapi_value_get_string(v));

                    slapi_mods_add_string(smods, LDAP_MOD_DELETE, "dsonlymemberuid", slapi_value_get_string(v));
                }
            }

            slapi_free_search_results_internal(search_pb);
            slapi_pblock_init(search_pb);
            slapi_ch_free_string(&filter);
        }

        if (dsmuid_vs) {
            slapi_valueset_free(dsmuid_vs); dsmuid_vs = NULL;
        }

        slapi_pblock_destroy(search_pb); search_pb = NULL;
    }

    /* Cleanup uniquemembers without a referent, and verify memberuid otherwise */
    Slapi_Attr *obj_attr = NULL;
    rc = slapi_entry_attr_find(e, "uniquemember", &obj_attr);
    if (rc == 0 && obj_attr) {
        int fixMembership = 0;
        Slapi_ValueSet *bad_ums = NULL;
        Slapi_Value *uniqval = NULL;   /* uniquemeber Attribute values */
        Slapi_ValueSet *uids = NULL;
        slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                        "_fix_memberuid scan uniquemember, group %s\n", dn);
        for (i = slapi_attr_first_value(obj_attr, &uniqval); i != -1;
             i = slapi_attr_next_value(obj_attr, i, &uniqval)) {

            const char *member = slapi_value_get_string(uniqval);
            char *attrs[] = { "uid", "objectclass", NULL };
            Slapi_Entry *child = getEntry(member, attrs);

            if (child) {
                slapi_entry_free(child);
            } else {
                slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                                "_fix_memberuid orphaned uniquemember found: %s\n", member);

                if ((strncasecmp(member, "cn=", 3) == 0) ||
                    (strncasecmp(member, "uid=", 4) == 0)) {
                    fixMembership = 1;
                }
                if (!bad_ums) {
                    bad_ums = slapi_valueset_new();
                }
                slapi_valueset_add_value(bad_ums, uniqval);
            }

            if (is_posix_group) {
                char *uid = NULL;
                /* search uid for member (DN) */
                slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME, "search %s\n", member);
                if ((uid = searchUid(member)) != NULL) {
                    Slapi_Value *value = slapi_value_new();
                    /* Search an entry having "member" as DN and get uid value from it. */
                    slapi_value_set_string_passin(value, uid);
                    /* add uids ValueSet */
                    if (NULL == uids) {
                        uids = slapi_valueset_new();
                    }
                    slapi_valueset_add_value(uids, value);
                    slapi_value_free(&value);
                }
            }
        }
        /* If we found some posix members, replace the existing memberuid attribute
         * with the found values.  */
        if (uids && slapi_valueset_count(uids)) {
            Slapi_Value *val = 0;
            Slapi_Mod *smod = slapi_mod_new();
            int hint = 0;

            slapi_mod_init(smod, 0);
            slapi_mod_set_operation(smod, LDAP_MOD_REPLACE | LDAP_MOD_BVALUES);
            slapi_mod_set_type(smod, "memberuid");

            /* Loop through all of our values and add them to smod */
            hint = slapi_valueset_first_value(uids, &val);
            while (val) {
                /* this makes a copy of the berval */
                slapi_mod_add_value(smod, slapi_value_get_berval(val));
                hint = slapi_valueset_next_value(uids, hint, &val);
            }
            slapi_mods_add_ldapmod(smods, slapi_mod_get_ldapmod_passout(smod));
            slapi_mod_free(&smod);
        }
        slapi_valueset_free(uids);

        slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                        "_fix_memberuid Finishing...\n");

        if (fixMembership && posix_winsync_config_get_mapNestedGrouping()) {
            Slapi_ValueSet *del_nested_vs = slapi_valueset_new();

            slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                            "_fix_memberuid group deleted, recalculating nesting\n");
            propogateDeletionsUpward(e, sdn, bad_ums, del_nested_vs, 0);

            slapi_valueset_free(del_nested_vs); del_nested_vs = NULL;
        }

        if (bad_ums) {
            slapi_mods_add_mod_values(smods, LDAP_MOD_DELETE, "uniquemember", valueset_get_valuearray(bad_ums));
            slapi_valueset_free(bad_ums); bad_ums = NULL;
        }
    }

    mods = slapi_mods_get_ldapmods_byref(smods);
    if (mods) {
        Slapi_PBlock *mod_pb = NULL;
        mod_pb = slapi_pblock_new();
        slapi_modify_internal_set_pb_ext(mod_pb, sdn, mods, 0, 0,
                                        posix_winsync_get_plugin_identity(), 0);

        slapi_pblock_set(mod_pb, SLAPI_TXN, the_cb_data->txn);
        slapi_modify_internal_pb(mod_pb);

        slapi_pblock_get(mod_pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
        slapi_pblock_destroy(mod_pb);
    }
    slapi_mods_free(&smods);
bail:
    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "_fix_memberuid <==\n");
    /*
     * Since Ticket #481 "expand nested posix groups",
     * there's a possibility the found entry does not contain
     * uniqueMember attribute.  But "not found" error shoud not
     * be returned, which stops the further fixup task.
     */
    return rc;
}

static void
posix_group_fixup_task_thread(void *arg)
{
    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "_task_thread ==>\n");

    Slapi_Task *task = (Slapi_Task *) arg;
    task_data *td = NULL;
    int rc = 0;

    if (!task) {
        return; /* no task */
    }
    slapi_task_inc_refcount(task);
    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_fixup_task_thread --> refcount incremented.\n" );
    /* Fetch our task data from the task */
    td = (task_data *) slapi_task_get_data(task);

    slapi_task_begin(task, 1);
    slapi_task_log_notice(task, "posix_group task starts (arg: %s) ...\n", td->filter_str);

    /* get the memberOf operation lock */
    memberUidLock();

    /* do real work */
    rc = posix_group_fix_memberuid(td->dn, td->filter_str, NULL /* no txn? */);

    /* release the memberOf operation lock */
    memberUidUnlock();
    

    slapi_task_log_notice(task, "posix_group task finished.");
    slapi_task_log_status(task, "posix_group task finished.");
    slapi_task_inc_progress(task);

    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "_task_thread finishing\n");

    /* this will queue the destruction of the task */
    slapi_task_finish(task, rc);

    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "_task_thread <==\n");
    slapi_task_dec_refcount(task);
    slapi_log_error(SLAPI_LOG_PLUGIN, POSIX_WINSYNC_PLUGIN_NAME,
                    "posix_group_fixup_task_thread <-- refcount decremented.\n");
}
