/*
 * Copyright (C) 2017 Canonical Ltd.
 *
 * This library 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 or version 3 of the License.
 * See http://www.gnu.org/copyleft/lgpl.html the full text of the license.
 */

#include <stdlib.h>
#include <string.h>

#include "snapd-json.h"

#include "snapd-error.h"
#include "snapd-app.h"
#include "snapd-category.h"
#include "snapd-media.h"
#include "snapd-screenshot.h"
#include "snapd-task.h"

void
_snapd_json_set_body (SoupMessage *message, JsonBuilder *builder, GBytes **body)
{
#if SOUP_CHECK_VERSION (2, 99, 2)
    SoupMessageHeaders *request_headers = soup_message_get_request_headers (message);
#else
    SoupMessageHeaders *request_headers = message->request_headers;
#endif
    soup_message_headers_set_content_type (request_headers, "application/json", NULL);

    g_autoptr(JsonNode) json_root = json_builder_get_root (builder);
    g_autoptr(JsonGenerator) json_generator = json_generator_new ();
    json_generator_set_pretty (json_generator, TRUE);
    json_generator_set_root (json_generator, json_root);
    gsize data_length;
    g_autofree guchar *data = (guchar *) json_generator_to_data (json_generator, &data_length);
    *body = g_bytes_new_take (g_steal_pointer (&data), data_length);
}

gboolean
_snapd_json_get_bool (JsonObject *object, const gchar *name, gboolean default_value)
{
    JsonNode *node = json_object_get_member (object, name);
    if (node != NULL && json_node_get_value_type (node) == G_TYPE_BOOLEAN)
        return json_node_get_boolean (node);
    else
        return default_value;
}

gint64
_snapd_json_get_int (JsonObject *object, const gchar *name, gint64 default_value)
{
    JsonNode *node = json_object_get_member (object, name);
    if (node != NULL && json_node_get_value_type (node) == G_TYPE_INT64)
        return json_node_get_int (node);
    else
        return default_value;
}

const gchar *
_snapd_json_get_string (JsonObject *object, const gchar *name, const gchar *default_value)
{
    JsonNode *node = json_object_get_member (object, name);
    if (node != NULL && json_node_get_value_type (node) == G_TYPE_STRING)
        return json_node_get_string (node);
    else
        return default_value;
}

JsonArray *
_snapd_json_get_array (JsonObject *object, const gchar *name)
{
    JsonNode *node = json_object_get_member (object, name);
    if (node != NULL && json_node_get_value_type (node) == JSON_TYPE_ARRAY)
        return json_array_ref (json_node_get_array (node));
    else
        return json_array_new ();
}

JsonObject *
_snapd_json_get_object (JsonObject *object, const gchar *name)
{
    JsonNode *node = json_object_get_member (object, name);
    if (node != NULL && json_node_get_value_type (node) == JSON_TYPE_OBJECT)
        return json_node_get_object (node);
    else
        return NULL;
}

gboolean
_snapd_json_parse_time_span (const gchar *time_span_string, GTimeSpan *time_span)
{
    /* Examples:
          3y
          1y5d
          4h32m2s

       Valid time units: y w d h m s ms us µs ns

       Since GTimeSpan is in microseconds, ns will be discarded.
     */

    GTimeSpan current_value = 0;
    if (time_span_string == NULL)
        return FALSE;
    gchar *p = (gchar *)time_span_string;

    *time_span = 0;
    if (*p == '-')
        p++;

    for (;*p != 0; p++) {
        switch (*p) {
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
            current_value *= 10;
            current_value += (GTimeSpan)((*p) - '0');
            break;
        case 'y':
            *time_span += current_value * 365 * G_TIME_SPAN_DAY;
            current_value = 0;
            break;
        case 'w':
            *time_span += current_value * 7 * G_TIME_SPAN_DAY;
            current_value = 0;
            break;
        case 'd':
            *time_span += current_value * G_TIME_SPAN_DAY;
            current_value = 0;
            break;
        case 'h':
            *time_span += current_value * G_TIME_SPAN_HOUR;
            current_value = 0;
            break;
        case 'm':
            if (*(p+1) == 's') {
                *time_span += current_value * G_TIME_SPAN_MILLISECOND;
                p++;
            } else {
                *time_span += current_value * G_TIME_SPAN_MINUTE;
            }
            current_value = 0;
            break;
        case 's':
            *time_span += current_value * G_TIME_SPAN_SECOND;
            current_value = 0;
            break;
        case '\xc2': // first byte of µ
            p++;
            if (*p != '\xb5') { // it's not µ
                *time_span = 0;
                return FALSE;
            }
            // continue like it is 'u'
        case 'u':
            p++;
            if (*p == 's') {
                *time_span += current_value; // microseconds
                current_value = 0;
            } else {
                *time_span = 0;
                return FALSE;
            }
            break;
        case 'n':
            p++;
            if (*p == 's') {
                current_value = 0;
            } else {
                *time_span = 0;
                return FALSE;
            }
            break;
        default:
            *time_span = 0;
            return FALSE;
        }
    }
    if (*time_span_string == '-') {
        *time_span *= -1;
    }
    return TRUE;
}

static gboolean
parse_date (const gchar *date_string, gint *year, gint *month, gint *day)
{
    /* Example: 2016-05-17 */
    if (strchr (date_string, '-') != NULL) {
        g_auto(GStrv) tokens = g_strsplit (date_string, "-", -1);
        if (g_strv_length (tokens) != 3)
            return FALSE;

        *year = atoi (tokens[0]);
        *month = atoi (tokens[1]);
        *day = atoi (tokens[2]);

        return TRUE;
    }
    /* Example: 20160517 */
    else if (strlen (date_string) == 8) {
        // FIXME: Implement
        return FALSE;
    }
    else
        return FALSE;
}

static gboolean
parse_time (const gchar *time_string, gint *hour, gint *minute, gdouble *seconds)
{
    /* Example: 09:36:53.682 or 09:36:53 or 09:36 */
    if (strchr (time_string, ':') != NULL) {
        g_auto(GStrv) tokens = g_strsplit (time_string, ":", 3);
        *hour = atoi (tokens[0]);
        if (tokens[1] == NULL)
            return FALSE;
        *minute = atoi (tokens[1]);
        if (tokens[2] != NULL) {
            *seconds = g_ascii_strtod (tokens[2], NULL);
        } else {
            *seconds = 0.0;
        }

        return TRUE;
    }
    /* Example: 093653.682 or 093653 or 0936 */
    else {
        // FIXME: Implement
        return FALSE;
    }
}

static gboolean
is_timezone_prefix (gchar c)
{
    return c == '+' || c == '-' || c == 'Z';
}

static int
parse_nanoseconds(gchar *token)
{
    gchar *nanostr;
    int nanosecs;
    size_t nanostr_len;

    // find the decimal dot
    for(nanostr = token; (*nanostr != '.') && (*nanostr != 0); nanostr++);

    // no decimal value
    if (*nanostr == 0)
        return 0;

    nanostr++; // jump over the dot
    nanosecs = atoi(nanostr);

    // RFC3999 removes trailing zeros, so we must complete up to nine decimals
    nanostr_len = strlen(nanostr);
    if (nanostr_len < 9)
        for (int i = 0; i < (9 - nanostr_len); i++)
            nanosecs *= 10;
    return nanosecs;
}

