/****************************************************************************
 * This file is part of the project AqHome.
 * AqHome (c) by 2023 Martin Preuss, all rights reserved.
 *
 * The license for this file can be found in the file COPYING which you
 * should have received along with this file.
 ****************************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif


#include "./u_login.h"
#include "aqhome/http/httpservice.h"
#include "aqhome/http/httpservice_http.h"
#include "aqhome/http/httpservice_conf.h"

#include <gwenhywfar/gwenhywfar.h>
#include <gwenhywfar/args.h>
#include <gwenhywfar/debug.h>
#include <gwenhywfar/endpoint.h>
#include <gwenhywfar/mdigest.h>
#include <gwenhywfar/text.h>
#include <gwenhywfar/timestamp.h>



static int _handleUrl(AQH_HTTP_URLHANDLER *uh, AQH_HTTP_REQUEST *rq);
static int _handleGet(AQH_HTTP_URLHANDLER *uh, AQH_HTTP_REQUEST *rq);
static int _handlePost(AQH_HTTP_URLHANDLER *uh, AQH_HTTP_REQUEST *rq);

static int _loginUser(AQH_HTTP_URLHANDLER *uh, AQH_HTTP_REQUEST *rq);
static int _verifyPass(AQH_USER *u, const char *userName, const char *password);
static AQH_SESSION *_generateSessionForUser(AQH_SERVICE *sv, AQH_USER *u);
static GWEN_BUFFER *_generateSessionUid(void);
static void _headerSetCookie(GWEN_DB_NODE *db, uint32_t flags, const char *cookieName, const char *cookieValue);
static GWEN_MSG *_createLoginResponseMsg(AQH_SERVICE *sv, const char *protocol, const char *sessionId, const char *newPage);





AQH_HTTP_URLHANDLER *AQH_LoginHttpUrlHandler_new(AQH_SERVICE *sv)
{
  AQH_HTTP_URLHANDLER *uh;

  uh=AQH_HttpUrlHandler_new(sv);
  AQH_HttpUrlHandler_SetHandleFn(uh, _handleUrl);

  return uh;
}



int _handleUrl(AQH_HTTP_URLHANDLER *uh, AQH_HTTP_REQUEST *rq)
{
  const char *protocol;
  const char *cmd;
  GWEN_MSG *msgOut=NULL;

  protocol=AQH_HttpRequest_GetProtocol(rq);
  cmd=AQH_HttpRequest_GetCommand(rq);
  if (cmd && *cmd) {
    int rv;

    if (strcasecmp(cmd, "GET")==0) {
      rv=_handleGet(uh, rq);
      if (rv<0) {
        DBG_INFO(AQH_LOGDOMAIN, "here (%d)", rv);
        return rv;
      }
    }
    else if (strcasecmp(cmd, "POST")==0) {
      rv=_handlePost(uh, rq);
      if (rv<0) {
        DBG_INFO(AQH_LOGDOMAIN, "here (%d)", rv);
        return rv;
      }
    }
    else {
      msgOut=AQH_HttpService_CreateResponseMsg(AQH_HttpUrlHandler_GetHttpService(uh), 405, "Method not allowed", protocol, NULL);
      AQH_HttpRequest_SetResponseMsg(rq, msgOut);
    }
    return 0;
  }
  else {
    DBG_ERROR(AQH_LOGDOMAIN, "No command in request");
    return GWEN_ERROR_INVALID;
  }
}



int _handleGet(AQH_HTTP_URLHANDLER *uh, AQH_HTTP_REQUEST *rq)
{
  GWEN_BUFFER *pageBuf;
  int rv;
  GWEN_MSG *msgOut=NULL;
  const char *protocol;

  protocol=AQH_HttpRequest_GetProtocol(rq);
  pageBuf=GWEN_Buffer_new(0, 256, 0, 1);
  rv=AQH_HttpUrlHandler_AddContentHeaders(uh, AQH_HTTP_CONTENT_MODE_DESKTOP, pageBuf);
  if (rv<0) {
    DBG_ERROR(AQH_LOGDOMAIN, "Error adding headers");
    GWEN_Buffer_free(pageBuf);
    msgOut=AQH_HttpService_CreateResponseMsg(AQH_HttpUrlHandler_GetHttpService(uh), 500, "Internal Error", protocol, NULL);
    AQH_HttpRequest_SetResponseMsg(rq, msgOut);
    return 0;
  }

  rv=AQH_HttpService_AddFile(AQH_HttpUrlHandler_GetHttpService(uh), NULL, "login.html", pageBuf);
  if (rv<0) {
    DBG_ERROR(AQH_LOGDOMAIN, "Error reading file \"login.html\"");
    GWEN_Buffer_free(pageBuf);
    msgOut=AQH_HttpService_CreateResponseMsg(AQH_HttpUrlHandler_GetHttpService(uh), 500, "Internal Error", protocol, NULL);
    AQH_HttpRequest_SetResponseMsg(rq, msgOut);
    return 0;
  }

  rv=AQH_HttpUrlHandler_AddContentFooters(uh, AQH_HTTP_CONTENT_MODE_DESKTOP, pageBuf);
  if (rv<0) {
    DBG_ERROR(AQH_LOGDOMAIN, "Error adding footers");
    msgOut=AQH_HttpService_CreateResponseMsg(AQH_HttpUrlHandler_GetHttpService(uh), 500, "Internal Error", protocol, NULL);
    AQH_HttpRequest_SetResponseMsg(rq, msgOut);
    GWEN_Buffer_free(pageBuf);
    return rv;
  }

  msgOut=AQH_HttpService_CreateResponseMsg(AQH_HttpUrlHandler_GetHttpService(uh), 200, "OK", protocol, GWEN_Buffer_GetStart(pageBuf));
  GWEN_Buffer_free(pageBuf);
  AQH_HttpRequest_SetResponseMsg(rq, msgOut);

  return 0;
}



int _handlePost(AQH_HTTP_URLHANDLER *uh, AQH_HTTP_REQUEST *rq)
{
  GWEN_DB_NODE *db;
  int rv;

  DBG_ERROR(NULL, "Login POST:");
  db=AQH_HttpRequest_GetDbPostBody(rq);
  GWEN_DB_Dump(db, 2);

  rv=_loginUser(uh, rq);
  if (rv<0) {
    DBG_INFO(NULL, "here (%d)", rv);
    return rv;
  }

  return 0;
}



int _loginUser(AQH_HTTP_URLHANDLER *uh, AQH_HTTP_REQUEST *rq)
{
  AQH_SERVICE *sv;
  GWEN_MSG *msgOut=NULL;
  const char *protocol;
  GWEN_DB_NODE *db;
  const char *userName;
  const char *password;
  AQH_USER *u;
  AQH_SESSION *session;
  int i;
  int rv;

  sv=AQH_HttpUrlHandler_GetHttpService(uh);
  db=AQH_HttpRequest_GetDbPostBody(rq);
  protocol=AQH_HttpRequest_GetProtocol(rq);

  userName=GWEN_DB_GetCharValue(db, "username", 0, NULL);
  password=GWEN_DB_GetCharValue(db, "password", 0, NULL);
  if (!(userName && *userName && password && *password)) {
    msgOut=AQH_HttpService_CreateResponseMsg(sv, 401, "Unauthorized", protocol, NULL);
    AQH_HttpRequest_SetResponseMsg(rq, msgOut);
    return 0;
  }

  u=AQH_HttpService_GetUser(sv, userName);
  if (u==NULL) {
    DBG_INFO(NULL, "User \"%s\" not found", userName);
    /* TODO: sleep */
    msgOut=AQH_HttpService_CreateResponseMsg(AQH_HttpUrlHandler_GetHttpService(uh), 401, "Unauthorized", protocol, NULL);
    AQH_HttpRequest_SetResponseMsg(rq, msgOut);
    return 0;
  }

  i=AQH_User_GetState(u);
  if (i==AQH_UserState_Suspended) {
    DBG_INFO(NULL, "User \"%s\" suspended", userName);
    /* TODO: sleep */
    msgOut=AQH_HttpService_CreateResponseMsg(AQH_HttpUrlHandler_GetHttpService(uh), 401, "Unauthorized", protocol, NULL);
    AQH_HttpRequest_SetResponseMsg(rq, msgOut);
    return 0;
  }

  rv=_verifyPass(u, userName, password);
  if (rv<0) {
    if (rv==GWEN_ERROR_VERIFY)
      msgOut=AQH_HttpService_CreateResponseMsg(AQH_HttpUrlHandler_GetHttpService(uh), 401, "Unauthorized", protocol, "error");
    else
      msgOut=AQH_HttpService_CreateResponseMsg(AQH_HttpUrlHandler_GetHttpService(uh), 500, "Internal Error", protocol, NULL);
    /* TODO: sleep */
    AQH_HttpRequest_SetResponseMsg(rq, msgOut);
    return 0;
  }

  session=_generateSessionForUser(sv, u);
  if (session==NULL) {
    msgOut=AQH_HttpService_CreateResponseMsg(AQH_HttpUrlHandler_GetHttpService(uh), 500, "Internal Error", protocol, NULL);
    /* TODO: sleep */
    AQH_HttpRequest_SetResponseMsg(rq, msgOut);
    return 0;
  }

  DBG_ERROR(NULL, "Session generated");
  AQH_User_SetTimestampLastLogin(u, AQH_Session_GetTimestampCreation(session));
  rv=AQH_HttpService_WriteUser(sv, u);
  if (rv<0) {
    DBG_INFO(AQH_LOGDOMAIN, "here (%d)", rv);
    msgOut=AQH_HttpService_CreateResponseMsg(AQH_HttpUrlHandler_GetHttpService(uh), 500, "Internal Error", protocol, NULL);
    AQH_HttpRequest_SetResponseMsg(rq, msgOut);
    return 0;
  }

  msgOut=_createLoginResponseMsg(sv, protocol, AQH_Session_GetUid(session), "/rooms/list");
  AQH_HttpRequest_SetResponseMsg(rq, msgOut);

  return 0;
}



