/******************************************************************************
 * Copyright (c) Huawei Technologies Co., Ltd. 2020. All rights reserved.
 * iSulad licensed under the Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *     http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
 * PURPOSE.
 * See the Mulan PSL v2 for more details.
 * Author: lifeng
 * Create: 2020-04-02
 * Description: provide quota function definition
 ******************************************************************************/
#include "project_quota.h"

#include <pthread.h>
#include <dirent.h>
#include <fcntl.h>
#include <limits.h>
#include <linux/dqblk_xfs.h>
#include <linux/quota.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/quota.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "utils.h"
#include "isula_libutils/log.h"

// define for quotactl commands
#define PDQ_ACCT_BIT 4 // 4 means project quota accounting ops
#define PDQ_ENFD_BIT 5 // 5 means project quota limits enforcement

// make device of the driver home directory
static char *make_backing_fs_device(const char *home_dir)
{
    int ret = 0;
    char full_path[PATH_MAX] = { 0 };
    struct stat current_stat = { 0 };

    ret = snprintf(full_path, PATH_MAX, "%s/%s", home_dir, "backingFsBlockDev");
    if (ret < 0 || ret >= PATH_MAX) {
        ERROR("Failed to get backing fs device");
        goto err_out;
    }

    ret = stat(home_dir, &current_stat);
    if (ret) {
        SYSERROR("get %s state failed", home_dir);
        goto err_out;
    }

    unlink(full_path);
    ret = mknod(full_path, S_IFBLK | S_IRUSR | S_IWUSR, current_stat.st_dev);
    if (ret != 0) {
        SYSERROR("Failed to mknod %s", full_path);
        goto err_out;
    }

    return util_strdup_s(full_path);

err_out:
    return NULL;
}

static int set_project_quota_id(const uint32_t projectid, const char *target)
{
    int ret = 0;
    struct fsxattr fsxattr_for_prjid = { 0 };
    DIR *dir = NULL;
    int fd = -1;

    dir = opendir(target);
    if (dir == NULL) {
        ret = -1;
        SYSERROR("opendir with path %s failed", target);
        goto out;
    }

    fd = dirfd(dir);
    if (fd < 0) {
        ret = -1;
        SYSERROR("open %s failed.", target);
        goto out;
    }

    ret = ioctl(fd, FS_IOC_FSGETXATTR, &fsxattr_for_prjid);
    if (ret != 0) {
        SYSERROR("failed to get projid for %s", target);
        goto out;
    }

    fsxattr_for_prjid.fsx_projid = projectid;
    fsxattr_for_prjid.fsx_xflags |= FS_XFLAG_PROJINHERIT;
    ret = ioctl(fd, FS_IOC_FSSETXATTR, &fsxattr_for_prjid);
    if (ret != 0) {
        SYSERROR("failed to set projid for %s", target);
        goto out;
    }

out:
    if (dir != NULL) {
        closedir(dir);
    }
    return ret;
}

static int ext4_set_project_quota(const char *backing_fs_blockdev, uint32_t project_id, uint64_t size)
{
    int ret;
    struct dqblk d = { 0 };
    d.dqb_bhardlimit = size / SIZE_KB;
    d.dqb_bsoftlimit = d.dqb_bhardlimit;
    d.dqb_valid = QIF_LIMITS;

    ret = quotactl(QCMD(Q_SETQUOTA, FS_PROJ_QUOTA), backing_fs_blockdev, project_id, (caddr_t)&d);
    if (ret != 0) {
        SYSERROR("Failed to set quota limit for projid %d on %s", project_id, backing_fs_blockdev);
    }
    return ret;
}

static int ext4_set_quota(const char *target, struct pquota_control *ctrl, uint64_t size)
{
    int ret = 0;
    uint32_t project_id = 0;

    if (target == NULL || ctrl == NULL) {
        return -1;
    }

    if (pthread_rwlock_wrlock(&(ctrl->rwlock)) != 0) {
        SYSERROR("Failed to get rwlock in set_quota");
        ret = -1;
        goto out;
    }

    project_id = ctrl->next_project_id;
    if (set_project_quota_id(project_id, target) != 0) {
        ERROR("Failed to set project id %d to %s.", project_id, target);
        ret = -1;
        goto unlock;
    }
    ctrl->next_project_id++;

    EVENT("Set directory %s project ID:%u quota size: %lu", target, project_id, size);

    if (ext4_set_project_quota(ctrl->backing_fs_device, project_id, size) != 0) {
        ERROR("Failed to set project id %d to %s.", project_id, target);
        ret = -1;
        goto unlock;
    }

unlock:
    (void)pthread_rwlock_unlock(&(ctrl->rwlock));
out:
    return ret;
}