GDateTime *
_snapd_json_get_date_time (JsonObject *object, const gchar *name, int *nanoseconds)
{
    const gchar *value = _snapd_json_get_string (object, name, NULL);
    if (value == NULL)
        return NULL;

    /* Example: 2016-05-17T09:36:53+12:00 */
    g_auto(GStrv) tokens = g_strsplit (value, "T", 2);
    gint year = 0, month = 0, day = 0;
    if (!parse_date (tokens[0], &year, &month, &day))
        return NULL;
    g_autoptr(GTimeZone) timezone = NULL;
    gint hour = 0, minute = 0;
    gdouble seconds = 0.0;
    if (tokens[1] != NULL) {
        gchar *timezone_start;

        /* Timezone is either Z (UTC) +hh:mm or -hh:mm */
        timezone_start = tokens[1];
        while (*timezone_start != '\0' && !is_timezone_prefix (*timezone_start))
            timezone_start++;
        if (*timezone_start != '\0') {
#ifdef GLIB_VERSION_2_68
            timezone = g_time_zone_new_identifier (timezone_start);
            if (timezone == NULL)
                timezone = g_time_zone_new_utc ();
#else
            timezone = g_time_zone_new (timezone_start);
#endif
        }

        /* Strip off timezone */
        *timezone_start = '\0';

        if (!parse_time (tokens[1], &hour, &minute, &seconds))
            return NULL;
    }

    if (timezone == NULL)
        timezone = g_time_zone_new_local ();

    if (nanoseconds != NULL)
        *nanoseconds = parse_nanoseconds(tokens[1]);
    return g_date_time_new (timezone, year, month, day, hour, minute, seconds);
}

static void
parse_error_response (JsonObject *root, JsonNode **error_value, GError **error)
{
    JsonObject *result = _snapd_json_get_object (root, "result");
    gint64 status_code = _snapd_json_get_int (root, "status-code", 0);
    const gchar *kind = result != NULL ? _snapd_json_get_string (result, "kind", NULL) : NULL;
    const gchar *message = result != NULL ? _snapd_json_get_string (result, "message", NULL) : NULL;

    if (error_value != NULL) {
        *error_value = result != NULL ? json_object_get_member (result, "value") : NULL;
        if (*error_value != NULL)
            json_node_ref (*error_value);
    }

    if (g_strcmp0 (kind, "login-required") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_AUTH_DATA_REQUIRED,
                             message);
    else if (g_strcmp0 (kind, "invalid-auth-data") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_AUTH_DATA_INVALID,
                             message);
    else if (g_strcmp0 (kind, "two-factor-required") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_TWO_FACTOR_REQUIRED,
                             message);
    else if (g_strcmp0 (kind, "two-factor-failed") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_TWO_FACTOR_INVALID,
                             message);
    else if (g_strcmp0 (kind, "terms-not-accepted") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_TERMS_NOT_ACCEPTED,
                             message);
    else if (g_strcmp0 (kind, "no-payment-methods") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_PAYMENT_NOT_SETUP,
                             message);
    else if (g_strcmp0 (kind, "payment-declined") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_PAYMENT_DECLINED,
                             message);
    else if (g_strcmp0 (kind, "snap-already-installed") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_ALREADY_INSTALLED,
                             message);
    else if (g_strcmp0 (kind, "snap-not-installed") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_NOT_INSTALLED,
                             message);
    else if (g_strcmp0 (kind, "snap-not-found") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_NOT_FOUND,
                             message);
    else if (g_strcmp0 (kind, "snap-local") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_NOT_IN_STORE,
                             message);
    else if (g_strcmp0 (kind, "snap-no-update-available") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_NO_UPDATE_AVAILABLE,
                             message);
    else if (g_strcmp0 (kind, "password-policy") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_PASSWORD_POLICY_ERROR,
                             message);
    else if (g_strcmp0 (kind, "snap-needs-devmode") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_NEEDS_DEVMODE,
                             message);
    else if (g_strcmp0 (kind, "snap-needs-classic") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_NEEDS_CLASSIC,
                             message);
    else if (g_strcmp0 (kind, "snap-needs-classic-system") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_NEEDS_CLASSIC_SYSTEM,
                             message);
    else if (g_strcmp0 (kind, "bad-query") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_BAD_QUERY,
                             message);
    else if (g_strcmp0 (kind, "network-timeout") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_NETWORK_TIMEOUT,
                             message);
    else if (g_strcmp0 (kind, "auth-cancelled") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_AUTH_CANCELLED,
                             message);
    else if (g_strcmp0 (kind, "snap-not-classic") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_NOT_CLASSIC,
                             message);
    else if (g_strcmp0 (kind, "snap-revision-not-available") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_REVISION_NOT_AVAILABLE,
                             message);
    else if (g_strcmp0 (kind, "snap-channel-not-available") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_CHANNEL_NOT_AVAILABLE,
                             message);
    else if (g_strcmp0 (kind, "snap-not-a-snap") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_NOT_A_SNAP,
                             message);
    else if (g_strcmp0 (kind, "dns-failure") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_DNS_FAILURE,
                             message);
    else if (g_strcmp0 (kind, "option-not-found") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_OPTION_NOT_FOUND,
                             message);
    else if (g_strcmp0 (kind, "unsuccessful") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_UNSUCCESSFUL,
                             message);
    else if (g_strcmp0 (kind, "app-not-found") == 0)
        g_set_error_literal (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_APP_NOT_FOUND,
                             message);
    else if (g_strcmp0 (kind, "snap-architecture-not-available") == 0)
         g_set_error_literal (error,
                              SNAPD_ERROR,
                              SNAPD_ERROR_ARCHITECTURE_NOT_AVAILABLE,
                              message);
    else if (g_strcmp0 (kind, "snap-change-conflict") == 0)
         g_set_error_literal (error,
                              SNAPD_ERROR,
                              SNAPD_ERROR_CHANGE_CONFLICT,
                              message);
    else if (g_strcmp0 (kind, "interfaces-unchanged") == 0)
         g_set_error_literal (error,
                              SNAPD_ERROR,
                              SNAPD_ERROR_INTERFACES_UNCHANGED,
                              message);
    else {
        switch (status_code) {
        case SOUP_STATUS_BAD_REQUEST:
            g_set_error (error,
                         SNAPD_ERROR,
                         SNAPD_ERROR_BAD_REQUEST,
                         "%s: %s", kind, message);
            break;
        case SOUP_STATUS_UNAUTHORIZED:
            g_set_error (error,
                         SNAPD_ERROR,
                         SNAPD_ERROR_AUTH_DATA_REQUIRED,
                         "%s: %s", kind, message);
            break;
        case SOUP_STATUS_FORBIDDEN:
            g_set_error (error,
                         SNAPD_ERROR,
                         SNAPD_ERROR_PERMISSION_DENIED,
                         "%s: %s", kind, message);
            break;
        case SOUP_STATUS_NOT_FOUND:
            g_set_error (error,
                         SNAPD_ERROR,
                         SNAPD_ERROR_NOT_FOUND,
                         "%s: %s", kind, message);
            break;
        /* Other response codes currently produced by snapd:
        case SOUP_STATUS_METHOD_NOT_ALLOWED:
        case SOUP_STATUS_NOT_IMPLEMENTED:
        case SOUP_STATUS_CONFLICT:
         */
        default:
            g_set_error (error,
                         SNAPD_ERROR,
                         SNAPD_ERROR_FAILED,
                         "status-code=%" G_GINT64_FORMAT " kind=%s message=%s", status_code, kind, message);
        }
    }
}

