/*
 * 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 "config.h"

#include <glib/gstdio.h>

#include "cockpitdbusinternal.h"
#include "common/cockpitmachinesjson.h"
#include "common/cockpitsystem.h"

#define MACHINES_SIG "a{sa{sv}}"

/* counts the number of file change events that have not yet gotten a PropertiesChanged signal */
static guint pending_updates;

GFileMonitor *machines_monitor;

/* returns a floating GVariant */
static GVariant *
get_machines (void)
{
  JsonNode *machines;
  GError *error = NULL;
  GVariant *variant;

  machines = read_machines_json ();
  variant = json_gvariant_deserialize (machines, MACHINES_SIG, &error);
  /* if the signature does not match, we screwed up in the parser already */
  g_assert (variant != NULL);
  json_node_free (machines);
  return variant;
}

static GVariant *
machines_get_property (GDBusConnection *connection,
                       const gchar *sender,
                       const gchar *object_path,
                       const gchar *interface_name,
                       const gchar *property_name,
                       GError **error,
                       gpointer user_data)
{
  g_return_val_if_fail (property_name != NULL, NULL);

  if (g_str_equal (property_name, "Machines"))
    return get_machines ();
  else
    g_return_val_if_reached (NULL);
}

static void
machines_method_call (GDBusConnection *connection,
                      const gchar *sender,
                      const gchar *object_path,
                      const gchar *interface_name,
                      const gchar *method_name,
                      GVariant *parameters,
                      GDBusMethodInvocation *invocation,
                      gpointer user_data)
{
  if (g_str_equal (method_name, "Update"))
    {
      const gchar *filename, *hostname;
      GVariant * info_v;
      JsonNode *info_json = NULL;
      GError *error = NULL;

      g_variant_get (parameters, "(&s&s@a{sv})", &filename, &hostname, &info_v);
      info_json = json_gvariant_serialize (info_v);
      g_debug ("Updating %s for machine %s", filename, hostname);
      g_variant_unref (info_v);

      if (update_machines_json (filename, hostname, info_json, &error))
        g_dbus_method_invocation_return_value (invocation, NULL);
      else
        g_dbus_method_invocation_take_error (invocation, error);
      json_node_free (info_json);
    }
  else
    g_return_if_reached ();
}

/**
 * notify_properties:
 * @user_data: GDBusConnection to which updates get sent
 *
 * Send a PropertiesChanged signal for invalidating the Machines property. This
 * avoids parsing files and constructing the property when nobody is listening.
 * This gets called in reaction to changed *.json configuration files.
 */
static gboolean
notify_properties (gpointer user_data)
{
  GDBusConnection *connection = user_data;
  GVariant *signal_value;
  GVariantBuilder builder;
  GError *error = NULL;

  /* reset pending counter before we do any actual work, to avoid races */
  pending_updates = 0;
  g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
  g_variant_builder_add (&builder, "s", "Machines");
  signal_value = g_variant_ref_sink (g_variant_new ("(sa{sv}as)", "cockpit.Machines", NULL, &builder));

  g_dbus_connection_emit_signal (connection,
                                 NULL,
                                 "/machines",
                                 "org.freedesktop.DBus.Properties",
                                 "PropertiesChanged",
                                 signal_value,
                                 &error);
  if (error != NULL)
    {
      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED))
        g_critical ("failed to send PropertiesChanged signal: %s", error->message);
      g_error_free (error);
    }
  g_variant_unref (signal_value);
  return G_SOURCE_REMOVE;
}


static void
on_machines_changed (GFileMonitor *monitor,
                     GFile *file,
                     GFile *other_file,
                     GFileMonitorEvent event_type,
                     gpointer user_data)
{
  gchar *path;

  /* ignore uninteresting events; note that DELETED does not get a followup CHANGES_DONE_HINT */
  if (event_type != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT && event_type != G_FILE_MONITOR_EVENT_DELETED)
    return;

  path = g_file_get_path (file);
  if (g_str_has_suffix (path, ".json"))
    {
      g_debug ("on_machines_changed: event type %i on %s", event_type, path);
      /* change events tend to come in batches, so slightly delay the re-reading of
       * files and sending PropertiesChanged; if we already have queued up an
       * update, don't queue it again */
      if (pending_updates++ == 0)
        g_timeout_add (100, notify_properties, user_data);
    }
  else
    {
      g_debug ("on_machines_changed: ignoring event type %i on non-.json file %s", event_type, path);
    }
  g_free (path);
}