static int xfs_set_project_quota(const char *backing_fs_blockdev, uint32_t project_id, uint64_t size)
{
    int ret;
    fs_disk_quota_t d = { 0 };
    d.d_version = FS_DQUOT_VERSION;
    d.d_id = project_id;
    d.d_flags = FS_PROJ_QUOTA;
    d.d_fieldmask = FS_DQ_BHARD | FS_DQ_BSOFT;
    d.d_blk_hardlimit = (size / 512);
    d.d_blk_softlimit = d.d_blk_hardlimit;

    ret = quotactl(QCMD(Q_XSETQLIM, FS_PROJ_QUOTA), backing_fs_blockdev, project_id, (caddr_t)&d);
    if (ret != 0) {
        SYSERROR("Failed to set quota limit for projid %d on %s", project_id, backing_fs_blockdev);
    }
    return ret;
}

static int xfs_set_quota(const char *target, struct pquota_control *ctrl, uint64_t size)
{
    int ret = 0;
    uint32_t project_id = 0;

    if (target == NULL || ctrl == NULL) {
        return -1;
    }

    if (pthread_rwlock_wrlock(&(ctrl->rwlock)) != 0) {
        SYSERROR("Failed to get rwlock in set_quota");
        ret = -1;
        goto out;
    }

    project_id = ctrl->next_project_id;
    if (set_project_quota_id(project_id, target) != 0) {
        ERROR("Failed to set project id %d to %s.", project_id, target);
        ret = -1;
        goto unlock;
    }
    ctrl->next_project_id++;

    EVENT("Set directory %s project ID:%u quota size: %lu", target, project_id, size);

    if (xfs_set_project_quota(ctrl->backing_fs_device, project_id, size) != 0) {
        ERROR("Failed to set project id %d to %s.", project_id, target);
        ret = -1;
        goto unlock;
    }

unlock:
    (void)pthread_rwlock_unlock(&(ctrl->rwlock));
out:
    return ret;
}

static int get_project_quota_id(const char *path, uint32_t *project_id)
{
    int ret = 0;
    DIR *dir = NULL;
    int fd = -1;
    struct fsxattr fsxattr = { 0 };

    dir = opendir(path);
    if (dir == NULL) {
        ret = -1;
        SYSERROR("opendir with path %s failed", path);
        goto out;
    }
    fd = dirfd(dir);
    if (fd < 0) {
        ret = -1;
        SYSERROR("open %s failed.", path);
        goto out;
    }
    ret = ioctl(fd, FS_IOC_FSGETXATTR, &fsxattr);
    if (ret != 0) {
        SYSERROR("failed to get projid for %s", path);
        goto out;
    }

    *project_id = (uint32_t)fsxattr.fsx_projid;
out:
    if (dir != NULL) {
        closedir(dir);
    }
    return ret;
}

static void get_next_project_id(const char *dirpath, struct pquota_control *ctrl)
{
    int nret = 0;
    struct dirent *pdirent = NULL;
    DIR *directory = NULL;
    char fname[PATH_MAX];

    directory = opendir(dirpath);
    if (directory == NULL) {
        ERROR("Failed to open %s", dirpath);
        return;
    }
    pdirent = readdir(directory);
    for (; pdirent != NULL; pdirent = readdir(directory)) {
        struct stat fstat;
        int pathname_len;
        uint32_t project_id = 0;

        if (!strcmp(pdirent->d_name, ".") || !strcmp(pdirent->d_name, "..")) {
            continue;
        }

        (void)memset(fname, 0, sizeof(fname));

        pathname_len = snprintf(fname, PATH_MAX, "%s/%s", dirpath, pdirent->d_name);
        if (pathname_len < 0 || pathname_len >= PATH_MAX) {
            ERROR("Pathname too long");
            continue;
        }

        nret = lstat(fname, &fstat);
        if (nret != 0) {
            ERROR("get_next_project_id failed to stat %s", fname);
            continue;
        }

        if (!S_ISDIR(fstat.st_mode)) {
            continue;
        }

        if (get_project_quota_id(fname, &project_id) != 0) {
            ERROR("Failed to get %s project id", fname);
            continue;
        }
        if (ctrl->next_project_id <= project_id) {
            ctrl->next_project_id = project_id + 1;
        }
    }

    nret = closedir(directory);
    if (nret) {
        ERROR("Failed to close directory %s", dirpath);
    }

    return;
}