JsonObject *
_snapd_json_parse_response (const gchar *content_type, GBytes *body, SnapdMaintenance **maintenance, JsonNode **error_value, GError **error)
{
    if (content_type == NULL) {
        g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_BAD_RESPONSE, "snapd returned no content type");
        return NULL;
    }
    if (g_strcmp0 (content_type, "application/json") != 0) {
        g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_BAD_RESPONSE, "snapd returned unexpected content type %s", content_type);
        return NULL;
    }

    g_autoptr(JsonParser) parser = json_parser_new ();
    g_autoptr(GError) error_local = NULL;
    if (!json_parser_load_from_data (parser, g_bytes_get_data (body, NULL), g_bytes_get_size (body), &error_local)) {
        g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_BAD_RESPONSE, "Unable to parse snapd response: %s", error_local->message);
        return NULL;
    }

    if (!JSON_NODE_HOLDS_OBJECT (json_parser_get_root (parser))) {
        g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_BAD_RESPONSE, "snapd response does is not a valid JSON object");
        return NULL;
    }
    JsonObject *root = json_node_get_object (json_parser_get_root (parser));

    JsonNode *type_node = json_object_get_member (root, "type");
    if (type_node == NULL || json_node_get_value_type (type_node) != G_TYPE_STRING) {
        g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_BAD_RESPONSE, "snapd response does not have a type");
        return NULL;
    }

    if (json_object_has_member (root, "maintenance") && maintenance != NULL) {
        JsonObject *m = json_object_get_object_member (root, "maintenance");

        const gchar *kind = _snapd_json_get_string (m, "kind", NULL);
        SnapdMaintenanceKind maintenance_kind = SNAPD_MAINTENANCE_KIND_UNKNOWN;
        if (g_strcmp0 (kind, "daemon-restart") == 0)
            maintenance_kind = SNAPD_MAINTENANCE_KIND_DAEMON_RESTART;
        else if (g_strcmp0 (kind, "system-restart") == 0)
            maintenance_kind = SNAPD_MAINTENANCE_KIND_SYSTEM_RESTART;

        *maintenance = g_object_new (SNAPD_TYPE_MAINTENANCE,
                                     "kind", maintenance_kind,
                                     "message", _snapd_json_get_string (m, "message", NULL),
                                     NULL);
    }

    if (strcmp (json_node_get_string (type_node), "error") == 0) {
        parse_error_response (root, error_value, error);
        return NULL;
    }

    return json_object_ref (root);
}

JsonNode *
_snapd_json_get_sync_result (JsonObject *response, GError **error)
{
    const gchar *type = json_object_get_string_member (response, "type");
    if (strcmp (type, "sync") != 0) {
        g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "Unexpected response '%s' returned for sync request", type);
        return NULL;
    }

    if (!json_object_has_member (response, "result")) {
        g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "No result returned");
        return NULL;
    }

    return json_node_ref (json_object_get_member (response, "result"));
}

JsonObject *
_snapd_json_get_sync_result_o (JsonObject *response, GError **error)
{
    /* FIXME: Needs json-glib to be fixed to use json_node_unref */
    /*g_autoptr(JsonNode) result = _snapd_json_get_sync_result (response, error);*/
    JsonNode *result = _snapd_json_get_sync_result (response, error);
    if (result == NULL)
        return NULL;

    if (!JSON_NODE_HOLDS_OBJECT (result)) {
        g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "Result is not an object");
        json_node_unref (result);
        return NULL;
    }

    JsonObject *r = json_object_ref (json_node_get_object (result));
    json_node_unref (result);
    return r;
}

JsonArray *
_snapd_json_get_sync_result_a (JsonObject *response, GError **error)
{
    /* FIXME: Needs json-glib to be fixed to use json_node_unref */
    /*g_autoptr(JsonNode) result = _snapd_json_get_sync_result (response, error);*/
    JsonNode *result = _snapd_json_get_sync_result (response, error);
    if (result == NULL)
        return NULL;

    if (!JSON_NODE_HOLDS_ARRAY (result)) {
        g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "Result is not an array");
        json_node_unref (result);
        return NULL;
    }

    JsonArray *r = json_array_ref (json_node_get_array (result));
    json_node_unref (result);
    return r;
}

gchar *
_snapd_json_get_async_result (JsonObject *response, GError **error)
{
    const gchar *type = json_object_get_string_member (response, "type");
    if (strcmp (type, "async") != 0) {
        g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "Unexpected response '%s' returned for async request", type);
        return NULL;
    }

    JsonNode *change_node = json_object_get_member (response, "change");
    if (change_node == NULL || json_node_get_value_type (change_node) != G_TYPE_STRING) {
        g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "No change returned for async request");
        return NULL;
    }

    return g_strdup (json_node_get_string (change_node));
}

static GStrv
create_str_array_from_jsonarray (JsonArray *data) {
    if (data == NULL)
        return NULL;

    guint len = json_array_get_length (data);
    GStrv list = g_malloc0_n (1 + len, sizeof(gchar *));

    for (guint i = 0; i <len; i++) {
        JsonNode *node = json_array_get_element (data, i);
        if (node == NULL || json_node_get_value_type (node) != G_TYPE_STRING) {
            g_strfreev (list);
            return NULL;
        }
        list[i] = g_strdup(json_node_get_string (node));
    }
    return list;
}

static gchar *
get_int_as_string (JsonObject *object, const gchar *name)
{
    JsonNode *node = json_object_get_member (object, name);
    if (node == NULL)
        return NULL;
    if (json_node_get_value_type (node) == G_TYPE_INT64)
        return g_strdup_printf("%" G_GINT64_FORMAT, json_node_get_int (node));
    else if (json_node_get_value_type (node) == G_TYPE_STRING)
        return g_strdup(json_node_get_string (node));
    return NULL;
}

static SnapdNotice *
parse_notice (JsonObject *object)
{
    int last_occurred_nanoseconds = 0;
    g_autoptr(GDateTime) first_occurred = _snapd_json_get_date_time (object, "first-occurred", NULL);
    g_autoptr(GDateTime) last_occurred = _snapd_json_get_date_time (object, "last-occurred", &last_occurred_nanoseconds);
    g_autoptr(GDateTime) last_repeated = _snapd_json_get_date_time (object, "last-repeated", NULL);

    GTimeSpan expire_after = 0, repeat_after = 0;
    _snapd_json_parse_time_span (_snapd_json_get_string (object, "expire-after", NULL), &expire_after);
    _snapd_json_parse_time_span (_snapd_json_get_string (object, "repeat-after", NULL), &repeat_after);

    const gchar *notice_type_string = _snapd_json_get_string (object, "type", NULL);
    SnapdNoticeType notice_type;
    if (notice_type_string == NULL)
        notice_type = SNAPD_NOTICE_TYPE_UNKNOWN;
    else if (g_str_equal (notice_type_string, "change-update"))
        notice_type = SNAPD_NOTICE_TYPE_CHANGE_UPDATE;
    else if (g_str_equal (notice_type_string, "refresh-inhibit"))
        notice_type = SNAPD_NOTICE_TYPE_REFRESH_INHIBIT;
    else if (g_str_equal (notice_type_string, "snap-run-inhibit"))
        notice_type = SNAPD_NOTICE_TYPE_SNAP_RUN_INHIBIT;
    else
        notice_type = SNAPD_NOTICE_TYPE_UNKNOWN;

    g_autoptr(GHashTable) last_data = NULL;
    if (json_object_has_member (object, "last-data")) {
        last_data = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
        JsonObject *data_dict = _snapd_json_get_object (object, "last-data");

        JsonObjectIter iter;
        json_object_iter_init (&iter, data_dict);
        const gchar *member_name;
        JsonNode *member_node;
        while (json_object_iter_next (&iter, &member_name, &member_node))
            g_hash_table_insert (last_data, g_strdup (member_name), g_strdup (json_node_get_string (member_node)));
    }
    g_autofree gchar *user_id = get_int_as_string (object, "user-id");
    return g_object_new (SNAPD_TYPE_NOTICE,
                         "id", _snapd_json_get_string (object, "id", NULL),
                         "user-id", user_id,
                         "notice-type", notice_type,
                         "key", _snapd_json_get_string (object, "key", NULL),
                         "first-occurred", first_occurred,
                         "last-occurred", last_occurred,
                         "last-occurred-nanoseconds", last_occurred_nanoseconds,
                         "last-repeated", last_repeated,
                         "occurrences", _snapd_json_get_int (object, "occurrences", -1),
                         "expire-after", expire_after,
                         "repeat-after", repeat_after,
                         "last-data", last_data,
                         NULL);
}

