#include "auth.h"
#include "hex.h"
#include "jwt.h"
#include <libweb/http.h>
#include <cjson/cJSON.h>
#include <dynstr.h>
#include <openssl/sha.h>
#include <errno.h>
#include <limits.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

struct auth
{
    struct dynstr dir, db;
};

enum {KEYLEN = 32};

static char *dump_db(const char *const path)
{
    char *ret = NULL;
    FILE *f = NULL;
    struct stat sb;

    if (stat(path, &sb))
    {
        fprintf(stderr, "%s: stat(2): %s\n", __func__, strerror(errno));
        goto end;
    }
    else if (sb.st_size > SIZE_MAX - 1)
    {
        fprintf(stderr, "%s: %s too big (%llu bytes, %zu max)\n",
            __func__, path, (unsigned long long)sb.st_size, (size_t)SIZE_MAX);
        goto end;
    }
    else if (!(f = fopen(path, "rb")))
    {
        fprintf(stderr, "%s: fopen(3): %s\n", __func__, strerror(errno));
        goto end;
    }
    else if (!(ret = malloc(sb.st_size + 1)))
    {
        fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno));
        goto end;
    }
    else if (!fread(ret, sb.st_size, 1, f))
    {
        fprintf(stderr, "%s: failed to dump %zu bytes, ferror=%d\n",
            __func__, (size_t)sb.st_size, ferror(f));
        goto end;
    }

    ret[sb.st_size] = '\0';

end:

    if (f && fclose(f))
    {
        fprintf(stderr, "%s: fclose(3): %s\n", __func__, strerror(errno));
        free(ret);
        return NULL;
    }

    return ret;
}

static int find_cookie(const cJSON *const users, const char *const cookie)
{
    const cJSON *u;

    cJSON_ArrayForEach(u, users)
    {
        const cJSON *const n = cJSON_GetObjectItem(u, "name"),
            *const k = cJSON_GetObjectItem(u, "key");
        const char *name, *key;
        unsigned char dkey[KEYLEN];

        if (!n || !(name = cJSON_GetStringValue(n)))
        {
            fprintf(stderr, "%s: missing username\n", __func__);
            return -1;
        }
        else if (!k || !(key = cJSON_GetStringValue(k)))
        {
            fprintf(stderr, "%s: missing key\n", __func__);
            return -1;
        }
        else if (hex_decode(key, dkey, sizeof dkey))
        {
            fprintf(stderr, "%s: hex_decode failed\n", __func__);
            return -1;
        }

        const int res = jwt_check(cookie, dkey, sizeof dkey);

        if (!res)
            return 0;
        if (res < 0)
        {
            fprintf(stderr, "%s: jwt_decode failed\n", __func__);
            return -1;
        }
    }

    return 1;
}

int auth_cookie(const struct auth *const a, const struct http_cookie *const c)
{
    int ret = -1;
    char *db = NULL;
    cJSON *json = NULL;

    if (!c->field || !c->value)
        return 1;
    else if (!(db = dump_db(a->db.str)))
    {
        fprintf(stderr, "%s: dump_db failed\n", __func__);
        goto end;
    }
    else if (!(json = cJSON_Parse(db)))
    {
        fprintf(stderr, "%s: cJSON_Parse failed\n", __func__);
        goto end;
    }

    const cJSON *const users = cJSON_GetObjectItem(json, "users");

    if (!users)
    {
        fprintf(stderr, "%s: could not find users\n", __func__);
        goto end;
    }
    else if (!cJSON_IsArray(users))
    {
        fprintf(stderr, "%s: expected JSON array for users\n", __func__);
        goto end;
    }
    else if ((ret = find_cookie(users, c->value)) < 0)
    {
        fprintf(stderr, "%s: find_cookie failed\n", __func__);
        goto end;
    }

end:
    free(db);
    cJSON_Delete(json);
    return ret;
}

static int generate_cookie(const cJSON *const json, const char *const path,
    const char *const name, const char *const key, char **const cookie)
{
    unsigned char dkey[KEYLEN];
    int ret = -1;
    char *jwt = NULL;

    if (hex_decode(key, dkey, sizeof dkey))
    {
        fprintf(stderr, "%s: hex_decode failed\n", __func__);
        goto end;
    }
    else if (!(jwt = jwt_encode(name, dkey, sizeof dkey)))
    {
        fprintf(stderr, "%s: jwt_encode failed\n", __func__);
        goto end;
    }
    else if (!(*cookie = http_cookie_create(name, jwt)))
    {
        fprintf(stderr, "%s: http_cookie_create failed\n", __func__);
        goto end;
    }

    ret = 0;

end:
    free(jwt);
    return ret;
}

