/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2017 Red Hat, Inc.
 *
 * Cockpit is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

#include "cockpitmachinesjson.h"

#include <errno.h>
#include <glob.h>

static int
glob_err_func (const char *epath,
               int eerrno)
{
  /* Should Not Happen™ -- log the error for debugging */
  if (eerrno != ENOENT)
    g_warning ("%s: cannot read: %s", epath, g_strerror (eerrno));
  return 0;
}

static JsonNode *
new_object_node (void)
{
  JsonNode *n = json_node_new (JSON_NODE_OBJECT);
  json_node_take_object (n, json_object_new ());
  return n;
}

static JsonNode *
parse_json_file (const char *path)
{
  JsonParser *parser = NULL;
  GError *error = NULL;
  gboolean success;
  JsonNode *result = NULL;

  parser = json_parser_new ();
  success = json_parser_load_from_file (parser, path, &error);
  if (success)
    {
      result = json_parser_get_root (parser);
      /* root is NULL if the file is empty */
      if (result != NULL)
        {
          if (JSON_NODE_HOLDS_OBJECT (result))
            {
              result = json_node_copy (result);
            }
          else
            {
              g_message ("%s: does not contain a JSON object, ignoring", path);
              result = NULL;
            }
        }
    }
  else
    {
      if (error->code != G_FILE_ERROR_NOENT)
        g_message ("%s: invalid JSON: %s", path, error->message);
      g_error_free (error);
    }

  g_object_unref (parser);
  return result;
}

static gboolean
write_json_file (JsonNode *config, const char *path, GError **error)
{
  JsonGenerator *json_gen;
  gboolean res;

  json_gen = json_generator_new ();
  json_generator_set_root (json_gen, config);
  json_generator_set_pretty (json_gen, TRUE); /* bikeshed zone */
  res = json_generator_to_file (json_gen, path, error);
  g_object_unref (json_gen);
  return res;
}

static void
merge_config (JsonObject *machines,
              JsonObject *delta,
              const char *path)
{
  GList *hosts = json_object_get_members (delta);
  for (GList *i = g_list_first (hosts); i; i = g_list_next (i))
    {
      const char *hostname = i->data;
      JsonNode *delta_props = json_object_get_member (delta, hostname);

      if (!JSON_NODE_HOLDS_OBJECT (delta_props))
        {
          g_message ("%s: host name definition %s does not contain a JSON object, ignoring", path, hostname);
          continue;
        }

      /* merge delta properties info existing machines host */
      if (!json_object_has_member (machines, hostname))
        json_object_set_member (machines, hostname, new_object_node ());
      JsonObject *machine_props = json_object_get_object_member (machines, hostname);

      g_debug ("%s: merging updates to host name %s", path, hostname);
      GList *proplist = json_object_get_members (json_node_get_object (delta_props));
      for (GList *p = g_list_first (proplist); p; p = g_list_next (p))
        {
          const char *propname = p->data;
          JsonNode *prop_node = json_object_get_member (json_node_get_object (delta_props), propname);

          if (!JSON_NODE_HOLDS_VALUE (prop_node))
            {
              g_message ("%s: host name definition %s: property %s does not contain a simple value, ignoring", path, hostname, propname);
              continue;
            }

          g_debug ("%s:  host name %s: merging property %s", path, hostname, propname);
          json_object_set_member (machine_props, propname, json_node_copy (prop_node));
        }

      g_list_free (proplist);
    }

  g_list_free (hosts);
}

const char *
get_machines_json_dir (void)
{
  static gchar *path = NULL;
  if (path == NULL)
    path = g_strdup_printf ("%s/machines.d", g_getenv ("COCKPIT_TEST_CONFIG_DIR") ?: "/etc/cockpit");
  return path;
}

JsonNode *
read_machines_json (void)
{
  gchar *glob_str;
  glob_t conf_glob = { .gl_offs = 1 };
  int res;
  JsonNode *machines = NULL;

  /* find json config files */
  glob_str = g_build_filename (get_machines_json_dir (), "*.json", NULL);
  res = glob (glob_str, GLOB_DOOFFS, glob_err_func, &conf_glob);
  if (G_UNLIKELY (res != 0 && res != GLOB_NOMATCH))
    {
      g_critical ("glob %s failed with return code %i", glob_str, res);
      globfree (&conf_glob);
      g_free (glob_str);
      return NULL;
    }

  /* also read /var/lib/cockpit/machines.json for backwards compat; except when
   * running unit tests, then disable this (this is covered by an integration test) */
  conf_glob.gl_pathv[0] = g_getenv ("COCKPIT_TEST_CONFIG_DIR") ? "/dev/null" : "/var/lib/cockpit/machines.json";

  /* start with an empty object */
  machines = new_object_node ();

  for (size_t i = 0; i < conf_glob.gl_pathc + 1; ++i)
    {
      JsonNode *j = parse_json_file (conf_glob.gl_pathv[i]);
      if (j)
        {
          merge_config (json_node_get_object (machines), json_node_get_object (j), conf_glob.gl_pathv[i]);
          json_node_free (j);
        }
    }

  globfree (&conf_glob);
  g_free (glob_str);

  return machines;
}

/* iterator function for update_machines_json() */
static void
update_machine_property (JsonObject *object,
                         const gchar *member_name,
                         JsonNode *member_node,
                         gpointer user_data)
{
  json_object_set_member ((JsonObject *) user_data, member_name, json_node_copy (member_node));
}

gboolean
update_machines_json (const char *filename,
                      const char *hostname,
                      JsonNode *info,
                      GError **error)
{
  gchar *path;
  JsonNode *cur_config;
  JsonObject *cur_config_obj;
  JsonNode *cur_props;
  gboolean res;

  g_assert (JSON_NODE_HOLDS_OBJECT (info));

  path = g_build_filename (get_machines_json_dir (), filename, NULL);
  cur_config = parse_json_file (path);
  if (cur_config == NULL)
    cur_config = new_object_node ();
  cur_config_obj = json_node_get_object (cur_config);
  cur_props = json_object_get_member (cur_config_obj, hostname);

  if (cur_props)
    {
      /* update settings for hostname */
      g_assert (JSON_NODE_HOLDS_OBJECT (cur_props));
      json_object_foreach_member (json_node_get_object (info), update_machine_property, json_node_get_object (cur_props));
    }
  else
    {
      /* create new entry for host name */
      json_object_set_member (cur_config_obj, hostname, json_node_copy (info));
    }

  res = write_json_file (cur_config, path, error);
  g_free (path);
  json_node_free (cur_config);
  return res;
}
