/*
   BAREOS® - Backup Archiving REcovery Open Sourced

   Copyright (C) 2026-2026 Bareos GmbH & Co. KG

   This program is Free Software; you can redistribute it and/or
   modify it under the terms of version three of the GNU Affero General Public
   License as published by the Free Software Foundation and included
   in the file LICENSE.

   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
   Affero General Public License for more details.

   You should have received a copy of the GNU Affero 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 "proxy_config.h"

#include <algorithm>
#include <cctype>
#include <fstream>
#include <limits>
#include <sstream>
#include <stdexcept>
#include <string>

namespace {

std::string Trim(std::string value)
{
  auto is_space = [](unsigned char ch) { return std::isspace(ch) != 0; };
  value.erase(value.begin(),
              std::find_if_not(value.begin(), value.end(), is_space));
  value.erase(std::find_if_not(value.rbegin(), value.rend(), is_space).base(),
              value.end());
  return value;
}

std::string StripQuotes(std::string value)
{
  value = Trim(std::move(value));
  if (value.size() >= 2 && value.front() == '"' && value.back() == '"') {
    return value.substr(1, value.size() - 2);
  }
  return value;
}

bool ParseBool(const std::string& value, const std::string& key)
{
  const std::string lower = [&]() {
    std::string normalized = value;
    std::transform(normalized.begin(), normalized.end(), normalized.begin(),
                   [](unsigned char ch) { return std::tolower(ch); });
    return normalized;
  }();

  if (lower == "true" || lower == "yes" || lower == "on" || lower == "1") {
    return true;
  }
  if (lower == "false" || lower == "no" || lower == "off" || lower == "0") {
    return false;
  }
  throw std::runtime_error("Proxy config: '" + key + "' must be a boolean");
}

int ParseInteger(const std::string& value, const std::string& key)
{
  try {
    size_t pos = 0;
    const int parsed = std::stoi(value, &pos, 10);
    if (pos != value.size()) { throw std::invalid_argument("trailing"); }
    return parsed;
  } catch (...) {
    throw std::runtime_error("Proxy config: '" + key + "' must be an integer");
  }
}

std::time_t ParseTimestamp(const std::string& value, const std::string& key)
{
  try {
    size_t pos = 0;
    const long long parsed = std::stoll(value, &pos, 10);
    if (pos != value.size()) { throw std::invalid_argument("trailing"); }
    if (parsed < std::numeric_limits<std::time_t>::min()
        || parsed > std::numeric_limits<std::time_t>::max()) {
      throw std::out_of_range("range");
    }
    return static_cast<std::time_t>(parsed);
  } catch (...) {
    throw std::runtime_error("Proxy config: '" + key
                             + "' must be a unix timestamp");
  }
}

std::vector<std::string> ParseList(const std::string& value)
{
  std::vector<std::string> entries;
  std::istringstream input(value);
  std::string item;
  while (std::getline(input, item, ',')) {
    item = Trim(std::move(item));
    if (!item.empty()) { entries.push_back(item); }
  }
  return entries;
}

void ApplyProxySetting(ProxyConfig& cfg,
                       const std::string& key,
                       const std::string& value)
{
  if (key == "ws_host") {
    cfg.bind_host = value;
  } else if (key == "ws_port") {
    cfg.port = ParseInteger(value, key);
  } else if (key == "session_ttl") {
    cfg.session_ttl = ParseInteger(value, key);
  } else {
    throw std::runtime_error("Proxy config: unknown key in [listen]: '" + key
                             + "'");
  }
}

void ApplyDirectorSetting(DirectorTargetConfig& cfg,
                          const std::string& key,
                          const std::string& value)
{
  if (key == "host") {
    cfg.host = value;
  } else if (key == "port") {
    cfg.port = ParseInteger(value, key);
  } else if (key == "director_name") {
    cfg.name = value;
  } else if (key == "tls_psk_disable") {
    cfg.tls_psk_disable = ParseBool(value, key);
  } else {
    throw std::runtime_error("Proxy config: unknown director key '" + key
                             + "'");
  }
  cfg.tls_psk_require = !cfg.tls_psk_disable;
}

void ApplyIdentityMappingSetting(IdentityMappingRule& rule,
                                 const std::string& key,
                                 const std::string& value)
{
  if (key == "provider") {
    rule.provider = value;
  } else if (key == "subject") {
    rule.subject = value;
  } else if (key == "username") {
    rule.username = value;
  } else if (key == "email") {
    rule.email = value;
  } else if (key == "groups") {
    rule.groups = ParseList(value);
  } else if (key == "roles") {
    rule.roles = ParseList(value);
  } else if (key == "director_username") {
    rule.director_username = value;
  } else if (key == "director_password") {
    rule.director_password = value;
  } else if (key == "preferred_director") {
    rule.preferred_director_id = value;
  } else {
    throw std::runtime_error("Proxy config: unknown identity map key '" + key
                             + "'");
  }
}

void ApplyLocalAuthUserSetting(LocalAuthUser& user,
                               const std::string& key,
                               const std::string& value)
{
  if (key == "username") {
    user.username = value;
  } else if (key == "password_hash") {
    user.password_hash = value;
  } else if (key == "subject") {
    user.subject = value;
  } else if (key == "email") {
    user.email = value;
  } else if (key == "groups") {
    user.groups = ParseList(value);
  } else if (key == "roles") {
    user.roles = ParseList(value);
  } else {
    throw std::runtime_error("Proxy config: unknown auth user key '" + key
                             + "'");
  }
}

void ApplyTokenAuthSetting(TokenAuthEntry& token,
                           const std::string& key,
                           const std::string& value)
{
  if (key == "token_hash") {
    token.token_hash = value;
  } else if (key == "subject") {
    token.subject = value;
  } else if (key == "username") {
    token.username = value;
  } else if (key == "email") {
    token.email = value;
  } else if (key == "groups") {
    token.groups = ParseList(value);
  } else if (key == "roles") {
    token.roles = ParseList(value);
  } else if (key == "expires_at") {
    token.expires_at = ParseTimestamp(value, key);
  } else {
    throw std::runtime_error("Proxy config: unknown auth token key '" + key
                             + "'");
  }
}

void ApplyOidcProviderSetting(OidcAuthProvider& provider,
                              const std::string& key,
                              const std::string& value)
{
  if (key == "display_name") {
    provider.display_name = value;
  } else if (key == "issuer") {
    provider.issuer = value;
  } else if (key == "authorization_endpoint") {
    provider.authorization_endpoint = value;
  } else if (key == "token_endpoint") {
    provider.token_endpoint = value;
  } else if (key == "jwks_uri") {
    provider.jwks_uri = value;
  } else if (key == "client_id") {
    provider.client_id = value;
  } else if (key == "client_secret") {
    provider.client_secret = value;
  } else if (key == "redirect_uri") {
    provider.redirect_uri = value;
  } else if (key == "scopes") {
    provider.scopes = ParseList(value);
  } else {
    throw std::runtime_error("Proxy config: unknown OIDC provider key '" + key
                             + "'");
  }
}

void ApplyParsedProxyConfig(const std::string& ini, ProxyConfig& cfg)
{
  std::istringstream input(ini);
  std::string line;
  std::string current_section;
  size_t line_number = 0;

  while (std::getline(input, line)) {
    ++line_number;
    const auto comment_pos = line.find_first_of("#;");
    if (comment_pos != std::string::npos) {
      line = line.substr(0, comment_pos);
    }
    line = Trim(std::move(line));
    if (line.empty()) { continue; }

    if (line.front() == '[' && line.back() == ']') {
      current_section = Trim(line.substr(1, line.size() - 2));
      continue;
    }

    const auto eq_pos = line.find('=');
    if (eq_pos == std::string::npos) {
      throw std::runtime_error("Proxy config: invalid line "
                               + std::to_string(line_number));
    }

    const std::string key = Trim(line.substr(0, eq_pos));
    const std::string value = StripQuotes(line.substr(eq_pos + 1));

    if (current_section == "listen") {
      ApplyProxySetting(cfg, key, value);
      continue;
    }
    if (current_section.rfind("director:", 0) == 0) {
      const std::string director_id = current_section.substr(9);
      auto [it, inserted]
          = cfg.allowed_directors.emplace(director_id, DirectorTargetConfig{});
      if (inserted) {
        it->second.id = director_id;
        it->second.name = director_id;
      }
      ApplyDirectorSetting(it->second, key, value);
      continue;
    }
    if (current_section.rfind("auth-user:", 0) == 0) {
      const std::string user_id = current_section.substr(10);
      auto it = std::find_if(
          cfg.local_auth_users.begin(), cfg.local_auth_users.end(),
          [&](const auto& user) { return user.id == user_id; });
      if (it == cfg.local_auth_users.end()) {
        LocalAuthUser user;
        user.id = user_id;
        cfg.local_auth_users.push_back(std::move(user));
        it = std::prev(cfg.local_auth_users.end());
      }
      ApplyLocalAuthUserSetting(*it, key, value);
      continue;
    }
    if (current_section.rfind("auth-token:", 0) == 0) {
      const std::string token_id = current_section.substr(11);
      auto it = std::find_if(
          cfg.token_auth_entries.begin(), cfg.token_auth_entries.end(),
          [&](const auto& token) { return token.id == token_id; });
      if (it == cfg.token_auth_entries.end()) {
        TokenAuthEntry token;
        token.id = token_id;
        cfg.token_auth_entries.push_back(std::move(token));
        it = std::prev(cfg.token_auth_entries.end());
      }
      ApplyTokenAuthSetting(*it, key, value);
      continue;
    }
    if (current_section.rfind("auth-oidc:", 0) == 0) {
      const std::string provider_id = current_section.substr(10);
      auto it = std::find_if(
          cfg.oidc_auth_providers.begin(), cfg.oidc_auth_providers.end(),
          [&](const auto& provider) { return provider.id == provider_id; });
      if (it == cfg.oidc_auth_providers.end()) {
        OidcAuthProvider provider;
        provider.id = provider_id;
        provider.display_name = provider_id;
        cfg.oidc_auth_providers.push_back(std::move(provider));
        it = std::prev(cfg.oidc_auth_providers.end());
      }
      ApplyOidcProviderSetting(*it, key, value);
      continue;
    }
    if (current_section.rfind("identity-map:", 0) == 0) {
      const std::string mapping_id = current_section.substr(13);
      auto it = std::find_if(
          cfg.identity_mappings.begin(), cfg.identity_mappings.end(),
          [&](const auto& rule) { return rule.id == mapping_id; });
      if (it == cfg.identity_mappings.end()) {
        IdentityMappingRule rule;
        rule.id = mapping_id;
        cfg.identity_mappings.push_back(std::move(rule));
        it = std::prev(cfg.identity_mappings.end());
      }
      ApplyIdentityMappingSetting(*it, key, value);
      continue;
    }

    throw std::runtime_error("Proxy config: unknown section '" + current_section
                             + "'");
  }

  if (cfg.allowed_directors.empty()) {
    throw std::runtime_error(
        "Proxy config: at least one [director:<id>] section is required");
  }
  for (const auto& user : cfg.local_auth_users) {
    if (user.username.empty() || user.password_hash.empty()) {
      throw std::runtime_error("Proxy config: auth user '" + user.id
                               + "' requires username and password_hash");
    }
  }
  for (const auto& token : cfg.token_auth_entries) {
    if (token.token_hash.empty()) {
      throw std::runtime_error("Proxy config: auth token '" + token.id
                               + "' requires token_hash");
    }
  }
  for (auto& provider : cfg.oidc_auth_providers) {
    if (provider.authorization_endpoint.empty() || provider.client_id.empty()
        || provider.redirect_uri.empty()) {
      throw std::runtime_error("Proxy config: OIDC provider '" + provider.id
                               + "' requires authorization_endpoint, "
                                 "client_id, and redirect_uri");
    }
    if (provider.scopes.empty()) {
      provider.scopes = {"openid", "profile", "email"};
    }
  }
  for (const auto& rule : cfg.identity_mappings) {
    if (rule.director_username.empty() || rule.director_password.empty()) {
      throw std::runtime_error("Proxy config: identity map '" + rule.id
                               + "' requires director_username and "
                                 "director_password");
    }
  }
}

}  // namespace

void LoadProxyConfigFile(const std::string& path, ProxyConfig& cfg)
{
  std::ifstream input(path);
  if (!input) {
    throw std::runtime_error("Proxy config: cannot load '" + path + "'");
  }
  std::ostringstream contents;
  contents << input.rdbuf();
  ApplyParsedProxyConfig(contents.str(), cfg);
}

void LoadProxyConfigFromString(const std::string& ini, ProxyConfig& cfg)
{
  ApplyParsedProxyConfig(ini, cfg);
}

DirectorTargetConfig ResolveDirectorTarget(
    const ProxyConfig& cfg,
    const std::optional<std::string>& requested_director,
    const std::optional<std::string>& requested_host,
    const std::optional<int>& requested_port)
{
  if (requested_host || requested_port) {
    throw std::runtime_error(
        "Proxy config: host and port overrides are not allowed");
  }

  std::string director_id;
  if (requested_director && !requested_director->empty()) {
    director_id = *requested_director;
  } else if (cfg.allowed_directors.size() == 1) {
    director_id = cfg.allowed_directors.begin()->first;
  } else {
    throw std::runtime_error(
        "Proxy config: no director selected from the allowlist");
  }

  auto it = cfg.allowed_directors.find(director_id);
  if (it == cfg.allowed_directors.end()) {
    throw std::runtime_error("Proxy config: director '" + director_id
                             + "' is not in the allowlist");
  }

  return it->second;
}

std::vector<std::string> GetAllowedDirectorIds(const ProxyConfig& cfg)
{
  std::vector<std::string> ids;
  ids.reserve(cfg.allowed_directors.size());
  for (const auto& [id, _] : cfg.allowed_directors) { ids.push_back(id); }
  return ids;
}
