/*
 * Copyright (C) 2025 The Phosh.mobi e.V.
 *
 * SPDX-License-Identifier: LGPL-2.1+
 *
 * Author: Evangelos Ribeiro Tzaras <devrtz@fortysixandtwo.eu>
 */

#include "lcb-util.h"
#include "lcb-util-priv.h"
#include "lcb-message-priv.h"

#include "gvdb/gvdb-builder.h"
#include "gvdb/gvdb-reader.h"

#include <gio/gio.h>
#include <glib.h>

#include <gmobile.h>

#define CBD_GVDB_SCHEMA_VERSION 1
/**
 * lcb_util_save_messages_to_file:
 * @messages:(nullable): The list of messages to save
 * @path: The db file path
 * @error: (nullable): An error location
 *
 * Save messages to a gvdb database file.
 *
 * Returns: %TRUE, if successful, %FALSE otherwise with an appropriate `error` set
 */
gboolean
lcb_util_save_messages_to_file (GListModel *messages,
                                const char *path,
                                GError    **error)
{
  g_autoptr (GHashTable) root_table = NULL;
  g_autoptr (GHashTable) messages_table = NULL;
  GvdbItem *item;
  guint n_items;

  g_return_val_if_fail (!messages || G_IS_LIST_MODEL (messages), FALSE);

  root_table = gvdb_hash_table_new (NULL, NULL);
  messages_table = gvdb_hash_table_new (root_table, "messages");

  item = gvdb_hash_table_insert (root_table, "db-version");
  gvdb_item_set_value (item, g_variant_new_uint16 (CBD_GVDB_SCHEMA_VERSION));

  n_items = messages ? g_list_model_get_n_items (messages) : 0;

  for (guint i = 0; i < n_items; i++) {
    const char *severity_subject, *operator_code;
    g_autoptr (LcbMessage) msg = g_list_model_get_item (messages, i);
    g_autoptr (GHashTable) msg_table = NULL;
    g_autofree char *key = g_strdup_printf ("%" G_GINT64_FORMAT "-%u-%u-%u",
                                            lcb_message_get_timestamp (msg),
                                            lcb_message_get_channel (msg),
                                            lcb_message_get_msg_code (msg),
                                            lcb_message_get_update (msg));

    msg_table = gvdb_hash_table_new (messages_table, key);

    gvdb_hash_table_insert_string (msg_table, "text", lcb_message_get_text (msg));

    item = gvdb_hash_table_insert (msg_table, "channel");
    gvdb_item_set_value (item, g_variant_new_uint16 (lcb_message_get_channel (msg)));

    item = gvdb_hash_table_insert (msg_table, "msg-code");
    gvdb_item_set_value (item, g_variant_new_uint16 (lcb_message_get_msg_code (msg)));

    item = gvdb_hash_table_insert (msg_table, "update");
    gvdb_item_set_value (item, g_variant_new_uint16 (lcb_message_get_update (msg)));

    item = gvdb_hash_table_insert (msg_table, "timestamp");
    gvdb_item_set_value (item, g_variant_new_int64 (lcb_message_get_timestamp (msg)));

    item = gvdb_hash_table_insert (msg_table, "severity");
    gvdb_item_set_value (item, g_variant_new_uint32 (lcb_message_get_severity (msg)));

    severity_subject = lcb_message_get_severity_subject (msg);
    if (severity_subject)
      gvdb_hash_table_insert_string (msg_table, "severity-subject", severity_subject);

    operator_code = lcb_message_get_operator_code (msg);
    if (operator_code)
      gvdb_hash_table_insert_string (msg_table, "operator-code", operator_code);
  }

  return gvdb_table_write_contents (root_table, path, FALSE, error);
}

/**
 * lcb_util_load_messages_from_file:
 * @path: The path to a database file
 * @error: An error
 *
 * A [class@Gio.ListModel] containing the messages.
 * Should be freed with [method@g_object_unref]
 *
 * Returns: (transfer full): The list model if the file is not malformed, %NULL otherwise.
 */
GListModel *
lcb_util_load_messages_from_file (const char *path,
                                  GError    **error)
{
  g_autoptr (GFile) file = NULL;
  g_autoptr (GBytes) bytes = NULL;

  g_return_val_if_fail (!gm_str_is_null_or_empty (path), NULL);

  file = g_file_new_for_path (path);
  bytes = g_file_load_bytes (file, NULL, NULL, error);

  return lcb_util_load_messages_from_bytes (bytes, error);
}

/**
 * lcb_util_load_messages_from_bytes:
 * @bytes: The data
 * @error: An error
 *
 * A [class@Gio.ListModel] containing the messages.
 * Should be freed with [method@g_object_unref]
 *
 * Returns: (transfer full): A new (possibly empty)
 *   list model, %NULL only on error
 */