void free_pquota_control(struct pquota_control *ctrl)
{
    if (ctrl == NULL) {
        return;
    }

    free(ctrl->backing_fs_type);
    ctrl->backing_fs_type = NULL;

    free(ctrl->backing_fs_device);
    ctrl->backing_fs_device = NULL;

    if (pthread_rwlock_destroy(&(ctrl->rwlock)) != 0) {
        SYSERROR("destory pquota_control rwlock failed");
    }
    free(ctrl);
}

static int get_quota_stat(const char *backing_fs_blockdev)
{
    int ret = 0;
    int nret = 0;
    fs_quota_stat_t fs_quota_stat_info = { 0 };

    ret = quotactl(QCMD(Q_XGETQSTAT, FS_PROJ_QUOTA), backing_fs_blockdev, 0, (caddr_t)&fs_quota_stat_info);
    if (ret != 0) {
        SYSERROR("Failed to get quota stat on %s", backing_fs_blockdev);
        return ret;
    }

    nret = ((fs_quota_stat_info.qs_flags & FS_QUOTA_PDQ_ACCT) >> PDQ_ACCT_BIT) +
           ((fs_quota_stat_info.qs_flags & FS_QUOTA_PDQ_ENFD) >> PDQ_ENFD_BIT);
    if (nret == FS_PROJ_QUOTA) { // return FS_PROJ_QUOTA(2) means project quota is on
        ret = 0;
    } else {
        ret = -1;
    }

    return ret;
}

static bool fs_support_quota(const char *fs)
{
    if (fs == NULL) {
        return false;
    }

    return (strcmp(fs, "xfs") == 0 || strcmp(fs, "extfs") == 0);
}

struct pquota_control *project_quota_control_init(const char *home_dir, const char *fs)
{
    int ret = 0;
    struct pquota_control *ctrl = NULL;
    uint32_t min_project_id = 0;

    if (home_dir == NULL || fs == NULL) {
        ERROR("Invalid input auguments");
        goto err_out;
    }

    if (!fs_support_quota(fs)) {
        ERROR("quota isn't supported for filesystem %s", fs);
        goto err_out;
    }

    ctrl = util_common_calloc_s(sizeof(struct pquota_control));
    if (ctrl == NULL) {
        ERROR("out of memory");
        goto err_out;
    }

    ret = pthread_rwlock_init(&(ctrl->rwlock), NULL);
    if (ret) {
        SYSERROR("init project quota rwlock failed");
        goto err_out;
    }

    ret = get_project_quota_id(home_dir, &min_project_id);
    if (ret) {
        ERROR("Failed to get mininal project id %s", home_dir);
        goto err_out;
    }
    min_project_id++;
    ctrl->next_project_id = min_project_id;
    get_next_project_id(home_dir, ctrl);

    ctrl->backing_fs_device = make_backing_fs_device(home_dir);
    if (ctrl->backing_fs_device == NULL) {
        ERROR("Failed to make backing fs device %s", home_dir);
        goto err_out;
    }

    if (get_quota_stat(ctrl->backing_fs_device) != 0) {
        ERROR("quota isn't supported on your system %s", home_dir);
        goto err_out;
    }

    ctrl->backing_fs_type = util_strdup_s(fs);

    if (strcmp(ctrl->backing_fs_type, "extfs") == 0) {
        ctrl->set_quota = ext4_set_quota;
    } else {
        ctrl->set_quota = xfs_set_quota;
    }

    return ctrl;

err_out:
    free_pquota_control(ctrl);
    return NULL;
}