static void
migrate_var_config (void)
{
  const gchar *var_path = "/var/lib/cockpit/machines.json";
  GError *error = NULL;

  /* TOCTOU, but if we really miss this, we'll migrate it the next time */
  if (!g_file_test (var_path, G_FILE_TEST_IS_REGULAR))
    {
      g_debug ("%s does not exist, nothing to migrate", var_path);
      return;
    }

  /* the directory should already exist (shipped by the package), but let's make sure */
  if (g_mkdir_with_parents (get_machines_json_dir (), 0755) < 0)
    {
      g_message ("failed to create %s, Cockpit will not work properly: %m", get_machines_json_dir ());
      return;
    }

  /* common case is to move it to 99-webui.json */
  gchar *etc_path = g_build_filename (get_machines_json_dir (), "99-webui.json", NULL);
  GFile *var_file = g_file_new_for_path (var_path);
  GFile *etc_file = g_file_new_for_path (etc_path);
  if (g_file_move (var_file, etc_file, G_FILE_COPY_NONE, NULL, NULL, NULL, &error))
    {
      g_info ("migrated %s to %s", var_path, etc_path);
    }
  else
    {
      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
        {
          GError *error2 = NULL;

          /* most likely an interrupted/failed previous transition attempt;
           * don't clobber the existing file but move it to 98-migrated.json
           * instead */
          g_free (etc_path);
          g_object_unref (etc_file);
          etc_path = g_build_filename (get_machines_json_dir (), "98-migrated.json", NULL);
          etc_file = g_file_new_for_path (etc_path);
          if (g_file_move (var_file, etc_file, G_FILE_COPY_NONE, NULL, NULL, NULL, &error2))
            {
              g_info ("migrated %s to %s (99-webui.json already exists)", var_path, etc_path);
            }
          else
            {
              g_message ("moving of %s to %s failed: %s", var_path, etc_path, error2->message);
              g_error_free (error2);
            }
        }
      else /* different g_file_move() error than EXISTS */
        {
          g_message ("migration of %s to %s failed: %s", var_path, etc_path, error->message);
        }
      g_error_free (error);
    }

  g_object_unref (etc_file);
  g_object_unref (var_file);
  g_free (etc_path);
}


static GDBusInterfaceVTable machines_vtable = {
  .method_call = machines_method_call,
  .get_property = machines_get_property,
};

static GDBusPropertyInfo machines_property = {
  -1, "Machines", MACHINES_SIG, G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
};

static GDBusPropertyInfo *machines_properties[] = {
  &machines_property,
  NULL
};

static GDBusArgInfo machines_update_filename_arg = {
  -1, "filename", "s", NULL
};

static GDBusArgInfo machines_update_hostname_arg = {
  -1, "hostname", "s", NULL
};

static GDBusArgInfo machines_update_info_arg = {
  -1, "info", "a{sv}", NULL
};

static GDBusArgInfo *machines_update_args[] = {
  &machines_update_filename_arg,
  &machines_update_hostname_arg,
  &machines_update_info_arg,
  NULL
};

static GDBusMethodInfo machines_update_method = {
  -1, "Update", machines_update_args, NULL, NULL
};

static GDBusMethodInfo *machines_methods[] = {
  &machines_update_method,
  NULL
};

static GDBusInterfaceInfo machines_interface = {
  -1, "cockpit.Machines", machines_methods, NULL, machines_properties, NULL
};

void
cockpit_dbus_machines_startup (void)
{
  GDBusConnection *connection;
  GFile *machines_monitor_file;
  GError *error = NULL;

  connection = cockpit_dbus_internal_server ();
  g_return_if_fail (connection != NULL);

  g_dbus_connection_register_object (connection, "/machines", &machines_interface,
                                     &machines_vtable, NULL, NULL, &error);

  if (error != NULL)
    {
      g_critical ("couldn't register DBus cockpit.Machines object: %s", error->message);
      g_error_free (error);
      return;
    }

  /* only attempt this in a privileged bridge, otherwise we get confusing failure messages */
  if (g_access ("/etc/cockpit", W_OK) >= 0)
    migrate_var_config ();

  /* watch for file changes and send D-Bus signal for it */
  machines_monitor_file = g_file_new_for_path (get_machines_json_dir ());
  machines_monitor = g_file_monitor (machines_monitor_file, G_FILE_MONITOR_NONE, NULL, &error);
  g_object_unref (machines_monitor_file);
  if (machines_monitor == NULL)
    {
      g_critical ("couldn't set up file watch: %s", error->message);
      g_error_free (error);
      return;
    }
  g_signal_connect (machines_monitor, "changed", G_CALLBACK (on_machines_changed), connection);

  g_object_unref (connection);
}

void
cockpit_dbus_machines_cleanup (void)
{
  g_object_unref (machines_monitor);
}