GPtrArray *
_snapd_json_parse_notices (JsonNode *node, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_ARRAY) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected change type");
        return NULL;
    }
    JsonArray *array = json_node_get_array (node);
    g_autoptr(GPtrArray) notices = g_ptr_array_new_with_free_func (g_object_unref);
    for (guint i = 0; i < json_array_get_length (array); i++) {
        JsonNode *node = json_array_get_element (array, i);

        if (!JSON_NODE_HOLDS_OBJECT (node)) {
            g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_BAD_RESPONSE, "Invalid notice");
            return NULL;
        }
        JsonObject *object = json_node_get_object (node);

        g_ptr_array_add (notices, parse_notice (object));
    }

    return g_steal_pointer (&notices);
}

SnapdChange *
_snapd_json_parse_change (JsonNode *node, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected change type");
        return NULL;
    }
    JsonObject *object = json_node_get_object (node);

    g_autoptr(JsonArray) array = _snapd_json_get_array (object, "tasks");
    g_autoptr(GPtrArray) tasks = g_ptr_array_new_with_free_func (g_object_unref);
    for (guint i = 0; i < json_array_get_length (array); i++) {
        JsonNode *node = json_array_get_element (array, i);

        if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
            g_set_error (error,
                         SNAPD_ERROR,
                         SNAPD_ERROR_READ_FAILED,
                         "Unexpected task type");
            return NULL;
        }
        JsonObject *object = json_node_get_object (node);
        JsonObject *progress = _snapd_json_get_object (object, "progress");
        g_autoptr(GDateTime) spawn_time = _snapd_json_get_date_time (object, "spawn-time", NULL);
        g_autoptr(GDateTime) ready_time = _snapd_json_get_date_time (object, "ready-time", NULL);
        JsonObject *data = _snapd_json_get_object (object, "data");
        g_autoptr(SnapdTaskData) task_data = NULL;
        if (data != NULL) {
            JsonNode *affected_snaps_node = json_object_get_member (data, "affected-snaps");
            if (affected_snaps_node != NULL) {
                if (json_node_get_value_type (affected_snaps_node) != JSON_TYPE_ARRAY) {
                    g_set_error (error,
                                SNAPD_ERROR,
                                SNAPD_ERROR_READ_FAILED,
                                "Unexpected affected-snaps type");
                    return NULL;
                }
                JsonArray *affected_snaps = json_node_get_array (affected_snaps_node);
                g_auto(GStrv) affected_snaps_array = create_str_array_from_jsonarray (affected_snaps);
                task_data = g_object_new (SNAPD_TYPE_TASK_DATA,
                                          "affected-snaps", affected_snaps_array,
                                          NULL);
            }
        }

        g_autoptr(SnapdTask) t = g_object_new (SNAPD_TYPE_TASK,
                                               "id", _snapd_json_get_string (object, "id", NULL),
                                               "kind", _snapd_json_get_string (object, "kind", NULL),
                                               "summary", _snapd_json_get_string (object, "summary", NULL),
                                               "status", _snapd_json_get_string (object, "status", NULL),
                                               "progress-label", progress != NULL ? _snapd_json_get_string (progress, "label", NULL) : NULL,
                                               "progress-done", progress != NULL ? _snapd_json_get_int (progress, "done", 0) : 0,
                                               "progress-total", progress != NULL ? _snapd_json_get_int (progress, "total", 0) : 0,
                                               "spawn-time", spawn_time,
                                               "ready-time", ready_time,
                                               "data", task_data,
                                               NULL);
        g_ptr_array_add (tasks, g_steal_pointer (&t));
    }

    g_autoptr(GDateTime) main_spawn_time = _snapd_json_get_date_time (object, "spawn-time", NULL);
    g_autoptr(GDateTime) main_ready_time = _snapd_json_get_date_time (object, "ready-time", NULL);

    g_autoptr(SnapdChangeData) data = NULL;
    JsonObject *autorefresh_data = _snapd_json_get_object (object, "data");
    const gchar *kind = _snapd_json_get_string (object, "kind", "");

    // Currently, only one kind of Change can have DATA field, so this is quite "hardcoded". When more
    // Change kinds have DATA field, new subclasses of SNAPD_TYPE_CHANGE_DATA must be created, and
    // the code in QT's snapd-change-data.c file must be completed to manage the new types.
    if (g_str_equal (kind, "auto-refresh") && (autorefresh_data != NULL)) {

        g_autoptr(JsonArray) snap_names_json = _snapd_json_get_array (autorefresh_data, "snap-names");
        g_auto(GStrv) snap_names = create_str_array_from_jsonarray (snap_names_json);

        g_autoptr(JsonArray) refresh_forced_json = _snapd_json_get_array (autorefresh_data, "refresh-forced");
        g_auto(GStrv) refresh_forced = create_str_array_from_jsonarray (refresh_forced_json);

        data = g_object_new (SNAPD_TYPE_AUTOREFRESH_CHANGE_DATA,
                             "snap-names", snap_names,
                             "refresh-forced", refresh_forced,
                             NULL);
    }

    return g_object_new (SNAPD_TYPE_CHANGE,
                         "id", _snapd_json_get_string (object, "id", NULL),
                         "kind", _snapd_json_get_string (object, "kind", NULL),
                         "summary", _snapd_json_get_string (object, "summary", NULL),
                         "status", _snapd_json_get_string (object, "status", NULL),
                         "tasks", tasks,
                         "ready", _snapd_json_get_bool (object, "ready", FALSE),
                         "spawn-time", main_spawn_time,
                         "ready-time", main_ready_time,
                         "error", _snapd_json_get_string (object, "err", NULL),
                         "data", data,
                         NULL);
}

static SnapdConfinement
parse_confinement (const gchar *value)
{
    if (strcmp (value, "strict") == 0)
        return SNAPD_CONFINEMENT_STRICT;
    else if (strcmp (value, "classic") == 0)
        return SNAPD_CONFINEMENT_CLASSIC;
    else if (strcmp (value, "devmode") == 0)
        return SNAPD_CONFINEMENT_DEVMODE;
    else
        return SNAPD_CONFINEMENT_UNKNOWN;
}