static int compare_pwd(const char *const salt, const char *const password,
    const char *const exp_password)
{
    int ret = -1;
    enum {SALT_LEN = SHA256_DIGEST_LENGTH};
    unsigned char dec_salt[SALT_LEN], sha256[SHA256_DIGEST_LENGTH];
    const size_t slen = strlen(salt),
        len = strlen(password), n = sizeof dec_salt + len;
    unsigned char *const salted = malloc(n);

    if (slen != SALT_LEN * 2)
    {
        fprintf(stderr, "%s: unexpected salt length: %zu\n", __func__, slen);
        goto end;
    }
    else if (!salted)
    {
        fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno));
        goto end;
    }
    else if (hex_decode(salt, dec_salt, sizeof dec_salt))
    {
        fprintf(stderr, "%s: hex_decode failed\n", __func__);
        goto end;
    }

    memcpy(salted, dec_salt, sizeof dec_salt);
    memcpy(salted + sizeof dec_salt, password, len);

    if (!SHA256(salted, n, sha256))
    {
        fprintf(stderr, "%s: SHA256 (first round) failed\n", __func__);
        goto end;
    }

    enum {ROUNDS = 1000 - 1};

    for (int i = 0; i < ROUNDS; i++)
        if (!SHA256(sha256, sizeof sha256, sha256))
        {
            fprintf(stderr, "%s: SHA256 failed\n", __func__);
            goto end;
        }

    char sha256_str[sizeof
        "00000000000000000000000000000000"
        "00000000000000000000000000000000"];

    for (struct {char *p; size_t i;} a = {.p = sha256_str};
        a.i < sizeof sha256 / sizeof *sha256; a.i++, a.p += 2)
        sprintf(a.p, "%02x", sha256[a.i]);

    if (!strcmp(sha256_str, exp_password))
        ret = 0;
    else
        /* Positive error code for password mismatch. */
        ret = 1;

end:
    free(salted);
    return ret;
}

int auth_login(const struct auth *const a, const char *const user,
    const char *const password, char **const cookie)
{
    int ret = -1;
    const char *const path = a->db.str;
    char *const db = dump_db(path);
    cJSON *json = NULL;

    if (!db)
    {
        fprintf(stderr, "%s: dump_db failed\n", __func__);
        goto end;
    }
    else if (!(json = cJSON_Parse(db)))
    {
        fprintf(stderr, "%s: cJSON_Parse failed\n", __func__);
        goto end;
    }

    const cJSON *const users = cJSON_GetObjectItem(json, "users");

    if (!users)
    {
        fprintf(stderr, "%s: could not find users\n", __func__);
        goto end;
    }
    else if (!cJSON_IsArray(users))
    {
        fprintf(stderr, "%s: expected JSON array for users\n", __func__);
        goto end;
    }

    const cJSON *u;

    cJSON_ArrayForEach(u, users)
    {
        const cJSON *const n = cJSON_GetObjectItem(u, "name"),
            *const s = cJSON_GetObjectItem(u, "salt"),
            *const p = cJSON_GetObjectItem(u, "password"),
            *const k = cJSON_GetObjectItem(u, "key");
        const char *name, *salt, *pwd, *key;

        if (!n || !(name = cJSON_GetStringValue(n)))
        {
            fprintf(stderr, "%s: missing username\n", __func__);
            goto end;
        }
        else if (!s || !(salt = cJSON_GetStringValue(s)))
        {
            fprintf(stderr, "%s: missing salt\n", __func__);
            goto end;
        }
        else if (!p || !(pwd = cJSON_GetStringValue(p)))
        {
            fprintf(stderr, "%s: missing password\n", __func__);
            goto end;
        }
        else if (!k || !(key = cJSON_GetStringValue(k)))
        {
            fprintf(stderr, "%s: missing key\n", __func__);
            goto end;
        }
        else if (!strcmp(name, user))
        {
            const int res = compare_pwd(salt, password, pwd);

            if (res < 0)
            {
                fprintf(stderr, "%s: generate_cookie failed\n", __func__);
                goto end;
            }
            else if (!res)
            {
                if (generate_cookie(json, path, name, key, cookie))
                {
                    fprintf(stderr, "%s: generate_cookie failed\n", __func__);
                    goto end;
                }

                ret = 0;
                goto end;
            }
        }
    }

    ret = 1;

end:
    free(db);
    cJSON_Delete(json);
    return ret;
}

void auth_free(struct auth *const a)
{
    if (a)
    {
        dynstr_free(&a->dir);
        dynstr_free(&a->db);
    }

    free(a);
}

const char *auth_dir(const struct auth *const a)
{
    return a->dir.str;
}