GListModel *
lcb_util_load_messages_from_bytes (GBytes  *bytes,
                                   GError **error)
{
  g_autoptr (GPtrArray) raw_msgs = NULL;
  GListStore *msgs;

  raw_msgs = lcb_util_load_raw_messages_from_bytes (bytes, error);
  if (!raw_msgs)
    return NULL;

  msgs = g_list_store_new (LCB_TYPE_MESSAGE);

  for (guint i = 0; i < raw_msgs->len; i++) {
    GVariant *var_text;
    GVariant *var_channel;
    GVariant *var_msg_code;
    GVariant *var_update;
    GVariant *var_timestamp;
    GVariant *var_severity;
    GVariant *var_severity_subject;
    GVariant *var_operator_code;
    GHashTable *msg_table = raw_msgs->pdata[i];
    g_autoptr (LcbMessage) msg = NULL;
    const char *operator_code = NULL;

    var_text = g_hash_table_lookup (msg_table, "text");
    var_channel = g_hash_table_lookup (msg_table, "channel");
    var_msg_code = g_hash_table_lookup (msg_table, "msg-code");
    var_update = g_hash_table_lookup (msg_table, "update");
    var_timestamp = g_hash_table_lookup (msg_table, "timestamp");
    var_severity = g_hash_table_lookup (msg_table, "severity");
    var_severity_subject = g_hash_table_lookup (msg_table, "severity-subject");
    var_operator_code = g_hash_table_lookup (msg_table, "operator-code");
    if (var_operator_code)
      operator_code = g_variant_get_string (var_operator_code, NULL);

    msg = lcb_message_new (g_variant_get_string (var_text, NULL),
                           g_variant_get_uint16 (var_channel),
                           g_variant_get_uint16 (var_msg_code),
                           g_variant_get_uint16 (var_update),
                           g_variant_get_int64 (var_timestamp),
                           g_variant_get_uint32 (var_severity),
                           var_severity_subject
                           ? g_variant_get_string (var_severity_subject, NULL)
                           : NULL,
                           operator_code);

    g_list_store_append (msgs, msg);
  }

  return G_LIST_MODEL (msgs);
}

/**
 * lcb_util_load_raw_messages_from_file:
 * @path: The path to a database file
 * @error: An error
 *
 * A [class@GPtrArray] containing the raw message contents.
 * This variant exists for consumers that want to avoid depending on glib-object.
 * Should be freed with [method@g_ptr_array_unref]
 *
 * Returns: (transfer full) (element-type GHashTable): The pointer array
 * if the file is not malformed, %NULL otherwise.
 */
GPtrArray *
lcb_util_load_raw_messages_from_file (const char *path,
                                      GError    **error)
{
  g_autoptr (GFile) file = NULL;
  g_autoptr (GBytes) bytes = NULL;

  g_return_val_if_fail (!gm_str_is_null_or_empty (path), NULL);

  file = g_file_new_for_path (path);
  bytes = g_file_load_bytes (file, NULL, NULL, error);

  return lcb_util_load_raw_messages_from_bytes (bytes, error);
}

/**
 * lcb_util_load_raw_messages_from_bytes:
 * @bytes: The data
 * @error: An error location
 *
 * A [class@GPtrArray] containing the raw message contents.
 * This variant exists for consumers that want to avoid depending on glib-object.
 * Should be freed with [method@g_ptr_array_unref]
 *
 * If the data is empty or otherwise malformed, %NULL will be returned
 * and an appropriate @error be set.

 * Returns: (transfer full) (element-type GHashTable):
 *   A (possibly empty) pointer array containing 'a{sv}'
 *   from which [class@LcbMessage]s can be created, see also
 *   [method@lcb_util_load_messages_from_bytes].
 */
GPtrArray *
lcb_util_load_raw_messages_from_bytes (GBytes  *bytes,
                                       GError **error)
{
  GPtrArray *messages;
  g_autoptr (GvdbTable) gvdb_table = NULL;
  g_autoptr (GvdbTable) messages_table = NULL;
  g_autoptr (GVariant) db_version = NULL;
  g_autofree char *buffer = NULL;
  g_auto (GStrv) msg_keys = NULL;
  gsize len_keys;

  if (!bytes || g_bytes_get_size (bytes) == 0) {
    g_debug ("bytes [%p] NULL or empty", bytes);
    g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
                         "bytes NULL or empty");
    return NULL;
  }

  gvdb_table = gvdb_table_new_from_bytes (bytes, FALSE, error);
  if (!gvdb_table || !gvdb_table_has_value (gvdb_table, "db-version")) {
    g_debug ("gvdb_table [%p] is NULL or has no 'db-version' value", gvdb_table);
    g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
                         "Key 'db-version' not found in db");
    return NULL;
  }

  db_version = gvdb_table_get_value (gvdb_table, "db-version");
  g_debug ("Database schema v%" G_GUINT16_FORMAT "; max supported db schema version: %" G_GUINT16_FORMAT,
           g_variant_get_uint16 (db_version), CBD_GVDB_SCHEMA_VERSION);

  if (g_variant_get_uint16 (db_version) > CBD_GVDB_SCHEMA_VERSION) {
    g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
                 "Database version too new: expected at most %" G_GUINT16_FORMAT " but got %" G_GUINT16_FORMAT,
                 CBD_GVDB_SCHEMA_VERSION, g_variant_get_uint16 (db_version));
    return NULL;
  }

  messages = g_ptr_array_new_full (0, (GDestroyNotify) g_hash_table_unref);
  messages_table = gvdb_table_get_table (gvdb_table, "messages");

  if (!messages_table) {
    g_debug ("No saved messages yet, that's fine");
    goto out;
  }

  msg_keys = gvdb_table_get_names (messages_table, &len_keys);
  for (guint i = 0; i < len_keys; i++) {
    GHashTable *msg_dict = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                  g_free, (GDestroyNotify) g_variant_unref);
    gsize n_msg_keys;
    g_autoptr (GvdbTable) msg_table = gvdb_table_get_table (messages_table, msg_keys[i]);
    g_auto (GStrv) keys = gvdb_table_get_names (msg_table, &n_msg_keys);

    g_debug ("Loading message '%s'", msg_keys[i]);
    for (guint j = 0; j < n_msg_keys; j++)
      g_hash_table_insert (msg_dict, g_strdup (keys[j]), gvdb_table_get_value (msg_table, keys[j]));

    g_ptr_array_add (messages, msg_dict);
  }

 out:
  return messages;
}