int _verifyPass(AQH_USER *u, const char *userName, const char *password)
{
  GWEN_MDIGEST *md;
  int rv;
  const char *storedPasswordHash;
  char buffer[70];

  storedPasswordHash=AQH_User_GetHashedPassword(u);
  if (!(storedPasswordHash && *storedPasswordHash)) {
    DBG_ERROR(NULL, "No password hash stored with user \"%s\"", userName);
    return GWEN_ERROR_INTERNAL;
  }

  md=GWEN_MDigest_Sha256_new();
  rv=GWEN_MDigest_Begin(md);
  if (rv<0) {
    DBG_ERROR(NULL, "Error digesting given password [begin] (%d)", rv);
    GWEN_MDigest_free(md);
    return rv;
  }

  rv=GWEN_MDigest_Update(md, (const uint8_t*) password, strlen(password));
  if (rv<0) {
    DBG_ERROR(NULL, "Error digesting given password [update] (%d)", rv);
    GWEN_MDigest_free(md);
    return rv;
  }

  rv=GWEN_MDigest_End(md);
  if (rv<0) {
    DBG_ERROR(NULL, "Error digesting given password [end] (%d)", rv);
    GWEN_MDigest_free(md);
    return rv;
  }

  DBG_ERROR(NULL, "Digest needs %d bytes", (GWEN_MDigest_GetDigestSize(md)*2)+1);
  if (NULL==GWEN_Text_ToHex((const char*) GWEN_MDigest_GetDigestPtr(md), GWEN_MDigest_GetDigestSize(md), buffer, sizeof(buffer)-1)) {
    DBG_ERROR(NULL, "Buffer too small (need %d)", (GWEN_MDigest_GetDigestSize(md)*2)+1);
    GWEN_MDigest_free(md);
    return GWEN_ERROR_INTERNAL;
  }
  GWEN_MDigest_free(md);

  if (strcasecmp(buffer, storedPasswordHash)!=0) {
    DBG_ERROR(NULL, "Bad password for user \"%s\"", userName);
    return GWEN_ERROR_VERIFY;
  }

  DBG_ERROR(NULL, "Valid password for user \"%s\"", userName);
  return 0;
}



