/*
 * Copyright © 2015 Red Hat, Inc
 *
 * This program 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 of the License, or (at your option) any later version.
 *
 * This library 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 this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *       Alexander Larsson <alexl@redhat.com>
 */

#include "config.h"

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

#include "libgsystem.h"
#include "libglnx/libglnx.h"

#include "flatpak-builtins.h"
#include "flatpak-utils.h"

static char *opt_ref;
static gboolean opt_oci = FALSE;

static GOptionEntry options[] = {
  { "ref", 0, 0, G_OPTION_ARG_STRING, &opt_ref, "Override the ref used for the imported bundle", "REF" },
  { "oci", 0, 0, G_OPTION_ARG_NONE, &opt_oci, "Import oci image instead of flatpak bundle"},
  { NULL }
};

static gboolean
import_oci (OstreeRepo *repo, GFile *file,
            GCancellable *cancellable, GError **error)
{
#if !defined(HAVE_OSTREE_EXPORT_PATH_PREFIX)
  /* This code actually doesn't user path_prefix, but it need the support
     for reading commits from the transaction that was added at the same time. */
  g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
               "This version of ostree is to old to support OCI exports");
  return FALSE;
#elif !defined(HAVE_LIBARCHIVE)
  g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
               "This version of flatpak is not compiled with libarchive support");
  return FALSE;
#else
  g_autoptr(OstreeMutableTree) archive_mtree = NULL;
  g_autoptr(OstreeMutableTree) mtree = NULL;
  g_autoptr(OstreeMutableTree) files_mtree = NULL;
  g_autoptr(OstreeMutableTree) export_mtree = NULL;
  g_autoptr(GFile) archive_root = NULL;
  g_autoptr(GFile) root = NULL;
  g_autoptr(GFile) files = NULL;
  g_autoptr(GFile) export = NULL;
  g_autoptr(GFile) ref = NULL;
  g_autoptr(GFile) commit = NULL;
  g_autoptr(GFile) commitmeta = NULL;
  g_autoptr(GFile) metadata = NULL;
  g_autofree char *commit_checksum = NULL;
  g_autofree char *ref_data = NULL;
  g_autofree char *commit_data = NULL;
  gsize commit_size;
  g_autofree char *commitmeta_data = NULL;
  g_autofree char *parent = NULL;
  const char *subject;
  const char *body;
  const char *target_ref;
  const char *files_source;
  gsize commitmeta_size;
  g_autoptr(GVariant) commitv = NULL;
  g_autoptr(GVariant) commitv_metadata = NULL;
  g_autoptr(GVariant) commitmetav = NULL;

  if (!ostree_repo_prepare_transaction (repo, NULL, cancellable, error))
    return FALSE;

  /* There is no way to write a subset of the archive to a mtree, so instead
     we write all of it and then build a new mtree with the subset */
  archive_mtree = ostree_mutable_tree_new ();
  if (!ostree_repo_write_archive_to_mtree (repo, file, archive_mtree, NULL,
                                           TRUE,
                                           cancellable, error))
    return FALSE;

  if (!ostree_repo_write_mtree (repo, archive_mtree, &archive_root, cancellable, error))
    return FALSE;

  if (!ostree_repo_file_ensure_resolved ((OstreeRepoFile *) archive_root, error))
    return FALSE;

  ref = g_file_resolve_relative_path (archive_root, "rootfs/ref");
  metadata = g_file_resolve_relative_path (archive_root, "rootfs/metadata");
  commit = g_file_resolve_relative_path (archive_root, "rootfs/commit");
  commitmeta = g_file_resolve_relative_path (archive_root, "rootfs/commitmeta");

  if (!g_file_query_exists (ref, NULL))
    return flatpak_fail (error, "Required file ref not in tarfile");
  if (!g_file_query_exists (metadata, NULL))
    return flatpak_fail (error, "Required file metadata not in tarfile");
  if (!g_file_query_exists (commit, NULL))
    return flatpak_fail (error, "Required file commit not in tarfile");

  if (!g_file_load_contents (ref, cancellable,
                             &ref_data, NULL,
                             NULL, error))
    return FALSE;

  if (g_str_has_prefix (ref_data, "app/"))
    files_source = "rootfs/app";
  else
    files_source = "rootfs/usr";

  files = g_file_resolve_relative_path (archive_root, files_source);
  if (!g_file_query_exists (files, NULL))
    return flatpak_fail (error, "Required directory %s not in tarfile", files_source);

  export = g_file_resolve_relative_path (archive_root, "rootfs/export");

  if (!g_file_load_contents (commit, cancellable,
                             &commit_data, &commit_size,
                             NULL, error))
    return FALSE;

  commitv = g_variant_new_from_data (OSTREE_COMMIT_GVARIANT_FORMAT,
                                     g_steal_pointer (&commit_data), commit_size,
                                     FALSE, g_free, commit_data);
  if (!ostree_validate_structureof_commit (commitv, error))
    return FALSE;

  if (g_file_query_exists (commitmeta, NULL) &&
      !g_file_load_contents (commitmeta, cancellable,
                             &commitmeta_data, &commitmeta_size,
                             NULL, error))
    return FALSE;

  if (commitmeta_data != NULL)
    commitmetav = g_variant_new_from_data (G_VARIANT_TYPE ("a{sv}"),
                                           g_steal_pointer (&commitmeta_data), commitmeta_size,
                                           FALSE, g_free, commitmeta_data);

  mtree = ostree_mutable_tree_new ();

  if (!flatpak_mtree_create_root (repo, mtree, cancellable, error))
    return FALSE;

  if (!ostree_mutable_tree_ensure_dir (mtree, "files", &files_mtree, error))
    return FALSE;

  if (!ostree_repo_write_directory_to_mtree (repo, files, files_mtree, NULL,
                                             cancellable, error))
    return FALSE;

  if (g_file_query_exists (export, NULL))
    {
      if (!ostree_mutable_tree_ensure_dir (mtree, "export", &export_mtree, error))
        return FALSE;

      if (!ostree_repo_write_directory_to_mtree (repo, export, export_mtree, NULL,
                                                 cancellable, error))
        return FALSE;
    }

  if (!ostree_mutable_tree_replace_file (mtree, "metadata",
                                         ostree_repo_file_get_checksum ((OstreeRepoFile *) metadata),
                                         error))
    return FALSE;

  if (!ostree_repo_write_mtree (repo, mtree, &root, cancellable, error))
    return FALSE;

  /* Verify that we created the same contents */
  {
    g_autoptr(GVariant) tree_contents_bytes = NULL;
    g_autofree char *tree_contents_checksum = NULL;
    g_autoptr(GVariant) tree_metadata_bytes = NULL;
    g_autofree char *tree_metadata_checksum = NULL;
    tree_contents_bytes = g_variant_get_child_value (commitv, 6);
    tree_contents_checksum = ostree_checksum_from_bytes_v (tree_contents_bytes);
    tree_metadata_bytes = g_variant_get_child_value (commitv, 7);
    tree_metadata_checksum = ostree_checksum_from_bytes_v (tree_metadata_bytes);

    if (strcmp (tree_contents_checksum, ostree_repo_file_tree_get_contents_checksum ((OstreeRepoFile *) root)))
      return flatpak_fail (error, "Imported content checksum (%s) does not match original checksum (%s)",
                           tree_contents_checksum, ostree_repo_file_tree_get_contents_checksum ((OstreeRepoFile *) root));

    if (strcmp (tree_metadata_checksum, ostree_repo_file_tree_get_metadata_checksum ((OstreeRepoFile *) root)))
      return flatpak_fail (error, "Imported metadata checksum (%s) does not match original checksum (%s)",
                           tree_metadata_checksum, ostree_repo_file_tree_get_metadata_checksum ((OstreeRepoFile *) root));
  }

  commitv_metadata = g_variant_get_child_value (commitv, 0);
  parent = ostree_commit_get_parent (commitv);
  g_variant_get_child (commitv, 3, "s", &subject);
  g_variant_get_child (commitv, 4, "s", &body);

  if (!ostree_repo_write_commit_with_time (repo,
                                           parent,
                                           subject,
                                           body,
                                           commitv_metadata,
                                           OSTREE_REPO_FILE (root),
                                           ostree_commit_get_timestamp (commitv),
                                           &commit_checksum,
                                           cancellable, error))
    return FALSE;

  if (commitmetav != NULL &&
      !ostree_repo_write_commit_detached_metadata (repo, commit_checksum,
                                                   commitmetav, cancellable, error))
    return FALSE;

  if (opt_ref != NULL)
    target_ref = opt_ref;
  else
    target_ref = ref_data;

  ostree_repo_transaction_set_ref (repo, NULL, target_ref, commit_checksum);

  if (!ostree_repo_commit_transaction (repo, NULL, cancellable, error))
    return FALSE;

  g_print ("Importing %s (%s)\n", target_ref, commit_checksum);

  return TRUE;