SnapdSystemInformation *
_snapd_json_parse_system_information (JsonNode *node, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected system information type");
        return NULL;
    }
    JsonObject *object = json_node_get_object (node);

    const gchar *architecture = _snapd_json_get_string (object, "architecture", "");
    const gchar *confinement_string = _snapd_json_get_string (object, "confinement", "");
    SnapdSystemConfinement confinement = SNAPD_SYSTEM_CONFINEMENT_UNKNOWN;
    if (strcmp (confinement_string, "strict") == 0)
        confinement = SNAPD_SYSTEM_CONFINEMENT_STRICT;
    else if (strcmp (confinement_string, "partial") == 0)
        confinement = SNAPD_SYSTEM_CONFINEMENT_PARTIAL;
    JsonObject *os_release = _snapd_json_get_object (object, "os-release");
    JsonObject *locations = _snapd_json_get_object (object, "locations");
    JsonObject *refresh = _snapd_json_get_object (object, "refresh");
    JsonObject *sandbox_features = _snapd_json_get_object (object, "sandbox-features");
    g_autoptr(GHashTable) sandbox_features_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_strfreev);
    if (sandbox_features != NULL) {
        JsonObjectIter iter;
        json_object_iter_init (&iter, sandbox_features);
        const gchar *name;
        JsonNode *features_node;
        while (json_object_iter_next (&iter, &name, &features_node)) {
            if (json_node_get_value_type (features_node) != JSON_TYPE_ARRAY) {
                g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "Unexpected sandbox features type");
                return FALSE;
            }

            JsonArray *features_array = json_node_get_array (features_node);
            g_autoptr(GPtrArray) features_ptr_array = g_ptr_array_new ();
            for (guint i = 0; i < json_array_get_length (features_array); i++) {
                JsonNode *node = json_array_get_element (features_array, i);

                if (json_node_get_value_type (node) != G_TYPE_STRING) {
                    g_set_error (error,
                                 SNAPD_ERROR,
                                 SNAPD_ERROR_READ_FAILED,
                                 "Unexpected sandbox feature type");
                    return FALSE;
                }

                g_ptr_array_add (features_ptr_array, (gpointer) json_node_get_string (node));
            }
            g_ptr_array_add (features_ptr_array, NULL);
            g_hash_table_insert (sandbox_features_hash, g_strdup (name), g_strdupv ((GStrv) features_ptr_array->pdata));
        }
    }

    g_autoptr(GDateTime) refresh_hold = _snapd_json_get_date_time (refresh, "hold", NULL);
    g_autoptr(GDateTime) refresh_last = _snapd_json_get_date_time (refresh, "last", NULL);
    g_autoptr(GDateTime) refresh_next = _snapd_json_get_date_time (refresh, "next", NULL);

    return g_object_new (SNAPD_TYPE_SYSTEM_INFORMATION,
                         "architecture", architecture,
                         "binaries-directory", locations != NULL ? _snapd_json_get_string (locations, "snap-bin-dir", NULL) : NULL,
                         "build-id", _snapd_json_get_string (object, "build-id", NULL),
                         "confinement", confinement,
                         "kernel-version", _snapd_json_get_string (object, "kernel-version", NULL),
                         "managed", _snapd_json_get_bool (object, "managed", FALSE),
                         "mount-directory", locations != NULL ? _snapd_json_get_string (locations, "snap-mount-dir", NULL) : NULL,
                         "on-classic", _snapd_json_get_bool (object, "on-classic", FALSE),
                         "os-id", os_release != NULL ? _snapd_json_get_string (os_release, "id", NULL) : NULL,
                         "os-version", os_release != NULL ? _snapd_json_get_string (os_release, "version-id", NULL) : NULL,
                         "sandbox-features", sandbox_features_hash,
                         "series", _snapd_json_get_string (object, "series", NULL),
                         "store", _snapd_json_get_string (object, "store", NULL),
                         "version", _snapd_json_get_string (object, "version", NULL),
                         "refresh-hold", refresh_hold,
                         "refresh-last", refresh_last,
                         "refresh-next", refresh_next,
                         "refresh-schedule", _snapd_json_get_string (refresh, "schedule", NULL),
                         "refresh-timer", _snapd_json_get_string (refresh, "timer", NULL),
                         NULL);
}