AQH_SESSION *_generateSessionForUser(AQH_SERVICE *sv, AQH_USER *u)
{
  AQH_SESSION *session;
  GWEN_BUFFER *buf;
  GWEN_TIMESTAMP *ts;
  int rv;

  buf=_generateSessionUid();
  if (buf==NULL) {
    DBG_INFO(NULL, "here");
    return NULL;
  }
  DBG_INFO(NULL, "New session id: [%s]", GWEN_Buffer_GetStart(buf));

  session=AQH_Session_new();
  AQH_Session_SetUid(session, GWEN_Buffer_GetStart(buf));
  GWEN_Buffer_free(buf);

  ts=GWEN_Timestamp_NowInLocalTime();
  AQH_Session_SetTimestampCreation(session, ts);
  AQH_Session_SetTimestampLastAccess(session, ts);
  GWEN_Timestamp_free(ts);

  AQH_Session_SetUserAlias(session, AQH_User_GetAlias(u));
  AQH_Session_SetUser(session, u);

  rv=AQH_HttpService_AddSession(sv, session);
  if (rv<0) {
    DBG_INFO(NULL, "Error adding session \"%s\" (%d)", AQH_Session_GetUid(session), rv);
    AQH_Session_free(session);
    return NULL;
  }

  return session;
}