#endif
}

static gboolean
import_bundle (OstreeRepo *repo, GFile *file,
               GCancellable *cancellable, GError **error)
{
  g_autoptr(GVariant) metadata = NULL;
  g_autofree char *bundle_ref = NULL;
  g_autofree char *to_checksum = NULL;
  const char *ref;

  metadata = flatpak_bundle_load (file, &to_checksum,
                                  &bundle_ref,
                                  NULL,
                                  NULL,
                                  NULL,
                                  error);
  if (metadata == NULL)
    return FALSE;

  if (opt_ref != NULL)
    ref = opt_ref;
  else
    ref = bundle_ref;

  g_print ("Importing %s (%s)\n", ref, to_checksum);
  if (!flatpak_pull_from_bundle (repo, file,
                                 NULL, ref, FALSE,
                                 cancellable,
                                 error))
    return FALSE;

  return TRUE;
}

gboolean
flatpak_builtin_build_import (int argc, char **argv, GCancellable *cancellable, GError **error)
{
  g_autoptr(GOptionContext) context = NULL;
  g_autoptr(GFile) file = NULL;
  g_autoptr(GFile) repofile = NULL;
  g_autoptr(OstreeRepo) repo = NULL;
  const char *location;
  const char *filename;

  context = g_option_context_new ("LOCATION FILENAME - Import a file bundle into a local repository");

  if (!flatpak_option_context_parse (context, options, &argc, &argv, FLATPAK_BUILTIN_FLAG_NO_DIR, NULL, cancellable, error))
    return FALSE;

  if (argc < 3)
    return usage_error (context, "LOCATION and FILENAME must be specified", error);

  location = argv[1];
  filename = argv[2];

  repofile = g_file_new_for_commandline_arg (location);
  repo = ostree_repo_new (repofile);

  if (!g_file_query_exists (repofile, cancellable))
    return flatpak_fail (error, "'%s' is not a valid repository", location);

  file = g_file_new_for_commandline_arg (filename);

  if (!ostree_repo_open (repo, cancellable, error))
    return FALSE;

if (opt_oci)
    {
      if (!import_oci (repo, file, cancellable, error))
        return FALSE;
    }
  else
    {
      if (!import_bundle (repo, file, cancellable, error))
        return FALSE;
    }

  return TRUE;
}