SnapdSnap *
_snapd_json_parse_snap (JsonNode *node, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected snap type");
        return NULL;
    }
    JsonObject *object = json_node_get_object (node);

    const gchar *name = _snapd_json_get_string (object, "name", NULL);

    SnapdConfinement confinement = parse_confinement (_snapd_json_get_string (object, "confinement", ""));

    const gchar *snap_type_string = _snapd_json_get_string (object, "type", "");
    SnapdSnapType snap_type = SNAPD_SNAP_TYPE_UNKNOWN;
    if (strcmp (snap_type_string, "app") == 0)
        snap_type = SNAPD_SNAP_TYPE_APP;
    else if (strcmp (snap_type_string, "kernel") == 0)
        snap_type = SNAPD_SNAP_TYPE_KERNEL;
    else if (strcmp (snap_type_string, "gadget") == 0)
        snap_type = SNAPD_SNAP_TYPE_GADGET;
    else if (strcmp (snap_type_string, "os") == 0)
        snap_type = SNAPD_SNAP_TYPE_OS;
    else if (strcmp (snap_type_string, "core") == 0)
        snap_type = SNAPD_SNAP_TYPE_CORE;
    else if (strcmp (snap_type_string, "base") == 0)
        snap_type = SNAPD_SNAP_TYPE_BASE;
    else if (strcmp (snap_type_string, "snapd") == 0)
        snap_type = SNAPD_SNAP_TYPE_SNAPD;

    const gchar *snap_status_string = _snapd_json_get_string (object, "status", "");
    SnapdSnapStatus snap_status = SNAPD_SNAP_STATUS_UNKNOWN;
    if (strcmp (snap_status_string, "available") == 0)
        snap_status = SNAPD_SNAP_STATUS_AVAILABLE;
    else if (strcmp (snap_status_string, "priced") == 0)
        snap_status = SNAPD_SNAP_STATUS_PRICED;
    else if (strcmp (snap_status_string, "installed") == 0)
        snap_status = SNAPD_SNAP_STATUS_INSTALLED;
    else if (strcmp (snap_status_string, "active") == 0)
        snap_status = SNAPD_SNAP_STATUS_ACTIVE;

    g_autoptr(JsonArray) apps = _snapd_json_get_array (object, "apps");
    g_autoptr(GPtrArray) apps_array = g_ptr_array_new_with_free_func (g_object_unref);
    for (guint i = 0; i < json_array_get_length (apps); i++) {
        JsonNode *node = json_array_get_element (apps, i);

        SnapdApp *app = _snapd_json_parse_app (node, name, error);
        if (app == NULL)
            return NULL;

        g_ptr_array_add (apps_array, app);
    }

    g_autoptr(JsonArray) categories = _snapd_json_get_array (object, "categories");
    g_autoptr(GPtrArray) categories_array = g_ptr_array_new_with_free_func (g_object_unref);
    for (guint i = 0; i < json_array_get_length (categories); i++) {
        JsonNode *node = json_array_get_element (categories, i);

        if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
            g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "Unexpected categories type");
            return NULL;
        }

        JsonObject *c = json_node_get_object (node);
        g_autoptr(SnapdCategory) category = g_object_new (SNAPD_TYPE_CATEGORY,
                                                          "featured", _snapd_json_get_bool (c, "featured", FALSE),
                                                          "name", _snapd_json_get_string (c, "name", NULL),
                                                          NULL);
        g_ptr_array_add (categories_array, g_steal_pointer (&category));
    }

    JsonObject *channels = _snapd_json_get_object (object, "channels");
    g_autoptr(GPtrArray) channels_array = g_ptr_array_new_with_free_func (g_object_unref);
    if (channels != NULL) {
        JsonObjectIter iter;
        json_object_iter_init (&iter, channels);
        const gchar *name;
        JsonNode *channel_node;
        while (json_object_iter_next (&iter, &name, &channel_node)) {
            if (json_node_get_value_type (channel_node) != JSON_TYPE_OBJECT) {
                g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "Unexpected channel type");
                return NULL;
            }
            JsonObject *c = json_node_get_object (channel_node);

            SnapdConfinement confinement = parse_confinement (_snapd_json_get_string (c, "confinement", ""));
            g_autoptr(GDateTime) released_at = _snapd_json_get_date_time (c, "released-at", NULL);

            g_autoptr(SnapdChannel) channel = g_object_new (SNAPD_TYPE_CHANNEL,
                                                            "confinement", confinement,
                                                            "epoch", _snapd_json_get_string (c, "epoch", NULL),
                                                            "name", _snapd_json_get_string (c, "channel", NULL),
                                                            "released-at", released_at,
                                                            "revision", _snapd_json_get_string (c, "revision", NULL),
                                                            "size", _snapd_json_get_int (c, "size", 0),
                                                            "version", _snapd_json_get_string (c, "version", NULL),
                                                            NULL);
            g_ptr_array_add (channels_array, g_steal_pointer (&channel));
        }
    }

    g_autoptr(JsonArray) common_ids = _snapd_json_get_array (object, "common-ids");
    g_autoptr(GPtrArray) common_ids_array = g_ptr_array_new ();
    for (guint i = 0; i < json_array_get_length (common_ids); i++) {
        JsonNode *node = json_array_get_element (common_ids, i);

        if (json_node_get_value_type (node) != G_TYPE_STRING) {
            g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "Unexpected common ID type");
            return NULL;
        }

        g_ptr_array_add (common_ids_array, (gpointer) json_node_get_string (node));
    }
    g_ptr_array_add (common_ids_array, NULL);

    g_autoptr(GDateTime) install_date = _snapd_json_get_date_time (object, "install-date", NULL);
    g_autoptr(GDateTime) hold = _snapd_json_get_date_time (object, "hold", NULL);

    JsonObject *prices = _snapd_json_get_object (object, "prices");
    g_autoptr(GPtrArray) prices_array = g_ptr_array_new_with_free_func (g_object_unref);
    if (prices != NULL) {
        JsonObjectIter iter;
        json_object_iter_init (&iter, prices);
        const gchar *currency;
        JsonNode *amount_node;
        while (json_object_iter_next (&iter, &currency, &amount_node)) {
            if (json_node_get_value_type (amount_node) != G_TYPE_DOUBLE) {
                g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "Unexpected price type");
                return NULL;
            }

            g_autoptr(SnapdPrice) price = g_object_new (SNAPD_TYPE_PRICE,
                                                        "amount", json_node_get_double (amount_node),
                                                        "currency", currency,
                                                        NULL);
            g_ptr_array_add (prices_array, g_steal_pointer (&price));
        }
    }

    g_autoptr(JsonArray) media = _snapd_json_get_array (object, "media");
    g_autoptr(GPtrArray) media_array = g_ptr_array_new_with_free_func (g_object_unref);
    for (guint i = 0; i < json_array_get_length (media); i++) {
        JsonNode *node = json_array_get_element (media, i);

        if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
            g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "Unexpected media type");
            return NULL;
        }

        JsonObject *s = json_node_get_object (node);
        g_autoptr(SnapdMedia) media = g_object_new (SNAPD_TYPE_MEDIA,
                                                    "type", _snapd_json_get_string (s, "type", NULL),
                                                    "url", _snapd_json_get_string (s, "url", NULL),
                                                    "width", (guint) _snapd_json_get_int (s, "width", 0),
                                                    "height", (guint) _snapd_json_get_int (s, "height", 0),
                                                    NULL);
        g_ptr_array_add (media_array, g_steal_pointer (&media));
    }

    g_autoptr(GPtrArray) screenshots_array = g_ptr_array_new_with_free_func (g_object_unref);

    /* The tracks field was originally incorrectly named, fixed in snapd 61ad9ed (2.29.5) */
    g_autoptr(JsonArray) tracks = NULL;
    if (json_object_has_member (object, "Tracks"))
        tracks = _snapd_json_get_array (object, "Tracks");
    else
        tracks = _snapd_json_get_array (object, "tracks");
    g_autoptr(GPtrArray) track_array = g_ptr_array_new ();
    for (guint i = 0; i < json_array_get_length (tracks); i++) {
        JsonNode *node = json_array_get_element (tracks, i);

        if (json_node_get_value_type (node) != G_TYPE_STRING) {
            g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "Unexpected track type");
            return NULL;
        }

        g_ptr_array_add (track_array, (gpointer) json_node_get_string (node));
    }
    g_ptr_array_add (track_array, NULL);

    /* The developer field originally contained the publisher username */
    const gchar *publisher_username = _snapd_json_get_string (object, "developer", NULL);
    JsonObject *publisher = _snapd_json_get_object (object, "publisher");
    const gchar *publisher_display_name = NULL;
    const gchar *publisher_id = NULL;
    SnapdPublisherValidation publisher_validation = SNAPD_PUBLISHER_VALIDATION_UNKNOWN;
    if (publisher != NULL) {
        publisher_display_name = _snapd_json_get_string (publisher, "display-name", NULL);
        publisher_id = _snapd_json_get_string (publisher, "id", NULL);
        publisher_username = _snapd_json_get_string (publisher, "username", publisher_username);
        const gchar *validation = _snapd_json_get_string (publisher, "validation", NULL);
        if (g_strcmp0 (validation, "unproven") == 0)
            publisher_validation = SNAPD_PUBLISHER_VALIDATION_UNPROVEN;
        else if (g_strcmp0 (validation, "starred") == 0)
            publisher_validation = SNAPD_PUBLISHER_VALIDATION_STARRED;
        else if (g_strcmp0 (validation, "verified") == 0)
            publisher_validation = SNAPD_PUBLISHER_VALIDATION_VERIFIED;
        else
            publisher_validation = SNAPD_PUBLISHER_VALIDATION_UNKNOWN;
    }

    JsonObject *refresh_inhibit = _snapd_json_get_object (object, "refresh-inhibit");
    g_autoptr(GDateTime) proceed_time = NULL;
    if (refresh_inhibit != NULL)
        proceed_time = _snapd_json_get_date_time (refresh_inhibit, "proceed-time", NULL);

    return g_object_new (SNAPD_TYPE_SNAP,
                         "apps", apps_array,
                         "base", _snapd_json_get_string (object, "base", NULL),
                         "broken", _snapd_json_get_string (object, "broken", NULL),
                         "categories", categories_array,
                         "channel", _snapd_json_get_string (object, "channel", NULL),
                         "channels", channels_array,
                         "common-ids", (GStrv) common_ids_array->pdata,
                         "confinement", confinement,
                         "contact", _snapd_json_get_string (object, "contact", NULL),
                         "description", _snapd_json_get_string (object, "description", NULL),
                         "devmode", _snapd_json_get_bool (object, "devmode", FALSE),
                         "download-size", _snapd_json_get_int (object, "download-size", 0),
                         "hold", hold,
                         "icon", _snapd_json_get_string (object, "icon", NULL),
                         "id", _snapd_json_get_string (object, "id", NULL),
                         "install-date", install_date,
                         "installed-size", _snapd_json_get_int (object, "installed-size", 0),
                         "jailmode", _snapd_json_get_bool (object, "jailmode", FALSE),
                         "license", _snapd_json_get_string (object, "license", NULL),
                         "media", media_array,
                         "mounted-from", _snapd_json_get_string (object, "mounted-from", NULL),
                         "name", name,
                         "prices", prices_array,
                         "private", _snapd_json_get_bool (object, "private", FALSE),
                         "publisher-id", publisher_id,
                         "publisher-username", publisher_username,
                         "publisher-display-name", publisher_display_name,
                         "publisher-validation", publisher_validation,
                         "revision", _snapd_json_get_string (object, "revision", NULL),
                         "screenshots", screenshots_array,
                         "snap-type", snap_type,
                         "status", snap_status,
                         "store-url", _snapd_json_get_string (object, "store-url", NULL),
                         "summary", _snapd_json_get_string (object, "summary", NULL),
                         "title", _snapd_json_get_string (object, "title", NULL),
                         "tracking-channel", _snapd_json_get_string (object, "tracking-channel", NULL),
                         "tracks", (GStrv) track_array->pdata,
                         "trymode", _snapd_json_get_bool (object, "trymode", FALSE),
                         "version", _snapd_json_get_string (object, "version", NULL),
                         "website", _snapd_json_get_string (object, "website", NULL),
                         "proceed-time", proceed_time,
                         NULL);
}