int auth_quota(const struct auth *const a, const char *const user,
    bool *const available, unsigned long long *const quota)
{
    int ret = -1;
    const char *const path = a->db.str;
    char *const db = dump_db(path);
    cJSON *json = NULL;

    if (!db)
    {
        fprintf(stderr, "%s: dump_db failed\n", __func__);
        goto end;
    }
    else if (!(json = cJSON_Parse(db)))
    {
        fprintf(stderr, "%s: cJSON_Parse failed\n", __func__);
        goto end;
    }

    const cJSON *const users = cJSON_GetObjectItem(json, "users");

    if (!users)
    {
        fprintf(stderr, "%s: could not find users\n", __func__);
        goto end;
    }
    else if (!cJSON_IsArray(users))
    {
        fprintf(stderr, "%s: expected JSON array for users\n", __func__);
        goto end;
    }

    *available = false;

    const cJSON *u;

    cJSON_ArrayForEach(u, users)
    {
        const cJSON *const n = cJSON_GetObjectItem(u, "name"),
            *const q = cJSON_GetObjectItem(u, "quota");
        const char *name;

        if (!n || !(name = cJSON_GetStringValue(n)))
        {
            fprintf(stderr, "%s: missing username\n", __func__);
            goto end;
        }
        else if (!strcmp(name, user))
        {
            const char *qs;

            if (!q || !(qs = cJSON_GetStringValue(q)) || !*qs)
            {
                /* Unlimited quota. */
                ret = 0;
                goto end;
            }

            char *end;

            errno = 0;
            *available = true;
            *quota = strtoull(qs, &end, 10);

            const unsigned long long mul = 1024ul * 1024ul;

            if (errno || *end != '\0')
            {
                fprintf(stderr, "%s: invalid quota %s: %s\n",
                    __func__, qs, strerror(errno));
                goto end;
            }
            else if (*quota >= ULLONG_MAX / mul)
            {
                fprintf(stderr, "%s: quota %s too large\n", __func__, qs);
                goto end;
            }

            *quota *= mul;
            break;
        }
    }

    ret = 0;

end:
    free(db);
    cJSON_Delete(json);
    return ret;
}

static int create_db(const char *const path)
{
    int ret = -1;
    const int fd = open(path, O_WRONLY | O_CREAT, 0600);

    if (fd < 0)
    {
        fprintf(stderr, "%s: open(2): %s\n", __func__, strerror(errno));
        goto end;
    }

    static const char db[] = "{\"users\": []}\n";

    for (struct {size_t n; const void *p;}
        a = {.n = strlen(db), .p = db}; a.n;)
    {
        const ssize_t n = write(fd, a.p, a.n);

        if (n < 0)
        {
            fprintf(stderr, "%s: write(2): %s\n", __func__, strerror(errno));
            goto end;
        }

        a.n -= n;
        a.p = ((const char *)a.p) + n;
    }

    printf("Created login database at %s. Please remember to add new users "
        "with usergen(1).\n", path);
    ret = 0;

end:

    if (fd >= 0 && close(fd))
    {
        fprintf(stderr, "%s: close(2): %s\n", __func__, strerror(errno));
        return -1;
    }

    return ret;
}

static int init_db(struct auth *const a)
{
    struct stat sb;
    const char *const path = a->db.str;

    if (stat(path, &sb))
    {
        if (errno == ENOENT)
            return create_db(path);
        else
        {
            fprintf(stderr, "%s: stat(2): %s\n", __func__, strerror(errno));
            return -1;
        }
    }

    return 0;
}

static char *resolve_cwd(void)
{
    size_t len = 1;
    char *p = NULL;

    for (;;)
    {
        char *const pp = realloc(p, len);

        if (!pp)
        {
            fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno));
            break;
        }

        p = pp;

        if (!getcwd(pp, len))
        {
            if (errno != ERANGE)
            {
                fprintf(stderr, "%s: getcwd(3): %s\n",
                    __func__, strerror(errno));
                break;
            }
            else
                len++;
        }
        else
            return p;
    }

    free(p);
    return NULL;
}

struct auth *auth_alloc(const char *const dir)
{
    struct auth *const a = malloc(sizeof *a), *ret = NULL;
    char *abspath = NULL;

    if (!a)
    {
        fprintf(stderr, "%s: malloc(3) auth: %s\n", __func__, strerror(errno));
        goto end;
    }

    *a = (const struct auth){0};

    dynstr_init(&a->db);
    dynstr_init(&a->dir);

    if (*dir != '/' && !(abspath = resolve_cwd()))
    {
        fprintf(stderr, "%s: resolve_cwd failed\n", __func__);
        goto end;
    }
    else if (abspath && dynstr_append(&a->dir, "%s", abspath))
    {
        fprintf(stderr, "%s: dynstr_append abspath failed\n", __func__);
        goto end;
    }
    else if (dynstr_append(&a->dir, "%s%s", abspath ? "/" : "", dir))
    {
        fprintf(stderr, "%s: dynstr_append dir failed\n", __func__);
        goto end;
    }
    else if (dynstr_append(&a->db, "%s/db.json", a->dir.str))
    {
        fprintf(stderr, "%s: dynstr_append db failed\n", __func__);
        goto end;
    }
    else if (init_db(a))
    {
        fprintf(stderr, "%s: init_db failed\n", __func__);
        goto end;
    }

    ret = a;

end:
    free(abspath);

    if (!ret)
        auth_free(a);

    return ret;
}
