#include <stdio.h>
#include <unistd.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "auth.h"
#include "sqlite3.h"
#include "util.h"
#include "list.h"
#include "dSFMT.h"
#include "ow-crypt.h"

#define BEGIN(db)  sqlite3_exec(db->conn, "BEGIN",  NULL, NULL, NULL)
#define COMMIT(db) sqlite3_exec(db->conn, "COMMIT", NULL, NULL, NULL)
#define buildList(...) _buildList(__VA_ARGS__, NULL) 
#define insertRow(...) _insertRow(__VA_ARGS__, NULL)

struct account {
  char* username;
  char* passhash;
  char* salt;
  char* sid;
  LIST* groups;
  LIST* perms;
};

struct group {
  char* groupname;
  LIST* members;
  LIST* perms;
};

struct passdata {
  char* passhash;
  char* salt;
};

typedef struct account account;
typedef struct group group;
typedef struct passdata passdata;
//#define group struct group

char* err; //Global SQLite3 error message buffer

void init_prng() { init_gen_rand(time(NULL)); }

static int selectCellWhereText(authdb* db, char* result, char* column, char* table, char* target, char* text) {
  char sql[MAXLEN_SQL]; memset(sql, 0, MAXLEN_SQL);
  sqlite3_stmt* stmt;
  snprintf(sql, MAXLEN_SQL, "select %s from %s where %s='%s'", column, table, target, text);
  if (sqlite3_prepare_v2(db->conn, sql, MAXLEN_SQL, &stmt, NULL) != SQLITE_OK) { return 1; }
  if (sqlite3_step(stmt) != SQLITE_ROW) { return 1; }
  snprintf(result, MAXLEN_RESULT, "%s", (char*)sqlite3_column_text(stmt, 0));
  sqlite3_finalize(stmt);
  return 0;
}

static int selectRowWhereText(authdb* db, LIST* list, char* column, char* table, char* target, char* text) {
  int c; int cmax;
  char sql[MAXLEN_SQL]; memset(sql, 0, MAXLEN_SQL);
  sqlite3_stmt* stmt;
  
  snprintf(sql, MAXLEN_SQL, "select %s from %s where %s='%s'", column, table, target, text);
  if (sqlite3_prepare_v2(db->conn, sql, MAXLEN_SQL, &stmt, NULL) != SQLITE_OK) { return 1; }
  if (sqlite3_step(stmt) != SQLITE_ROW) { return 1; }
  cmax = sqlite3_column_count(stmt);
  if (!cmax) { return 1; }

  do {
    c = 0;
    while (c < cmax) {
      listQueue(list, strndup((char*)sqlite3_column_text(stmt, c), MAXLEN_RESULT));
      c++;
    }
  } while (sqlite3_step(stmt) == SQLITE_ROW);
  sqlite3_finalize(stmt);
  return 0;
}

static int updateRowWhereText(authdb* db, char* column, char* table, char* value, char* target, char* against) {
  int status = 0;
  char* sql = malloc(MAXLEN_SQL); memset(sql, 0, MAXLEN_SQL);
  snprintf(sql, MAXLEN_SQL, "update %s set %s='%s' where %s='%s'", table, column, value, target, against);

  logs("SQL: %s", sql);
  BEGIN(db);
  if (sqlite3_exec(db->conn, sql, NULL, NULL, &err) != SQLITE_OK) {
    logs("updateRowWhereText: sqlite3_exec failed: %s", err);
    logs("updateRowWhereText: statement was: %s", sql);
    status = 1;
  }
  COMMIT(db);
#if DO_FREE
  sqlite3_free(err);
  free(sql);
#endif
  return status;
}

static int _insertRow(authdb* db, char* table, char* val, ...) {
  va_list ap; int l;
  char* v = val; 
  int c=0; int status=0;
  char* sql = malloc(MAXLEN_SQL);  memset(sql, 0, MAXLEN_SQL);
  char* vals = malloc(MAXLEN_SQL); memset(vals, 0, MAXLEN_SQL);

  va_start(ap, val);
  while (v) {
    snprintf(vals, MAXLEN_SQL, "'%s',", v);
    l = strlen(vals);
    vals += l; c+= l;
    v = va_arg(ap, char*);
  }
  va_end(ap);

  vals -= c;
  vals[(c-1)] = '\0'; //Strip off the last comma
  snprintf(sql, MAXLEN_SQL, "insert into %s values (%s)", table, vals);

  logs("SQL: %s", sql);
  
  BEGIN(db);
  if (sqlite3_exec(db->conn, sql, NULL, NULL, &err) != SQLITE_OK) {
    logs("insertRow: sqlite3_exec failed: %s", err);
    logs("insertRow: statement was: %s", sql);
    status=1;
  }
  COMMIT(db);

#if DO_FREE
  sqlite3_free(err);
  deleteListIterator(i);
  free(vals);
  free(sql);
#endif
  
  return status;
}