SnapdApp *
_snapd_json_parse_app (JsonNode *node, const gchar *snap_name, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected app type");
        return NULL;
    }
    JsonObject *object = json_node_get_object (node);

    const gchar *daemon = _snapd_json_get_string (object, "daemon", NULL);
    SnapdDaemonType daemon_type = SNAPD_DAEMON_TYPE_NONE;
    if (daemon == NULL)
        daemon_type = SNAPD_DAEMON_TYPE_NONE;
    else if (strcmp (daemon, "simple") == 0)
        daemon_type = SNAPD_DAEMON_TYPE_SIMPLE;
    else if (strcmp (daemon, "forking") == 0)
        daemon_type = SNAPD_DAEMON_TYPE_FORKING;
    else if (strcmp (daemon, "oneshot") == 0)
        daemon_type = SNAPD_DAEMON_TYPE_ONESHOT;
    else if (strcmp (daemon, "dbus") == 0)
        daemon_type = SNAPD_DAEMON_TYPE_DBUS;
    else if (strcmp (daemon, "notify") == 0)
        daemon_type = SNAPD_DAEMON_TYPE_NOTIFY;
    else
        daemon_type = SNAPD_DAEMON_TYPE_UNKNOWN;

    const gchar *app_snap_name = _snapd_json_get_string (object, "snap", NULL);
    return g_object_new (SNAPD_TYPE_APP,
                         "name", _snapd_json_get_string (object, "name", NULL),
                         "active", _snapd_json_get_bool (object, "active", FALSE),
                         "common-id", _snapd_json_get_string (object, "common-id", NULL),
                         "daemon-type", daemon_type,
                         "desktop-file", _snapd_json_get_string (object, "desktop-file", NULL),
                         "enabled", _snapd_json_get_bool (object, "enabled", FALSE),
                         "snap", snap_name ? snap_name : app_snap_name,
                         NULL);
}

SnapdAlias *
_snapd_json_parse_alias (JsonNode *node, const gchar *snap_name, const gchar *name, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected alias type");
        return FALSE;
    }
    JsonObject *object = json_node_get_object (node);

    const gchar *status_string = _snapd_json_get_string (object, "status", NULL);
    SnapdAliasStatus status = SNAPD_ALIAS_STATUS_UNKNOWN;
    if (strcmp (status_string, "disabled") == 0)
        status = SNAPD_ALIAS_STATUS_DISABLED;
    else if (strcmp (status_string, "auto") == 0)
        status = SNAPD_ALIAS_STATUS_AUTO;
    else if (strcmp (status_string, "manual") == 0)
        status = SNAPD_ALIAS_STATUS_MANUAL;
    else
        status = SNAPD_ALIAS_STATUS_UNKNOWN;

    return g_object_new (SNAPD_TYPE_ALIAS,
                         "snap", snap_name,
                         "app-auto", _snapd_json_get_string (object, "auto", NULL),
                         "app-manual", _snapd_json_get_string (object, "manual", NULL),
                         "command", _snapd_json_get_string (object, "command", NULL),
                         "name", name,
                         "status", status,
                         NULL);
}

SnapdUserInformation *
_snapd_json_parse_user_information (JsonNode *node, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected user information type");
        return NULL;
    }
    JsonObject *object = json_node_get_object (node);

    g_autoptr(JsonArray) ssh_keys = _snapd_json_get_array (object, "ssh-keys");
    g_autoptr(GPtrArray) ssh_key_array = g_ptr_array_new ();
    for (guint i = 0; i < json_array_get_length (ssh_keys); i++) {
        JsonNode *node = json_array_get_element (ssh_keys, i);

        if (json_node_get_value_type (node) != G_TYPE_STRING) {
            g_set_error (error, SNAPD_ERROR, SNAPD_ERROR_READ_FAILED, "Unexpected SSH key type");
            return NULL;
        }

        g_ptr_array_add (ssh_key_array, (gpointer) json_node_get_string (node));
    }

    g_ptr_array_add (ssh_key_array, NULL);

    g_autoptr(SnapdAuthData) auth_data = NULL;
    if (json_object_has_member (object, "macaroon")) {
        g_autoptr(JsonArray) discharges = _snapd_json_get_array (object, "discharges");
        g_autoptr(GPtrArray) discharge_array = g_ptr_array_new ();
        for (guint i = 0; i < json_array_get_length (discharges); i++) {
            JsonNode *node = json_array_get_element (discharges, i);

            if (json_node_get_value_type (node) != G_TYPE_STRING) {
                g_set_error (error,
                             SNAPD_ERROR,
                             SNAPD_ERROR_READ_FAILED,
                             "Unexpected discharge type");
                return FALSE;
            }

            g_ptr_array_add (discharge_array, (gpointer) json_node_get_string (node));
        }
        g_ptr_array_add (discharge_array, NULL);
        auth_data = snapd_auth_data_new (_snapd_json_get_string (object, "macaroon", NULL), (GStrv) discharge_array->pdata);
    }

    return g_object_new (SNAPD_TYPE_USER_INFORMATION,
                         "id", _snapd_json_get_int (object, "id", -1),
                         "username", _snapd_json_get_string (object, "username", NULL),
                         "email", _snapd_json_get_string (object, "email", NULL),
                         "ssh-keys", (GStrv) ssh_key_array->pdata,
                         "auth-data", auth_data,
                         NULL);
}

GHashTable *
_snapd_json_parse_object (JsonObject *object, GError **error)
{
    g_autoptr(GHashTable) attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);

    JsonObjectIter iter;
    json_object_iter_init (&iter, object);
    const gchar *attribute_name;
    JsonNode *node;
    while (json_object_iter_next (&iter, &attribute_name, &node)) {
        GVariant *variant = json_gvariant_deserialize (node, NULL, error);
        if (variant == NULL)
            return NULL;
        g_hash_table_insert (attributes, g_strdup (attribute_name), g_variant_ref_sink (variant));
    }

    return g_steal_pointer (&attributes);
}

