#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pwd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/acl.h>
#include <acl/libacl.h>

#define GREENSTONE_DIR "/Greenstone"
// #define DEBUG

static acl_entry_t
find_entry(
    acl_t acl,
    acl_tag_t type,
    id_t id)
{
    acl_entry_t ent;
    acl_tag_t e_type;
    id_t *e_id_p;

    if (acl_get_entry(acl, ACL_FIRST_ENTRY, &ent) != 1)
        return NULL;

    for(;;) {
        acl_get_tag_type(ent, &e_type);
        if (type == e_type) {
            if (id != ACL_UNDEFINED_ID) {
                e_id_p = acl_get_qualifier(ent);
                if (e_id_p == NULL)
                    return NULL;
                if (*e_id_p == id) {
                    acl_free(e_id_p);
                    return ent;
                }
                acl_free(e_id_p);
            } else {
                return ent;
            }
        }
        if (acl_get_entry(acl, ACL_NEXT_ENTRY, &ent) != 1)
            return NULL;
    }
}

static int
clone_entry(
    acl_t from_acl,
    acl_tag_t from_type,
    acl_t *to_acl,
    acl_tag_t to_type)
{
    acl_entry_t from_entry, to_entry;
    from_entry = find_entry(from_acl, from_type, ACL_UNDEFINED_ID);
    if (from_entry) {
        if (acl_create_entry(to_acl, &to_entry) != 0)
            return -1;
        acl_copy_entry(to_entry, from_entry);
        acl_set_tag_type(to_entry, to_type);
        return 0;
    } else {
        return 1;
    }
}

static int set_perms (acl_t *acl, int type, int uid, int perms) {
    acl_entry_t entry = find_entry (*acl, type, uid);
    if (!entry) {
        if (acl_create_entry(acl, &entry)) {
            perror ("acl_create_entry");
            return 1;
        }
        acl_set_tag_type(entry, type);
        acl_set_qualifier(entry, &uid);
    }
    acl_permset_t set;
    acl_get_permset(entry, &set);
    
    if (perms & 4) {
        acl_add_perm(set, ACL_READ);
    } else {
        acl_delete_perm(set, ACL_READ);
    }
    if (perms & 2) {
        acl_add_perm(set, ACL_WRITE);
    } else {
        acl_delete_perm(set, ACL_WRITE);
    }
    if (perms & 1) {
        acl_add_perm(set, ACL_EXECUTE);
    } else {
        acl_delete_perm(set, ACL_EXECUTE);
    }
    return 0;
}

static int check_acl (acl_t *acl) {
#ifdef DEBUG
    int which_entry;
    int error;
    if ((error = acl_check(*acl, &which_entry))) {
        if (error > 0) {
            fprintf (stderr,
                    "Malformed ACL '%s': %s at entry %d\n",
                    acl_to_any_text(*acl, NULL, ',', 0),
                    acl_error(error), which_entry + 1
            );
        }
#else
    if (acl_check(*acl, NULL)) {
        perror ("acl_check");
#endif
        return 1;
    }
    return 0;
}

static int set_acl (char *fpath, int user_uid, int greenstone_gid) {
    acl_t default_acl = acl_get_file (fpath, ACL_TYPE_DEFAULT);
    acl_t acl = acl_get_file (fpath, ACL_TYPE_ACCESS);
    if (!default_acl || !acl) {
        perror (fpath);
        return 1;
    }
    int error = 0;

    set_perms (&default_acl, ACL_USER, user_uid, 07);
    set_perms (&acl, ACL_USER, user_uid, 07);
    set_perms (&default_acl, ACL_GROUP, greenstone_gid, 07);
    set_perms (&acl, ACL_GROUP, greenstone_gid, 07);

    /* Try to fill in missing entries */
    if (!find_entry(default_acl, ACL_USER_OBJ, ACL_UNDEFINED_ID)) {
        clone_entry(acl, ACL_USER_OBJ, &default_acl, ACL_USER_OBJ);
    }
    if (!find_entry(default_acl, ACL_GROUP_OBJ, ACL_UNDEFINED_ID)) {
        clone_entry(acl, ACL_GROUP_OBJ, &default_acl, ACL_GROUP_OBJ);
    }
    if (!find_entry(default_acl, ACL_OTHER, ACL_UNDEFINED_ID)) {
        clone_entry(acl, ACL_OTHER, &default_acl, ACL_OTHER);
    }

    if (acl_equiv_mode (acl, NULL) != 0) {
        if (!find_entry (acl, ACL_MASK, ACL_UNDEFINED_ID)) {
            clone_entry (acl, ACL_GROUP_OBJ, &acl, ACL_MASK);
        }
        acl_calc_mask(&acl);
    }
    if (acl_equiv_mode (default_acl, NULL) != 0) {
        if (!find_entry (default_acl, ACL_MASK, ACL_UNDEFINED_ID)) {
            clone_entry (default_acl, ACL_GROUP_OBJ, &default_acl, ACL_MASK);
        }
        acl_calc_mask(&default_acl);
    }

    if (check_acl (&acl) || check_acl (&default_acl)) {
        goto acl_error;
    }

    if (acl_set_file (fpath, ACL_TYPE_DEFAULT, default_acl) ||
        acl_set_file (fpath, ACL_TYPE_ACCESS, acl)) {
        perror ("acl_set_file");
        goto acl_error;
    }

acl_cleanup:
    if (acl_free (acl) || acl_free (default_acl)) {
        perror ("acl_free");
    }
    return error;
acl_error:
    error = 1;
    goto acl_cleanup;
}


int main(int argc, char **argv) {
    if (argc < 2) {
        fprintf (stderr, "Usage: gs-mkdir <username> [...]\n");
        return 1;
    }
    int greenstone_gid;
    {
        struct stat greenstone_user;
        if (stat (argv[0], &greenstone_user)) {
            perror (argv[0]);
            return 1;
        }
        greenstone_gid = greenstone_user.st_gid;
    }

    int i;
    for (i = 1; i < argc; i++) {
        /* Ensure the user exists */
        struct passwd *user = getpwnam(argv[i]);
        if (!user) {
            fprintf (stderr, "User '%s' does not exist\n", argv[i]);
            return 1;
        }

        char *path = user->pw_dir;
        struct stat st;
        /* Ensure the user home folder exists */
        if (stat (path, &st)) {
            perror (path);
            return 1;
        }
        int path_len = strlen (path);
        char *full_path = malloc (path_len + strlen (GREENSTONE_DIR) + 1);
        memcpy (full_path, path, path_len);
        memcpy (full_path + path_len, GREENSTONE_DIR, strlen (GREENSTONE_DIR));
        full_path[path_len + strlen (GREENSTONE_DIR)] = 0;

        /* Check if the user's greenstone folder exists */
        if (stat (full_path, &st)) {
            if (errno == ENOENT) {
                /* Create the folder as it doesn't exist */
                if (mkdir (full_path, 0770)) {
                    perror (full_path);
                    return 1;
                }
                chown (full_path, user->pw_uid, user->pw_gid);

                if (set_acl (full_path, user->pw_uid, greenstone_gid)) {
                    return 1;
                }
            } else {
                /* Something went wrong */
                perror (full_path);
                return 1;
            }
        }
        
        puts (full_path);
        free (full_path);
    }
    return 0;
}