static LIST* _buildList(char* v, ...) {
  char* arg;
  va_list ap;
  LIST* list = newList();

  listQueue(list, v);
  va_start(ap, v);
  while ((arg = va_arg(ap, char*))) {
    listQueue(list, arg);
  }
  
  return list;
}

#if DO_FREE
static void freeAccount(account* acc) {
  deleteList(acc->groups);
  deleteList(acc->perms);
  free(acc->username);
  free(acc->passhash);
  free(acc->salt);
  free(acc->sid);
  free(acc);
}

static void freeGroup(group* grp) {
  deleteList(grp->members);
  deleteList(grp->perms);
  free(grp->groupname);
  free(grp);
}

static void freePassdata(passdata *pd) {
  free(pd->passhash);
  free(pd->salt);
  free(pd);
}
#endif

static account* newAccount() {
  account* acc = malloc(sizeof(*acc));
  if (!acc) { return NULL; }
  memset(acc, 0, sizeof(account));
  acc->groups = newList();
  acc->perms  = newList();

  return acc;
}

static group* newGroup() {
  group* grp = malloc(sizeof(*grp)); 
  if (!grp) { return NULL; }
  memset(grp, 0, sizeof(group));
  grp->perms   = newList();
  grp->members = newList();

  return grp;
}

static void getGroup(authdb* db, char* groupname, group* grp) {
  grp->groupname = groupname;
  
  selectRowWhereText(db, grp->members, "username", "groups_membership", "groupname", groupname);
  selectRowWhereText(db, grp->perms,   "perm",     "groups_perms",      "groupname", groupname);
}

static void getAccount(authdb* db, char* user, account* acc) {
  LIST* fields = newList();

  selectRowWhereText(db, acc->groups, "groupname", "groups_membership", "username", user);
  selectRowWhereText(db, acc->perms,  "perm",      "users_perms",       "username", user);
  selectRowWhereText(db, fields,      "*",         "auth",              "username", user);
  
  acc->username = (char*)listPop(fields); acc->passhash = (char*)listPop(fields); 
  acc->salt = (char*)listPop(fields);     acc->sid  = (char*)listPop(fields);
  deleteList(fields);
}

static int connect_db(char* path, sqlite3** conn) {
  if (sqlite3_open(path, conn) != SQLITE_OK) {
    logs("AUTH: sqlite3_open failed");
    return 1;
  }
  return 0;
}

static int init_authdb(authdb* db) {
  if (connect_db(db->path, &db->conn)) {
    return 1;
  }
  char* query = read_file("initauth.sql", MAXLEN_FILE, 1);
  if (!query) {
    logs("AUTH: couldn't read init SQL file");
    return 1;
  }
  BEGIN(db);
  if (sqlite3_exec(db->conn, query, NULL, NULL, &err) != SQLITE_OK) {
    logs("AUTH: database initialization failed: %s", err);
#if DO_FREE
    sqlite3_free(err);
#endif
    return 1;
  }
  COMMIT(db);
  return 0;
}

static passdata* newPassdata() {
  passdata* pd = malloc(sizeof(*pd));
  memset(pd, 0, sizeof(passdata));

  pd->passhash = malloc((MAXLEN_PASSHASH+1)); memset(pd->passhash, 0, (MAXLEN_PASSHASH+1));
  pd->salt = malloc(MAXLEN_SALT); memset(pd->salt, 0, MAXLEN_SALT);
  
  return pd;
}

static passdata* hash_password(char* password, char* salt) {
  passdata* pd = newPassdata();

  if (!salt) {
    double salt[(LEN_SALT/8)]; memset(salt, 0, LEN_SALT);      //8 chars to 1 double
    fill_array_close1_open2(salt, (LEN_SALT/8));               //Get entropy for salt
    crypt_gensalt_rn("$2a$", BCRYPT_ROUNDS, (char*)salt, LEN_SALT, pd->salt, MAXLEN_SALT); //Generate salt
  }
  else { snprintf(pd->salt, MAXLEN_SALT, "%s", salt); }
  crypt_rn(password, pd->salt, pd->passhash, MAXLEN_PASSHASH); //Generate password hash
  
  return pd;
}

int user_addgroup(authdb* db, char* username, char* groupname) {
  if (insertRow(db, "groups_membership", username, groupname)) { return 1; }
  return 0;
}