GHashTable *
_snapd_json_parse_attributes (JsonNode *node, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected attributes type");
        return NULL;
    }

    return _snapd_json_parse_object (json_node_get_object (node), error);
}

SnapdSlot *
_snapd_json_parse_slot (JsonNode *node, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected slot type");
        return NULL;
    }
    JsonObject *object = json_node_get_object (node);

    g_autoptr(JsonArray) connections = _snapd_json_get_array (object, "connections");
    g_autoptr(GPtrArray) plug_refs = g_ptr_array_new_with_free_func (g_object_unref);
    for (guint i = 0; i < json_array_get_length (connections); i++) {
        JsonNode *node = json_array_get_element (connections, i);

        SnapdPlugRef *plug_ref = _snapd_json_parse_plug_ref (node, error);
        if (plug_ref == NULL)
            return NULL;
        g_ptr_array_add (plug_refs, plug_ref);
    }

    g_autoptr(GHashTable) attributes = NULL;
    if (json_object_has_member (object, "attrs")) {
        attributes = _snapd_json_parse_attributes (json_object_get_member (object, "attrs"), error);
        if (attributes == NULL)
            return FALSE;
    }
    else
        attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);

    return g_object_new (SNAPD_TYPE_SLOT,
                         "name", _snapd_json_get_string (object, "slot", NULL),
                         "snap", _snapd_json_get_string (object, "snap", NULL),
                         "interface", _snapd_json_get_string (object, "interface", NULL),
                         "label", _snapd_json_get_string (object, "label", NULL),
                         "connections", plug_refs,
                         "attributes", attributes,
                         // FIXME: apps
                         NULL);
}

SnapdPlug *
_snapd_json_parse_plug (JsonNode *node, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected plug type");
        return NULL;
    }
    JsonObject *object = json_node_get_object (node);

    g_autoptr(JsonArray) connections = _snapd_json_get_array (object, "connections");
    g_autoptr(GPtrArray) slot_refs = g_ptr_array_new_with_free_func (g_object_unref);
    for (guint i = 0; i < json_array_get_length (connections); i++) {
        JsonNode *node = json_array_get_element (connections, i);

        SnapdSlotRef *slot_ref = _snapd_json_parse_slot_ref (node, error);
        if (slot_ref == NULL)
            return NULL;
        g_ptr_array_add (slot_refs, slot_ref);
    }

    g_autoptr(GHashTable) attributes = NULL;
    if (json_object_has_member (object, "attrs")) {
        attributes = _snapd_json_parse_attributes (json_object_get_member (object, "attrs"), error);
        if (attributes == NULL)
            return FALSE;
    }
    else
        attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);

    return g_object_new (SNAPD_TYPE_PLUG,
                         "name", _snapd_json_get_string (object, "plug", NULL),
                         "snap", _snapd_json_get_string (object, "snap", NULL),
                         "interface", _snapd_json_get_string (object, "interface", NULL),
                         "label", _snapd_json_get_string (object, "label", NULL),
                         "connections", slot_refs,
                         "attributes", attributes,
                         // FIXME: apps
                         NULL);
}

SnapdSlotRef *
_snapd_json_parse_slot_ref (JsonNode *node, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected slot ref type");
        return NULL;
    }
    JsonObject *object = json_node_get_object (node);

    return g_object_new (SNAPD_TYPE_SLOT_REF,
                         "slot", _snapd_json_get_string (object, "slot", NULL),
                         "snap", _snapd_json_get_string (object, "snap", NULL),
                         NULL);
}

SnapdPlugRef *
_snapd_json_parse_plug_ref (JsonNode *node, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected plug ref type");
        return NULL;
    }
    JsonObject *object = json_node_get_object (node);

    return g_object_new (SNAPD_TYPE_PLUG_REF,
                         "plug", _snapd_json_get_string (object, "plug", NULL),
                         "snap", _snapd_json_get_string (object, "snap", NULL),
                         NULL);
}

SnapdConnection *
_snapd_json_parse_connection (JsonNode *node, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected connection type");
        return NULL;
    }
    JsonObject *object = json_node_get_object (node);

    g_autoptr(SnapdSlotRef) slot_ref = NULL;
    if (json_object_has_member (object, "slot")) {
        slot_ref = _snapd_json_parse_slot_ref (json_object_get_member (object, "slot"), error);
        if (slot_ref == NULL)
            return NULL;
    }
    g_autoptr(SnapdPlugRef) plug_ref = NULL;
    if (json_object_has_member (object, "plug")) {
        plug_ref = _snapd_json_parse_plug_ref (json_object_get_member (object, "plug"), error);
        if (plug_ref == NULL)
            return NULL;
    }

    g_autoptr(GHashTable) slot_attributes = NULL;
    if (json_object_has_member (object, "slot-attrs")) {
        slot_attributes = _snapd_json_parse_attributes (json_object_get_member (object, "slot-attrs"), error);
        if (slot_attributes == NULL)
            return FALSE;
    }
    else
        slot_attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);

    g_autoptr(GHashTable) plug_attributes = NULL;
    if (json_object_has_member (object, "plug-attrs")) {
        plug_attributes = _snapd_json_parse_attributes (json_object_get_member (object, "plug-attrs"), error);
        if (plug_attributes == NULL)
            return FALSE;
    }
    else
        plug_attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);

    return g_object_new (SNAPD_TYPE_CONNECTION,
                         "slot", slot_ref,
                         "plug", plug_ref,
                         "interface", _snapd_json_get_string (object, "interface", NULL),
                         "manual", _snapd_json_get_bool (object, "manual", FALSE),
                         "gadget", _snapd_json_get_bool (object, "gadget", FALSE),
                         "slot-attrs", slot_attributes,
                         "plug-attrs", plug_attributes,
                         NULL);
}

SnapdInterface *
_snapd_json_parse_interface (JsonNode *node, GError **error)
{
    if (json_node_get_value_type (node) != JSON_TYPE_OBJECT) {
        g_set_error (error,
                     SNAPD_ERROR,
                     SNAPD_ERROR_READ_FAILED,
                     "Unexpected interface type");
        return NULL;
    }
    JsonObject *object = json_node_get_object (node);

    g_autoptr(JsonArray) plugs = _snapd_json_get_array (object, "plugs");
    g_autoptr(GPtrArray) plug_array = g_ptr_array_new_with_free_func (g_object_unref);
    for (guint i = 0; i < json_array_get_length (plugs); i++) {
        JsonNode *node = json_array_get_element (plugs, i);

        SnapdPlug *plug = _snapd_json_parse_plug (node, error);
        if (plug == NULL)
            return FALSE;

        g_ptr_array_add (plug_array, plug);
    }

    g_autoptr(JsonArray) slots = _snapd_json_get_array (object, "slots");
    g_autoptr(GPtrArray) slot_array = g_ptr_array_new_with_free_func (g_object_unref);
    for (guint i = 0; i < json_array_get_length (slots); i++) {
        JsonNode *node = json_array_get_element (slots, i);

        SnapdSlot *slot = _snapd_json_parse_slot (node, error);
        if (slot == NULL)
            return FALSE;

        g_ptr_array_add (slot_array, slot);
    }

    return g_object_new (SNAPD_TYPE_INTERFACE,
                         "name", _snapd_json_get_string (object, "name", NULL),
                         "summary", _snapd_json_get_string (object, "summary", NULL),
                         "doc-url", _snapd_json_get_string (object, "doc-url", NULL),
                         "plugs", plug_array,
                         "slots", slot_array,
                         NULL);
}
