/*
 * Copyright (c) 2019 TAOS Data, Inc. <jhtao@taosdata.com>
 *
 * This program is free software: you can use, redistribute, and/or modify
 * it under the terms of the GNU Affero General Public License, version 3
 * or later ("AGPL"), as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#define _DEFAULT_SOURCE
#include "os.h"
#include "trpc.h"
#include "ttime.h"
#include "mgmtAcct.h"
#include "mgmtGrant.h"
#include "mgmtMnode.h"
#include "mgmtShell.h"
#include "mgmtUser.h"

void *tsUserSdb = NULL;
static int32_t tsUserUpdateSize = 0;

static int32_t   mgmtCreateUser(SAcctObj *pAcct, char *name, char *pass);
static int32_t   mgmtDropUser(SAcctObj *pAcct, char *name);
static int32_t   mgmtUpdateUser(SUserObj *pUser);
static int32_t   mgmtGetUserMeta(STableMetaMsg *pMeta, SShowObj *pShow, void *pConn);
static int32_t   mgmtRetrieveUsers(SShowObj *pShow, char *data, int32_t rows, void *pConn);

static void mgmtProcessCreateUserMsg(SQueuedMsg *pMsg);
static void mgmtProcessAlterUserMsg(SQueuedMsg *pMsg);
static void mgmtProcessDropUserMsg(SQueuedMsg *pMsg);

static void *(*mgmtUserActionFp[SDB_MAX_ACTION_TYPES])(void *row, char *str, int32_t size, int32_t *ssize);
static void *mgmtUserActionInsert(void *row, char *str, int32_t size, int32_t *ssize);
static void *mgmtUserActionDelete(void *row, char *str, int32_t size, int32_t *ssize);
static void *mgmtUserActionUpdate(void *row, char *str, int32_t size, int32_t *ssize);
static void *mgmtUserActionEncode(void *row, char *str, int32_t size, int32_t *ssize);
static void *mgmtUserActionDecode(void *row, char *str, int32_t size, int32_t *ssize);
static void *mgmtUserActionReset(void *row, char *str, int32_t size, int32_t *ssize);
static void *mgmtUserActionDestroy(void *row, char *str, int32_t size, int32_t *ssize);
static void *mgmtUserAction(char action, void *row, char *str, int32_t size, int32_t *ssize);
static void  mgmtUserActionInit();

int32_t mgmtInitUsers() {
  void     *pNode     = NULL;
  SUserObj *pUser     = NULL;
  SAcctObj *pAcct     = NULL;
  int32_t  numOfUsers = 0;

  mgmtUserActionInit();

  SUserObj tObj;
  tsUserUpdateSize = tObj.updateEnd - (int8_t *)&tObj;

  tsUserSdb = sdbOpenTable(tsMaxUsers, tsUserUpdateSize, "users", SDB_KEYTYPE_STRING, tsMnodeDir, mgmtUserAction);
  if (tsUserSdb == NULL) {
    mError("failed to init user data");
    return -1;
  }

  while (1) {
    pNode = sdbFetchRow(tsUserSdb, pNode, (void **)&pUser);
    if (pUser == NULL) break;

    pUser->prev = NULL;
    pUser->next = NULL;

    pAcct = mgmtGetAcct(pUser->acct);
    mgmtAddUserIntoAcct(pAcct, pUser);

    numOfUsers++;
  }

  pAcct = mgmtGetAcct("root");
  mgmtCreateUser(pAcct, "root", "taosdata");
  mgmtCreateUser(pAcct, "monitor", tsInternalPass);
  mgmtCreateUser(pAcct, "_root", tsInternalPass);

  mgmtAddShellMsgHandle(TSDB_MSG_TYPE_CM_CREATE_USER, mgmtProcessCreateUserMsg);
  mgmtAddShellMsgHandle(TSDB_MSG_TYPE_CM_ALTER_USER, mgmtProcessAlterUserMsg);
  mgmtAddShellMsgHandle(TSDB_MSG_TYPE_CM_DROP_USER, mgmtProcessDropUserMsg);
  mgmtAddShellShowMetaHandle(TSDB_MGMT_TABLE_USER, mgmtGetUserMeta);
  mgmtAddShellShowRetrieveHandle(TSDB_MGMT_TABLE_USER, mgmtRetrieveUsers);
  
  mTrace("user data is initialized");
  return 0;
}

void mgmtCleanUpUsers() {
  sdbCloseTable(tsUserSdb);
}

SUserObj *mgmtGetUser(char *name) {
  return (SUserObj *)sdbGetRow(tsUserSdb, name);
}

static int32_t mgmtUpdateUser(SUserObj *pUser) {
  return sdbUpdateRow(tsUserSdb, pUser, 0, 1);
}

static int32_t mgmtCreateUser(SAcctObj *pAcct, char *name, char *pass) {
  int32_t numOfUsers = sdbGetNumOfRows(tsUserSdb);
  if (numOfUsers >= tsMaxUsers) {
    mWarn("numOfUsers:%d, exceed tsMaxUsers:%d", numOfUsers, tsMaxUsers);
    return TSDB_CODE_TOO_MANY_USERS;
  }

  int32_t code = mgmtCheckUserLimit(pAcct);
  if (code != 0) {
    return code;
  }

  if (name[0] == 0 || pass[0] == 0) {
    return TSDB_CODE_INVALID_MSG;
  }

  SUserObj *pUser = (SUserObj *)sdbGetRow(tsUserSdb, name);
  if (pUser != NULL) {
    mTrace("user:%s is already there", name);
    return TSDB_CODE_USER_ALREADY_EXIST;
  }

  code = mgmtCheckUserGrant();
  if (code != 0) {
    return code;
  }

  pUser = malloc(sizeof(SUserObj));
  memset(pUser, 0, sizeof(SUserObj));
  strcpy(pUser->user, name);
  taosEncryptPass((uint8_t*) pass, strlen(pass), pUser->pass);
  strcpy(pUser->acct, pAcct->user);
  pUser->createdTime = taosGetTimestampMs();
  pUser->superAuth = 0;
  pUser->writeAuth = 1;
  if (strcmp(pUser->user, "root") == 0 || strcmp(pUser->user, pUser->acct) == 0) {
    pUser->superAuth = 1;
  }

  code = TSDB_CODE_SUCCESS;
  if (sdbInsertRow(tsUserSdb, pUser, 0) < 0) {
    tfree(pUser);
    code = TSDB_CODE_SDB_ERROR;
  }

  return code;
}

static int32_t mgmtDropUser(SAcctObj *pAcct, char *name) {
  SUserObj *pUser;

  pUser = (SUserObj *)sdbGetRow(tsUserSdb, name);
  if (pUser == NULL) {
    mWarn("user:%s is not there", name);
    return TSDB_CODE_INVALID_USER;
  }

  if (strcmp(pAcct->user, pUser->acct) != 0) {
    return TSDB_CODE_NO_RIGHTS;
  }

  sdbDeleteRow(tsUserSdb, pUser);

  return 0;
}

static int32_t mgmtGetUserMeta(STableMetaMsg *pMeta, SShowObj *pShow, void *pConn) {
  SUserObj *pUser = mgmtGetUserFromConn(pConn);
  if (pUser == NULL) {
    return TSDB_CODE_INVALID_USER;
  }

  int32_t cols     = 0;
  SSchema *pSchema = pMeta->schema;

  pShow->bytes[cols] = TSDB_USER_LEN;
  pSchema[cols].type = TSDB_DATA_TYPE_BINARY;
  strcpy(pSchema[cols].name, "name");
  pSchema[cols].bytes = htons(pShow->bytes[cols]);
  cols++;

  pShow->bytes[cols] = 6;
  pSchema[cols].type = TSDB_DATA_TYPE_BINARY;
  strcpy(pSchema[cols].name, "privilege");
  pSchema[cols].bytes = htons(pShow->bytes[cols]);
  cols++;

  pShow->bytes[cols] = 8;
  pSchema[cols].type = TSDB_DATA_TYPE_TIMESTAMP;
  strcpy(pSchema[cols].name, "created_time");
  pSchema[cols].bytes = htons(pShow->bytes[cols]);
  cols++;

  pMeta->numOfColumns = htons(cols);
  strcpy(pMeta->tableId, "show users");
  pShow->numOfColumns = cols;

  pShow->offset[0] = 0;
  for (int32_t i = 1; i < cols; ++i) {
    pShow->offset[i] = pShow->offset[i - 1] + pShow->bytes[i - 1];
  }

  pShow->numOfRows = pUser->pAcct->acctInfo.numOfUsers;
  pShow->pNode = pUser->pAcct->pUser;
  pShow->rowSize = pShow->offset[cols - 1] + pShow->bytes[cols - 1];

  return 0;
}

static int32_t mgmtRetrieveUsers(SShowObj *pShow, char *data, int32_t rows, void *pConn) {
  int32_t  numOfRows = 0;
  SUserObj *pUser    = NULL;
  int32_t  cols      = 0;
  char     *pWrite;

  while (numOfRows < rows) {
    pUser = (SUserObj *)pShow->pNode;
    if (pUser == NULL) break;
    pShow->pNode = (void *)pUser->next;

    cols = 0;

    pWrite = data + pShow->offset[cols] * rows + pShow->bytes[cols] * numOfRows;
    strcpy(pWrite, pUser->user);
    cols++;

    pWrite = data + pShow->offset[cols] * rows + pShow->bytes[cols] * numOfRows;
    if (pUser->superAuth) {
      strcpy(pWrite, "super");
    } else if (pUser->writeAuth) {
      strcpy(pWrite, "write");
    } else {
      strcpy(pWrite, "read");
    }
    cols++;

    pWrite = data + pShow->offset[cols] * rows + pShow->bytes[cols] * numOfRows;
    *(int64_t *)pWrite = pUser->createdTime;
    cols++;

    numOfRows++;
  }
  pShow->numOfReads += numOfRows;
  return numOfRows;
}

static void mgmtUserActionInit() {
  mgmtUserActionFp[SDB_TYPE_INSERT]  = mgmtUserActionInsert;
  mgmtUserActionFp[SDB_TYPE_DELETE]  = mgmtUserActionDelete;
  mgmtUserActionFp[SDB_TYPE_UPDATE]  = mgmtUserActionUpdate;
  mgmtUserActionFp[SDB_TYPE_ENCODE]  = mgmtUserActionEncode;
  mgmtUserActionFp[SDB_TYPE_DECODE]  = mgmtUserActionDecode;
  mgmtUserActionFp[SDB_TYPE_RESET]   = mgmtUserActionReset;
  mgmtUserActionFp[SDB_TYPE_DESTROY] = mgmtUserActionDestroy;
}

static void *mgmtUserAction(char action, void *row, char *str, int32_t size, int32_t *ssize) {
  if (mgmtUserActionFp[(uint8_t) action] != NULL) {
    return (*(mgmtUserActionFp[(uint8_t) action]))(row, str, size, ssize);
  }
  return NULL;
}

static void *mgmtUserActionInsert(void *row, char *str, int32_t size, int32_t *ssize) {
  SUserObj *pUser = (SUserObj *) row;
  SAcctObj *pAcct = mgmtGetAcct(pUser->acct);

  pUser->pAcct = pAcct;
  mgmtAddUserIntoAcct(pAcct, pUser);

  return NULL;
}

static void *mgmtUserActionDelete(void *row, char *str, int32_t size, int32_t *ssize) {
  SUserObj *pUser = (SUserObj *) row;
  SAcctObj *pAcct = mgmtGetAcct(pUser->acct);

  mgmtRemoveUserFromAcct(pAcct, pUser);

  return NULL;
}

static void *mgmtUserActionUpdate(void *row, char *str, int32_t size, int32_t *ssize) {
  return mgmtUserActionReset(row, str, size, ssize);
}

static void *mgmtUserActionEncode(void *row, char *str, int32_t size, int32_t *ssize) {
  SUserObj *pUser = (SUserObj *) row;

  if (size < tsUserUpdateSize) {
    *ssize = -1;
  } else {
    memcpy(str, pUser, tsUserUpdateSize);
    *ssize = tsUserUpdateSize;
  }

  return NULL;
}

static void *mgmtUserActionDecode(void *row, char *str, int32_t size, int32_t *ssize) {
  SUserObj *pUser = (SUserObj *) malloc(sizeof(SUserObj));
  if (pUser == NULL) return NULL;
  memset(pUser, 0, sizeof(SUserObj));

  memcpy(pUser, str, tsUserUpdateSize);

  return (void *)pUser;
}

static void *mgmtUserActionReset(void *row, char *str, int32_t size, int32_t *ssize) {
  SUserObj *pUser = (SUserObj *)row;

  memcpy(pUser, str, tsUserUpdateSize);

  return NULL;
}

static void *mgmtUserActionDestroy(void *row, char *str, int32_t size, int32_t *ssize) {
  tfree(row);

  return NULL;
}

SUserObj *mgmtGetUserFromConn(void *pConn) {
  SRpcConnInfo connInfo;
  if (rpcGetConnInfo(pConn, &connInfo) == 0) {
    return mgmtGetUser(connInfo.user);
  }

  return NULL;
}

static void mgmtProcessCreateUserMsg(SQueuedMsg *pMsg) {
  if (mgmtCheckRedirect(pMsg->thandle)) return;

  int32_t code;
  SUserObj *pUser = pMsg->pUser;
  
  if (pUser->superAuth) {
    SCMCreateUserMsg *pCreate = pMsg->pCont;
    code = mgmtCreateUser(pUser->pAcct, pCreate->user, pCreate->pass);
    if (code == TSDB_CODE_SUCCESS) {
      mLPrint("user:%s is created by %s", pCreate->user, pUser->user);
    }
  } else {
    code = TSDB_CODE_NO_RIGHTS;
  }

  mgmtSendSimpleResp(pMsg->thandle, code);
}

static void mgmtProcessAlterUserMsg(SQueuedMsg *pMsg) {
  if (mgmtCheckRedirect(pMsg->thandle)) return;

  int32_t code;
  SUserObj *pOperUser = pMsg->pUser;
  
  SCMAlterUserMsg *pAlter = pMsg->pCont;
  SUserObj *pUser = mgmtGetUser(pAlter->user);
  if (pUser == NULL) {
    mgmtSendSimpleResp(pMsg->thandle, TSDB_CODE_INVALID_USER);
    return;
  }

  if (strcmp(pUser->user, "monitor") == 0 || (strcmp(pUser->user + 1, pUser->acct) == 0 && pUser->user[0] == '_')) {
    mgmtSendSimpleResp(pMsg->thandle, TSDB_CODE_NO_RIGHTS);
    return;
  }

  if ((pAlter->flag & TSDB_ALTER_USER_PASSWD) != 0) {
    bool hasRight = false;
    if (strcmp(pOperUser->user, "root") == 0) {
      hasRight = true;
    } else if (strcmp(pUser->user, pOperUser->user) == 0) {
      hasRight = true;
    } else if (pOperUser->superAuth) {
      if (strcmp(pUser->user, "root") == 0) {
        hasRight = false;
      } else if (strcmp(pOperUser->acct, pUser->acct) != 0) {
        hasRight = false;
      } else {
        hasRight = true;
      }
    }

    if (hasRight) {
      memset(pUser->pass, 0, sizeof(pUser->pass));
      taosEncryptPass((uint8_t*)pAlter->pass, strlen(pAlter->pass), pUser->pass);
      code = mgmtUpdateUser(pUser);
      mLPrint("user:%s password is altered by %s, code:%d", pAlter->user, pUser->user, code);
    } else {
      code = TSDB_CODE_NO_RIGHTS;
    }

    mgmtSendSimpleResp(pMsg->thandle, code);
    return;
  }

  if ((pAlter->flag & TSDB_ALTER_USER_PRIVILEGES) != 0) {
    bool hasRight = false;

    if (strcmp(pUser->user, "root") == 0) {
      hasRight = false;
    } else if (strcmp(pUser->user, pUser->acct) == 0) {
      hasRight = false;
    } else if (strcmp(pOperUser->user, "root") == 0) {
      hasRight = true;
    } else if (strcmp(pUser->user, pOperUser->user) == 0) {
      hasRight = false;
    } else if (pOperUser->superAuth) {
      if (strcmp(pUser->user, "root") == 0) {
        hasRight = false;
      } else if (strcmp(pOperUser->acct, pUser->acct) != 0) {
        hasRight = false;
      } else {
        hasRight = true;
      }
    }

    if (pAlter->privilege == 1) { // super
      hasRight = false;
    }

    if (hasRight) {
      //if (pAlter->privilege == 1) {  // super
      //  pUser->superAuth = 1;
      //  pUser->writeAuth = 1;
      //}
      if (pAlter->privilege == 2) {  // read
        pUser->superAuth = 0;
        pUser->writeAuth = 0;
      }
      if (pAlter->privilege == 3) {  // write
        pUser->superAuth = 0;
        pUser->writeAuth = 1;
      }

      code = mgmtUpdateUser(pUser);
      mLPrint("user:%s privilege is altered by %s, code:%d", pAlter->user, pUser->user, code);
    } else {
      code = TSDB_CODE_NO_RIGHTS;
    }

    mgmtSendSimpleResp(pMsg->thandle, code);
    return;
  }

  mgmtSendSimpleResp(pMsg->thandle, TSDB_CODE_NO_RIGHTS);
}

static void mgmtProcessDropUserMsg(SQueuedMsg *pMsg) {
  if (mgmtCheckRedirect(pMsg->thandle)) return;

  int32_t code;
  SUserObj *pOperUser = pMsg->pUser;

  SCMDropUserMsg *pDrop = pMsg->pCont;
  SUserObj *pUser = mgmtGetUser(pDrop->user);
  if (pUser == NULL) {
    mgmtSendSimpleResp(pMsg->thandle, TSDB_CODE_INVALID_USER);
    return ;
  }

  if (strcmp(pUser->user, "monitor") == 0 || (strcmp(pUser->user + 1, pUser->acct) == 0 && pUser->user[0] == '_')) {
    mgmtSendSimpleResp(pMsg->thandle, TSDB_CODE_NO_RIGHTS);
    return ;
  }

  bool hasRight = false;
  if (strcmp(pUser->user, "root") == 0) {
    hasRight = false;
  } else if (strcmp(pOperUser->user, "root") == 0) {
    hasRight = true;
  } else if (strcmp(pUser->user, pOperUser->user) == 0) {
    hasRight = false;
  } else if (pOperUser->superAuth) {
    if (strcmp(pUser->user, "root") == 0) {
      hasRight = false;
    } else if (strcmp(pOperUser->acct, pUser->acct) != 0) {
      hasRight = false;
    } else {
      hasRight = true;
    }
  }

  if (hasRight) {
    code = mgmtDropUser(pUser->pAcct, pDrop->user);
    if (code == TSDB_CODE_SUCCESS) {
      mLPrint("user:%s is dropped by %s", pDrop->user, pUser->user);
    }
  } else {
    code = TSDB_CODE_NO_RIGHTS;
  }

  mgmtSendSimpleResp(pMsg->thandle, code);
}
