/*
 * ZoneMinder regular expression class implementation, $Date$, $Revision$
 * Copyright (C) 2001-2008 Philip Coombes
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * 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.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

#include "zm_user.h"

#include "zm_crypt.h"
#include "zm_logger.h"
#include "zm_time.h"
#include "zm_utils.h"
#include <cstring>

User::User() : id(0), enabled(false), role_id(0) {
  username[0] = password[0] = 0;
  stream = events = control = monitors = system = PERM_NONE;
}

User::User(const MYSQL_ROW &dbrow) {
  int index = 0;
  id = atoi(dbrow[index++]);
  strncpy(username, dbrow[index++], sizeof(username)-1);
  strncpy(password, dbrow[index++], sizeof(password)-1);
  enabled = static_cast<bool>(atoi(dbrow[index++]));
  stream = (Permission)atoi(dbrow[index++]);
  events = (Permission)atoi(dbrow[index++]);
  control = (Permission)atoi(dbrow[index++]);
  monitors = (Permission)atoi(dbrow[index++]);
  system = (Permission)atoi(dbrow[index++]);
  role_id = atoi(dbrow[index++]);
  monitor_permissions_loaded = false;
  group_permissions_loaded = false;
  role_monitor_permissions_loaded = false;
  role_group_permissions_loaded = false;

  if (role_id > 0) {
    loadRoleBasePermissions();
  }
}

User::~User() {
}

void User::Copy(const User &u) {
  id = u.id;
  strncpy(username, u.username, sizeof(username));
  strncpy(password, u.password, sizeof(password));
  enabled = u.enabled;
  stream = u.stream;
  events = u.events;
  control = u.control;
  monitors = u.monitors;
  system = u.system;
  role_id = u.role_id;
  monitor_permissions_loaded = u.monitor_permissions_loaded;
  monitor_permissions = u.monitor_permissions;
  group_permissions_loaded = u.group_permissions_loaded;
  group_permissions = u.group_permissions;
  role_monitor_permissions_loaded = u.role_monitor_permissions_loaded;
  role_monitor_permissions = u.role_monitor_permissions;
  role_group_permissions_loaded = u.role_group_permissions_loaded;
  role_group_permissions = u.role_group_permissions;
}

void User::loadMonitorPermissions() {
  for (const Monitor_Permission &p : Monitor_Permission::find(id) ) {
    monitor_permissions[p.MonitorId()] = p;
  }
  Debug(1, "# of Monitor_Permissions %zu", monitor_permissions.size());
}

void User::loadGroupPermissions() {
  group_permissions = Group_Permission::find(id);
  Debug(1, "# of Group_Permissions %zu", group_permissions.size());
}

void User::loadRoleBasePermissions() {
  std::string sql = stringtf("SELECT `Stream`+0, `Events`+0, `Control`+0, `Monitors`+0, `System`+0"
                             " FROM `User_Roles` WHERE `Id` = %d", role_id);
  MYSQL_RES *result = zmDbFetch(sql);
  if (!result) return;
  if (mysql_num_rows(result) != 1) {
    mysql_free_result(result);
    return;
  }
  MYSQL_ROW dbrow = mysql_fetch_row(result);
  Permission role_stream = (Permission)atoi(dbrow[0]);
  Permission role_events = (Permission)atoi(dbrow[1]);
  Permission role_control = (Permission)atoi(dbrow[2]);
  Permission role_monitors = (Permission)atoi(dbrow[3]);
  Permission role_system = (Permission)atoi(dbrow[4]);
  mysql_free_result(result);

  // Use role permission as fallback when user permission is PERM_NONE
  if (stream == PERM_NONE && role_stream > PERM_NONE) stream = role_stream;
  if (events == PERM_NONE && role_events > PERM_NONE) events = role_events;
  if (control == PERM_NONE && role_control > PERM_NONE) control = role_control;
  if (monitors == PERM_NONE && role_monitors > PERM_NONE) monitors = role_monitors;
  if (system == PERM_NONE && role_system > PERM_NONE) system = role_system;

  Debug(1, "Merged role %d base permissions: stream=%d events=%d control=%d monitors=%d system=%d",
        role_id, stream, events, control, monitors, system);
}

void User::loadRoleMonitorPermissions() {
  for (const Monitor_Permission &p : Monitor_Permission::findByRole(role_id)) {
    role_monitor_permissions[p.MonitorId()] = p;
  }
  role_monitor_permissions_loaded = true;
  Debug(1, "# of Role_Monitor_Permissions %zu", role_monitor_permissions.size());
}

void User::loadRoleGroupPermissions() {
  role_group_permissions = Group_Permission::findByRole(role_id);
  role_group_permissions_loaded = true;
  Debug(1, "# of Role_Group_Permissions %zu", role_group_permissions.size());
}

bool User::canAccess(int monitor_id) {
  if (!monitor_permissions_loaded) loadMonitorPermissions();
  auto it = monitor_permissions.find(monitor_id);

  if (it != monitor_permissions.end()) {
    auto permission = it->second.getPermission();
    switch (permission) {
    case Monitor_Permission::PERM_NONE :
      Debug(1, "Returning None from monitor_permission");
      return false;
    case Monitor_Permission::PERM_VIEW :
      Debug(1, "Returning true because VIEW from monitor_permission");
      return true;
    case Monitor_Permission::PERM_EDIT :
      Debug(1, "Returning true because EDIT from monitor_permission");
      return true;
    case Monitor_Permission::PERM_INHERIT :
      Debug(1, "INHERIT from monitor_permission");
      break;
    default:
      Warning("UNKNOWN permission %d from monitor_permission", permission);
      break;
    }
  }

  if (!group_permissions_loaded) loadGroupPermissions();

  for (Group_Permission &gp : group_permissions) {
    auto permission = gp.getPermission(monitor_id);
    switch (permission) {
    case Group_Permission::PERM_NONE :
      Debug(1, "Returning None from group_permission");
      return false;
    case Group_Permission::PERM_VIEW :
      Debug(1, "Returning true because VIEW from group_permission");
      return true;
    case Group_Permission::PERM_EDIT :
      Debug(1, "Returning true because EDIT from group_permission");
      return true;
    case Group_Permission::PERM_INHERIT :
      Debug(1, "INHERIT from group_permission %d", gp.GroupId());
      break;
    default :
      Warning("UNKNOWN permission %d from group_permission %d", permission, gp.GroupId());
      break;
    }
  }  // end foreach Group_Permission

  // Check role permissions if user has a role
  if (role_id > 0) {
    if (!role_monitor_permissions_loaded) loadRoleMonitorPermissions();
    auto rit = role_monitor_permissions.find(monitor_id);

    if (rit != role_monitor_permissions.end()) {
      auto permission = rit->second.getPermission();
      switch (permission) {
      case Monitor_Permission::PERM_NONE :
        Debug(1, "Returning None from role_monitor_permission");
        return false;
      case Monitor_Permission::PERM_VIEW :
        Debug(1, "Returning true because VIEW from role_monitor_permission");
        return true;
      case Monitor_Permission::PERM_EDIT :
        Debug(1, "Returning true because EDIT from role_monitor_permission");
        return true;
      case Monitor_Permission::PERM_INHERIT :
        Debug(1, "INHERIT from role_monitor_permission");
        break;
      default:
        Warning("UNKNOWN permission %d from role_monitor_permission", permission);
        break;
      }
    }

    if (!role_group_permissions_loaded) loadRoleGroupPermissions();

    for (Group_Permission &gp : role_group_permissions) {
      auto permission = gp.getPermission(monitor_id);
      switch (permission) {
      case Group_Permission::PERM_NONE :
        Debug(1, "Returning None from role_group_permission");
        return false;
      case Group_Permission::PERM_VIEW :
        Debug(1, "Returning true because VIEW from role_group_permission");
        return true;
      case Group_Permission::PERM_EDIT :
        Debug(1, "Returning true because EDIT from role_group_permission");
        return true;
      case Group_Permission::PERM_INHERIT :
        Debug(1, "INHERIT from role_group_permission %d", gp.GroupId());
        break;
      default :
        Warning("UNKNOWN permission %d from role_group_permission %d", permission, gp.GroupId());
        break;
      }
    }  // end foreach role Group_Permission
  }  // end if role_id

  return (monitors != PERM_NONE);
}

User *User::find(const std::string &username) {
  std::string escaped_username = zmDbEscapeString(username);

  std::string sql = stringtf("SELECT `Id`, `Username`, `Password`, `Enabled`,"
                             " `Stream`+0, `Events`+0, `Control`+0, `Monitors`+0, `System`+0,"
                             " COALESCE(`RoleId`, 0)"
                             " FROM `Users` WHERE `Username` = '%s' AND `Enabled` = 1",
                             escaped_username.c_str());
  MYSQL_RES *result = zmDbFetch(sql);
  if (!result)
    return nullptr;
  if (mysql_num_rows(result) != 1) {
    mysql_free_result(result);
    return nullptr;
  }
  MYSQL_ROW dbrow = mysql_fetch_row(result);
  User *user = new User(dbrow);
  mysql_free_result(result);
  return user;
}

User *User::find(int id) {
  std::string sql = stringtf("SELECT `Id`, `Username`, `Password`, `Enabled`,"
                             " `Stream`+0, `Events`+0, `Control`+0, `Monitors`+0, `System`+0,"
                             " COALESCE(`RoleId`, 0)"
                             " FROM `Users` WHERE `Id` = %d AND `Enabled` = 1",
                             id);
  MYSQL_RES *result = zmDbFetch(sql);
  if (!result)
    return nullptr;
  if (mysql_num_rows(result) != 1) {
    mysql_free_result(result);
    return nullptr;
  }
  MYSQL_ROW dbrow = mysql_fetch_row(result);
  User *user = new User(dbrow);
  mysql_free_result(result);
  return user;
}

std::string User::getAuthHash() {
  SystemTimePoint now = std::chrono::system_clock::now();
  time_t now_t = std::chrono::system_clock::to_time_t(now);
  tm now_tm = {};
  localtime_r(&now_t, &now_tm);
  std::string auth_key = stringtf("%s%s%s%s%d%d%d%d",
      config.auth_hash_secret,
      username,
      password,
      (config.auth_hash_ips ? "127.0.0.1" : ""),
      now_tm.tm_hour,
      now_tm.tm_mday,
      now_tm.tm_mon,
      now_tm.tm_year);
  Debug(1, "Creating auth_key '%s'", auth_key.c_str());

  zm::crypto::MD5::Digest md5_digest = zm::crypto::MD5::GetDigestOf(auth_key);
  return ByteArrayToHexString(md5_digest);
}

// Function to load a user from username and password
// Please note that in auth relay mode = none, password is NULL
User *zmLoadUser(const std::string &username, const std::string &password) {
  User *user = User::find(username);
  if (!user) return nullptr;

  if (
      (password.empty() and (!strcmp(config.auth_relay, "none")))  // relay type must be none
      ||
      verifyPassword(username.c_str(), password.c_str(), user->getPassword()) ) {
    Debug(1, "Authenticated user '%s'", user->getUsername());
    return user;
  }
  delete user;
  Warning("Unable to authenticate user %s", username.c_str());
  return nullptr;
}  // end User *zmLoadUser(const char *username, const char *password)

User *zmLoadTokenUser(const std::string &jwt_token_str, bool use_remote_addr) {
  std::string key = config.auth_hash_secret;
  std::string remote_addr;

  if ( use_remote_addr ) {
    const char *remote_addr_env = getenv("REMOTE_ADDR");
    if (remote_addr_env) {
      remote_addr = remote_addr_env;
      key += remote_addr;
    } else {
      Warning("Can't determine remote address, using null");
    }
  }

  Debug(1, "Inside zmLoadTokenUser, formed key=%s", key.c_str());

  std::pair<std::string, unsigned int> ans = verifyToken(jwt_token_str, key);
  std::string username = zmDbEscapeString(ans.first);
  unsigned int iat = ans.second;
  Debug(1, "retrieved user '%s' from token", username.c_str());

  if ( username == "" ) {
    return nullptr;
  }

  std::string sql = stringtf("SELECT `Id`, `Username`, `Password`, `Enabled`, `Stream`+0, `Events`+0,"
                             " `Control`+0, `Monitors`+0, `System`+0, COALESCE(`RoleId`, 0),"
                             " `TokenMinExpiry`"
                             " FROM `Users` WHERE `Username` = '%s' AND `Enabled` = 1", username.c_str());

  MYSQL_RES *result = zmDbFetch(sql);
  if (!result)
    return nullptr;

  int n_users = mysql_num_rows(result);
  if ( n_users != 1 ) {
    mysql_free_result(result);
    Error("Unable to authenticate user '%s'", username.c_str());
    return nullptr;
  }

  MYSQL_ROW dbrow = mysql_fetch_row(result);
  User *user = new User(dbrow);
  unsigned int stored_iat = strtoul(dbrow[10], nullptr, 0);

  if ( stored_iat > iat ) { // admin revoked tokens
    mysql_free_result(result);
    delete user;
    Error("Token was revoked for '%s'", username.c_str());
    return nullptr;
  }

  Debug(1, "Authenticated user '%s' via token with last revoke time: %u",
        username.c_str(), stored_iat);
  mysql_free_result(result);
  return user;
}  // User *zmLoadTokenUser(std::string jwt_token_str, bool use_remote_addr)

// Function to validate an authentication string
User *zmLoadAuthUser(const std::string &auth, const std::string &username, bool use_remote_addr) {
  const char *remote_addr = "";
  std::string forwarded_for_first; // must outlive remote_addr
  if (use_remote_addr) {
    // Prefer HTTP_X_FORWARDED_FOR if available, consistent with PHP session.php which uses it for hash generation.
    // This ensures that when ZoneMinder is behind a reverse proxy (e.g. Nginx → Apache),
    // the same IP address is used for both hash generation and validation.
    // Use only the first IP to guard against spoofed multi-value headers.
    const char *forwarded_for = getenv("HTTP_X_FORWARDED_FOR");
    if (forwarded_for && *forwarded_for) {
      // Extract the first IP from a potentially comma-separated list
      forwarded_for_first = forwarded_for;
      auto comma = forwarded_for_first.find(',');
      if (comma != std::string::npos) {
        forwarded_for_first = forwarded_for_first.substr(0, comma);
      }
      // Trim leading/trailing whitespace
      auto start = forwarded_for_first.find_first_not_of(' ');
      auto end = forwarded_for_first.find_last_not_of(' ');
      if (start != std::string::npos) {
        forwarded_for_first = forwarded_for_first.substr(start, end - start + 1);
      }
      remote_addr = forwarded_for_first.c_str();
    } else {
      remote_addr = getenv("REMOTE_ADDR");
      if (!remote_addr) {
        Warning("Can't determine remote address, using null");
        remote_addr = "";
      }
    }
  }

  Debug(1, "Attempting to authenticate user %s from auth string '%s', remote addr(%s)",
        username.c_str(), auth.c_str(), remote_addr);
  std::string sql = "SELECT `Id`, `Username`, `Password`, `Enabled`,"
                    " `Stream`+0, `Events`+0, `Control`+0, `Monitors`+0, `System`+0,"
                    " COALESCE(`RoleId`, 0)"
                    " FROM `Users` WHERE `Enabled` = 1";
  if (!username.empty()) {
    sql += " AND `Username`='"+zmDbEscapeString(username)+"'";
  }

  MYSQL_RES *result = zmDbFetch(sql);
  if (!result)
    return nullptr;

  int n_users = mysql_num_rows(result);
  if (n_users < 1) {
    mysql_free_result(result);
    Warning("Unable to authenticate user");
    return nullptr;
  }

  SystemTimePoint now = std::chrono::system_clock::now();
  Hours hours = Hours(config.auth_hash_ttl);

  if (hours == Hours(0)) {
    Warning("No value set for ZM_AUTH_HASH_TTL. Defaulting to 2.");
    hours = Hours(2);
  } else {
    Debug(1, "AUTH_HASH_TTL is %" PRIi64 " h, time is %" PRIi64 " s",
          static_cast<int64>(Hours(hours).count()),
          static_cast<int64>(std::chrono::duration_cast<Seconds>(now.time_since_epoch()).count()));
  }

  while (MYSQL_ROW dbrow = mysql_fetch_row(result)) {
    const char *db_username = dbrow[1];
    const char *db_password = dbrow[2];

    SystemTimePoint our_now = now;
    tm now_tm = {};
    for (Hours i = Hours(0); i < hours; i++, our_now -= Hours(1)) {
      time_t our_now_t = std::chrono::system_clock::to_time_t(our_now);
      localtime_r(&our_now_t, &now_tm);

      std::string auth_key = stringtf("%s%s%s%s%d%d%d%d",
                                      config.auth_hash_secret,
                                      db_username,
                                      db_password,
                                      remote_addr,
                                      now_tm.tm_hour,
                                      now_tm.tm_mday,
                                      now_tm.tm_mon,
                                      now_tm.tm_year);

      zm::crypto::MD5::Digest md5_digest = zm::crypto::MD5::GetDigestOf(auth_key);
      std::string auth_md5 = ByteArrayToHexString(md5_digest);

      Debug(1, "Checking auth_key '%s' -> auth_md5 '%s' == '%s'", auth_key.c_str(), auth_md5.c_str(), auth.c_str());

      if (auth == auth_md5) {
        // We have a match
        User *user = new User(dbrow);
        Debug(1, "Authenticated user '%s'", user->getUsername());
        mysql_free_result(result);
        return user;
      } else {
        Debug(1, "No match for %s", auth.c_str());
      }
    }  // end foreach hour
  }  // end foreach user
  mysql_free_result(result);

  Debug(1, "No user found for auth_key %s", auth.c_str());
  return nullptr;
}  // end User *zmLoadAuthUser( const std::string &auth, const std::string &username, bool use_remote_addr )

// Function to check Username length
bool checkUser(const std::string &username) {
  if (username.empty())
    return false;
  if (username.length() > 32)
    return false;

  return true;
}

// Function to check password length
bool checkPass(const std::string &password) {
  if (password.empty())
    return false;
  if (password.length() > 64)
    return false;

  return true;
}