GWEN_BUFFER *_generateSessionUid(void)
{
  uint8_t binbuf[8];
  GWEN_BUFFER *buf;
  int rv;

  rv=GWEN_SyncIo_Helper_PartiallyReadFile("/dev/urandom", binbuf, sizeof(binbuf));
  if (rv<sizeof(binbuf)) {
    DBG_ERROR(NULL, "Error reading from /dev/urandom: %d", rv);
    return NULL;
  }

  buf=GWEN_Buffer_new(0, 20, 0, 1);
  rv=GWEN_Text_ToHexBuffer((const char*) binbuf, rv, buf, 0, 0, 0);
  if (rv<0) {
    DBG_ERROR(NULL, "Error converting random bytes to hex (%d)", rv);
    GWEN_Buffer_free(buf);
    return NULL;
  }

  return buf;
}



GWEN_MSG *_createLoginResponseMsg(AQH_SERVICE *sv, const char *protocol, const char *sessionId, const char *newPage)
{
  GWEN_BUFFER *buf;
  GWEN_MSG *msg;
  GWEN_DB_NODE *db;

  buf=GWEN_Buffer_new(0, 256, 0, 1);
  AQH_HttpService_AddStatusLine(sv, 303, "OK, redirecting to content", protocol, buf);
  db=GWEN_DB_Group_new("header");
  GWEN_DB_SetCharValue(db, GWEN_DB_FLAGS_OVERWRITE_VARS, "Connection", "Keep-Alive");
  GWEN_DB_SetCharValue(db, GWEN_DB_FLAGS_OVERWRITE_VARS, "Content-Type", "text/html; charset=utf-8");
  _headerSetCookie(db, GWEN_DB_FLAGS_OVERWRITE_VARS, AQH_HTTP_REQUEST_SESSIONCOOKIE, sessionId);
  GWEN_DB_SetCharValue(db, GWEN_DB_FLAGS_OVERWRITE_VARS, "Location", newPage);
  GWEN_DB_SetIntValue(db, GWEN_DB_FLAGS_OVERWRITE_VARS, "Content-Length", 0);
  AQH_HttpService_AddHeader(sv, db, buf);
  GWEN_DB_Group_free(db);

  msg=GWEN_Msg_fromBytes((const uint8_t*)GWEN_Buffer_GetStart(buf), GWEN_Buffer_GetUsedBytes(buf));
  GWEN_Buffer_free(buf);
  return msg;
}



void _headerSetCookie(GWEN_DB_NODE *db, uint32_t flags, const char *cookieName, const char *cookieValue)
{
  if (cookieName && cookieValue) {
    GWEN_BUFFER *dbuf;

    dbuf=GWEN_Buffer_new(0, 32, 0, 1);
    GWEN_Buffer_AppendArgs(dbuf, "%s=%s", cookieName, cookieValue);
    GWEN_DB_SetCharValue(db, flags, "Set-Cookie", GWEN_Buffer_GetStart(dbuf));
    GWEN_Buffer_free(dbuf);
  }
}