int group_addperm(authdb* db, char* groupname, char* perm) {
  if (insertRow(db, "groups_perms", groupname, perm)) { return 1; }
  return 0;
}

int user_addperm(authdb* db, char* username, char* perm) {
  if (insertRow(db, "users_perms", username, perm)) { return 1; }
  return 0;
}

int register_perm(authdb *db, char* perm, char* desc) {
  if (insertRow(db, "perms", desc)) { return 1; }
  return 0;
}

int register_group(authdb *db, char* groupname) {
  if (insertRow(db, "auth", groupname)) { return 1; }
  return 0;
}

int user_validate(authdb *db, char* username, char* password) {
  char* salt; char* passhash; passdata* pd;
  LIST* data = newList();
  
  if (selectRowWhereText(db, data, "passhash,salt", "auth", "username", username)) { return 1; }
  passhash = listPop(data);
  salt = listPop(data);
  if (!salt || !passhash) { return 1; }
  
  pd = hash_password(password, salt);
  if (!pd) { return 1; }
  if (!(strncmp(passhash, pd->passhash, MAXLEN_PASSHASH))) { return 0; }
  
  return 1;
}

int user_setpass(authdb *db, char* username, char* password) {
  passdata* pd = hash_password(password, NULL);
  if (updateRowWhereText(db, "passhash", "auth", pd->passhash, "username", username) ||
      updateRowWhereText(db, "salt",     "auth", pd->salt,     "username", username))
    { return 1; }
  return 0;
}

int register_user(authdb *db, char* username, char* password, char* groupname, ...) {
  va_list ap;
  int status = 0;
  char sql[MAXLEN_SQL]; memset(sql, 0, MAXLEN_SQL);
  
  if (strlen(password) > MAXLEN_PASSWORD) {
    logs("register_user: Password for '%s' exceeds maximum length of %d", username, MAXLEN_PASSWORD);
    return 1;
  }

  passdata* pd = hash_password(password, NULL);

  if (insertRow(db, "auth", username, pd->passhash, pd->salt, "")) {
    logs("register_user: Couldn't insert row");
    return 1;
  }
  
  //Join supplied groups - move into own function
  va_start(ap, groupname);
  do {
    snprintf(sql, MAXLEN_SQL, "insert into groups_membership values('%s','%s')", username, groupname);
    if (sqlite3_exec(db->conn, sql, NULL, NULL, NULL) != SQLITE_OK) {
      logs("register_user: Couldn't add user '%s' to group '%s'", username, groupname);
      status=1;
      break;
    }
  } while ((groupname = va_arg(ap, char*)));
  va_end(ap);

#if DO_FREE
  free(pd);
#endif
  
  return status;
}

authdb* new_authdb(char* path) {
  authdb* db = malloc(sizeof(authdb)); memset(db, 0, sizeof(authdb));
  db->path   = malloc(MAXLEN_PATH); memset(db->path, 0, MAXLEN_PATH);
  snprintf(db->path, MAXLEN_PATH, "%s", path);
  if (access(db->path, F_OK) == -1) {
    logs("AUTH: initializing new database at '%s'", path);
    if (init_authdb(db)) { return NULL; }
  }
  else if (connect_db(db->path, &db->conn)) {
    return NULL;
  }
  return db;
}

int group_has_perm(authdb* db, char* groupname, char* perm) {
  char* p;
  group* grp = newGroup();
  getGroup(db, groupname, grp);
  if (!grp) { return 1; }

  LIST_ITERATOR* ip = newListIterator(grp->perms);
  ITERATE_LIST(p, ip) {
    if (!(strncmp(p, perm, MAXLEN_PERM))) { return 0; }
  } 

#if DO_FREE
  deleteListIterator(ip);
  freeGroup(grp);
#endif
  
  return 1;
}

int user_has_perm(authdb* db, char* user, char* perm) {
  char* p; char* g;
  int            status = 1;
  account*       acc    = newAccount();
  getAccount(db, user, acc);
  if (!acc) { return 1; }
  LIST_ITERATOR* ip     = newListIterator(acc->perms);
  LIST_ITERATOR* ig;
  
  //Check if user has permission assigned directly
  ITERATE_LIST(p, ip) {
    if (!(strncmp(p,perm,MAXLEN_PERM))) { status=0; break;}
  } 

  if (status) {
    ig = newListIterator(acc->groups);
    //Check if any of the users groups have the permission
    ITERATE_LIST(g, ig) {
      if (!(group_has_perm(db, g, perm))) { status=0; break; }
    } 
  }

#if DO_FREE
  deleteListIterator(ip);
  deleteListIterator(ig);
  freeAccount(acc);
#endif
  
  return status;
}


