/*
 * Copyright (C) 2011,2013 Colin Walters <walters@verbum.org>
 *
 * SPDX-License-Identifier: LGPL-2.0+
 *
 * 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 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 <https://www.gnu.org/licenses/>.
 *
 * Author: Colin Walters <walters@verbum.org>
 */

#include "config.h"

#include "ostree.h"
#include "ot-builtins.h"
#include "otutil.h"

static gboolean opt_disable_fsync;
static gboolean opt_per_object_fsync;
static gboolean opt_mirror;
static gboolean opt_commit_only;
static gboolean opt_dry_run;
static gboolean opt_disable_static_deltas;
static gboolean opt_require_static_deltas;
static gboolean opt_untrusted;
static gboolean opt_http_trusted;
static gboolean opt_timestamp_check;
static gboolean opt_disable_verify_bindings;
static char *opt_timestamp_check_from_rev;
static gboolean opt_bareuseronly_files;
static gboolean opt_retry_all;
static char **opt_subpaths;
static char **opt_http_headers;
static char *opt_cache_dir;
static char *opt_append_user_agent;
static int opt_depth = 0;
static int opt_frequency = 0;
static int opt_network_retries = -1;
static int opt_low_speed_limit_bytes = -1;
static int opt_low_speed_time_seconds = -1;
static int opt_max_outstanding_fetcher_requests = -1;
static char *opt_url;
static char **opt_localcache_repos;

/* ATTENTION:
 * Please remember to update the bash-completion script (bash/ostree) and
 * man page (man/ostree-pull.xml) when changing the option list.
 */

static GOptionEntry options[]
    = { { "commit-metadata-only", 0, 0, G_OPTION_ARG_NONE, &opt_commit_only,
          "Fetch only the commit metadata", NULL },
        { "cache-dir", 0, 0, G_OPTION_ARG_FILENAME, &opt_cache_dir, "Use custom cache dir", NULL },
        { "disable-fsync", 0, 0, G_OPTION_ARG_NONE, &opt_disable_fsync, "Do not invoke fsync()",
          NULL },
        { "per-object-fsync", 0, 0, G_OPTION_ARG_NONE, &opt_per_object_fsync,
          "Perform writes in such a way that avoids stalling concurrent processes", NULL },
        { "disable-static-deltas", 0, 0, G_OPTION_ARG_NONE, &opt_disable_static_deltas,
          "Do not use static deltas", NULL },
        { "require-static-deltas", 0, 0, G_OPTION_ARG_NONE, &opt_require_static_deltas,
          "Require static deltas", NULL },
        { "disable-retry-on-network-errors", 0, 0, G_OPTION_ARG_NONE, &opt_retry_all,
          "Do not retry when network issues happen, instead fail automatically. (Currently only "
          "affects libcurl)",
          NULL },
        { "mirror", 0, 0, G_OPTION_ARG_NONE, &opt_mirror,
          "Write refs suitable for a mirror and fetches all refs if none provided", NULL },
        { "subpath", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &opt_subpaths,
          "Only pull the provided subpath(s)", NULL },
        { "untrusted", 0, 0, G_OPTION_ARG_NONE, &opt_untrusted,
          "Verify checksums of local sources (always enabled for HTTP pulls)", NULL },
        { "http-trusted", 0, 0, G_OPTION_ARG_NONE, &opt_http_trusted,
          "Do not verify checksums of HTTP sources (mostly useful when mirroring)", NULL },
        { "bareuseronly-files", 0, 0, G_OPTION_ARG_NONE, &opt_bareuseronly_files,
          "Reject regular files with mode outside of 0775 (world writable, suid, etc.)", NULL },
        { "dry-run", 0, 0, G_OPTION_ARG_NONE, &opt_dry_run,
          "Only print information on what will be downloaded (requires static deltas)", NULL },
        { "depth", 0, 0, G_OPTION_ARG_INT, &opt_depth,
          "Traverse DEPTH parents (-1=infinite) (default: 0)", "DEPTH" },
        { "url", 0, 0, G_OPTION_ARG_STRING, &opt_url,
          "Pull objects from this URL instead of the one from the remote config", "URL" },
        { "http-header", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_http_headers,
          "Add NAME=VALUE as HTTP header to all requests", "NAME=VALUE" },
        { "update-frequency", 0, 0, G_OPTION_ARG_INT, &opt_frequency,
          "Sets the update frequency, in milliseconds (0=1000ms) (default: 0)", "FREQUENCY" },
        { "network-retries", 0, 0, G_OPTION_ARG_INT, &opt_network_retries,
          "Specifies how many times each download should be retried upon error (default: 5)", "N" },
        { "low-speed-limit-bytes", 0, 0, G_OPTION_ARG_INT, &opt_low_speed_limit_bytes,
          "The average transfer speed per second of a transfer during the time set via "
          "'low-speed-time-seconds' for libcurl to abort(default: 1000)",
          "N" },
        { "low-speed-time-seconds", 0, 0, G_OPTION_ARG_INT, &opt_low_speed_time_seconds,
          "The time in number seconds that the transfer speed should be below the "
          "'low-speed-limit-bytes' setting for libcurl to abort (default: 30)",
          "N" },
        { "max-outstanding-fetcher-requests", 0, 0, G_OPTION_ARG_INT,
          &opt_max_outstanding_fetcher_requests,
          "The max amount of concurrent connections allowed. (default: 8)", "N" },
        { "localcache-repo", 'L', 0, G_OPTION_ARG_FILENAME_ARRAY, &opt_localcache_repos,
          "Add REPO as local cache source for objects during this pull", "REPO" },
        { "timestamp-check", 'T', 0, G_OPTION_ARG_NONE, &opt_timestamp_check,
          "Require fetched commits to have newer timestamps", NULL },
        { "timestamp-check-from-rev", 0, 0, G_OPTION_ARG_STRING, &opt_timestamp_check_from_rev,
          "Require fetched commits to have newer timestamps than given rev", NULL },
        { "disable-verify-bindings", 0, 0, G_OPTION_ARG_NONE, &opt_disable_verify_bindings,
          "Do not verify commit bindings", NULL },
        /* let's leave this hidden for now; we just need it for tests */
        { "append-user-agent", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &opt_append_user_agent,
          "Append string to user agent", NULL },
        { NULL } };

#ifndef OSTREE_DISABLE_GPGME
static void
gpg_verify_result_cb (OstreeRepo *repo, const char *checksum, OstreeGpgVerifyResult *result,
                      GLnxConsoleRef *console)
{
  /* Temporarily place the tty back in normal mode before printing GPG
   * verification results.
   */
  glnx_console_unlock (console);

  g_print ("\n");
  ostree_print_gpg_verify_result (result);

  glnx_console_lock (console);
}
#endif /* OSTREE_DISABLE_GPGME */

static gboolean printed_console_progress;

static void
dry_run_console_progress_changed (OstreeAsyncProgress *progress, gpointer user_data)
{
  guint fetched_delta_parts, total_delta_parts;
  guint fetched_delta_part_fallbacks, total_delta_part_fallbacks;
  guint64 fetched_delta_part_size, total_delta_part_size, total_delta_part_usize;

  g_assert (!printed_console_progress);
  printed_console_progress = TRUE;

  ostree_async_progress_get (
      progress,
      /* Number of parts */
      "fetched-delta-parts", "u", &fetched_delta_parts, "total-delta-parts", "u",
      &total_delta_parts, "fetched-delta-fallbacks", "u", &fetched_delta_part_fallbacks,
      "total-delta-fallbacks", "u", &total_delta_part_fallbacks,
      /* Size variables */
      "fetched-delta-part-size", "t", &fetched_delta_part_size, "total-delta-part-size", "t",
      &total_delta_part_size, "total-delta-part-usize", "t", &total_delta_part_usize, NULL);

  /* Fold the count of deltaparts + fallbacks for simplicity; if changing this,
   * please change ostree_repo_pull_default_console_progress_changed() first.
   */
  fetched_delta_parts += fetched_delta_part_fallbacks;
  total_delta_parts += total_delta_part_fallbacks;

  g_autoptr (GString) buf = g_string_new ("");

  {
    g_autofree char *formatted_fetched = g_format_size (fetched_delta_part_size);
    g_autofree char *formatted_size = g_format_size (total_delta_part_size);
    g_autofree char *formatted_usize = g_format_size (total_delta_part_usize);

    g_string_append_printf (buf, "Delta update: %u/%u parts, %s/%s, %s total uncompressed",
                            fetched_delta_parts, total_delta_parts, formatted_fetched,
                            formatted_size, formatted_usize);
  }
  g_print ("%s\n", buf->str);
}

static void
noninteractive_console_progress_changed (OstreeAsyncProgress *progress, gpointer user_data)
{
  /* We do nothing here - we just want the final status */
}

gboolean
ostree_builtin_pull (int argc, char **argv, OstreeCommandInvocation *invocation,
                     GCancellable *cancellable, GError **error)
{
  g_autoptr (GOptionContext) context = NULL;
  g_autoptr (OstreeRepo) repo = NULL;
  gboolean ret = FALSE;
  g_autofree char *remote = NULL;
  OstreeRepoPullFlags pullflags = 0;
  g_autoptr (GPtrArray) refs_to_fetch = NULL;
  g_autoptr (GPtrArray) override_commit_ids = NULL;
  g_autoptr (OstreeAsyncProgress) progress = NULL;
  gulong signal_handler_id = 0;

  context = g_option_context_new ("REMOTE [BRANCH...]");

  if (!ostree_option_context_parse (context, options, &argc, &argv, invocation, &repo, cancellable,
                                    error))
    goto out;

  if (!ostree_ensure_repo_writable (repo, error))
    goto out;

  if (argc < 2)
    {
      ot_util_usage_error (context, "REMOTE must be specified", error);
      goto out;
    }

  if (opt_disable_fsync)
    ostree_repo_set_disable_fsync (repo, TRUE);

  if (opt_cache_dir)
    {
      if (!ostree_repo_set_cache_dir (repo, AT_FDCWD, opt_cache_dir, cancellable, error))
        goto out;
    }

  if (opt_mirror)
    pullflags |= OSTREE_REPO_PULL_FLAGS_MIRROR;

  if (opt_commit_only)
    pullflags |= OSTREE_REPO_PULL_FLAGS_COMMIT_ONLY;

  if (opt_http_trusted)
    pullflags |= OSTREE_REPO_PULL_FLAGS_TRUSTED_HTTP;
  if (opt_untrusted)
    {
      pullflags |= OSTREE_REPO_PULL_FLAGS_UNTRUSTED;
      /* If the user specifies both, assume they really mean untrusted */
      pullflags &= ~OSTREE_REPO_PULL_FLAGS_TRUSTED_HTTP;
    }
  if (opt_bareuseronly_files)
    pullflags |= OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES;

  if (opt_dry_run && !opt_require_static_deltas)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "--dry-run requires --require-static-deltas");
      goto out;
    }

  if (strchr (argv[1], ':') == NULL)
    {
      remote = g_strdup (argv[1]);
      if (argc > 2)
        {
          int i;
          refs_to_fetch = g_ptr_array_new_with_free_func (g_free);

          for (i = 2; i < argc; i++)
            {
              const char *at = strrchr (argv[i], '@');

              if (at)
                {
                  guint j;
                  const char *override_commit_id = at + 1;

                  if (!ostree_validate_checksum_string (override_commit_id, error))
                    goto out;

                  if (!override_commit_ids)
                    {
                      override_commit_ids = g_ptr_array_new_with_free_func (g_free);

                      /* Backfill */
                      for (j = 2; j < i; j++)
                        g_ptr_array_add (override_commit_ids, g_strdup (""));
                    }

                  g_ptr_array_add (override_commit_ids, g_strdup (override_commit_id));
                  g_ptr_array_add (refs_to_fetch, g_strndup (argv[i], at - argv[i]));
                }
              else
                {
                  g_ptr_array_add (refs_to_fetch, g_strdup (argv[i]));
                  if (override_commit_ids)
                    g_ptr_array_add (override_commit_ids, g_strdup (""));
                }
            }

          g_ptr_array_add (refs_to_fetch, NULL);
        }
    }
  else
    {
      char *ref_to_fetch;
      refs_to_fetch = g_ptr_array_new_with_free_func (g_free);
      if (!ostree_parse_refspec (argv[1], &remote, &ref_to_fetch, error))
        goto out;
      /* Transfer ownership */
      g_ptr_array_add (refs_to_fetch, ref_to_fetch);
      g_ptr_array_add (refs_to_fetch, NULL);
    }

  {
    GVariantBuilder builder;
    g_autoptr (GVariant) pull_options = NULL;
    g_auto (GLnxConsoleRef) console = {
      0,
    };
    g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}"));

    glnx_console_lock (&console);

    if (opt_url)
      g_variant_builder_add (&builder, "{s@v}", "override-url",
                             g_variant_new_variant (g_variant_new_string (opt_url)));
    if (opt_subpaths && opt_subpaths[0] != NULL)
      {
        /* Special case the one-element case so that we excercise this
           old single-argument version in the tests */
        if (opt_subpaths[1] == NULL)
          g_variant_builder_add (&builder, "{s@v}", "subdir",
                                 g_variant_new_variant (g_variant_new_string (opt_subpaths[0])));
        else
          g_variant_builder_add (
              &builder, "{s@v}", "subdirs",
              g_variant_new_variant (g_variant_new_strv ((const char *const *)opt_subpaths, -1)));
      }
    g_variant_builder_add (&builder, "{s@v}", "flags",
                           g_variant_new_variant (g_variant_new_int32 (pullflags)));
    if (refs_to_fetch)
      g_variant_builder_add (&builder, "{s@v}", "refs",
                             g_variant_new_variant (g_variant_new_strv (
                                 (const char *const *)refs_to_fetch->pdata, -1)));
    g_variant_builder_add (&builder, "{s@v}", "depth",
                           g_variant_new_variant (g_variant_new_int32 (opt_depth)));

    g_variant_builder_add (&builder, "{s@v}", "update-frequency",
                           g_variant_new_variant (g_variant_new_uint32 (opt_frequency)));

    if (opt_network_retries >= 0)
      g_variant_builder_add (&builder, "{s@v}", "n-network-retries",
                             g_variant_new_variant (g_variant_new_uint32 (opt_network_retries)));

    if (opt_low_speed_limit_bytes >= 0)
      g_variant_builder_add (
          &builder, "{s@v}", "low-speed-limit-bytes",
          g_variant_new_variant (g_variant_new_uint32 (opt_low_speed_limit_bytes)));

    if (opt_low_speed_time_seconds >= 0)
      g_variant_builder_add (
          &builder, "{s@v}", "low-speed-time-seconds",
          g_variant_new_variant (g_variant_new_uint32 (opt_low_speed_time_seconds)));

    if (opt_max_outstanding_fetcher_requests >= 0)
      g_variant_builder_add (
          &builder, "{s@v}", "max-outstanding-fetcher-requests",
          g_variant_new_variant (g_variant_new_uint32 (opt_max_outstanding_fetcher_requests)));

    if (opt_retry_all)
      g_variant_builder_add (&builder, "{s@v}", "retry-all-network-errors",
                             g_variant_new_variant (g_variant_new_boolean (FALSE)));

    g_variant_builder_add (
        &builder, "{s@v}", "disable-static-deltas",
        g_variant_new_variant (g_variant_new_boolean (opt_disable_static_deltas)));

    g_variant_builder_add (
        &builder, "{s@v}", "require-static-deltas",
        g_variant_new_variant (g_variant_new_boolean (opt_require_static_deltas)));

    g_variant_builder_add (&builder, "{s@v}", "dry-run",
                           g_variant_new_variant (g_variant_new_boolean (opt_dry_run)));
    if (opt_timestamp_check)
      g_variant_builder_add (&builder, "{s@v}", "timestamp-check",
                             g_variant_new_variant (g_variant_new_boolean (opt_timestamp_check)));
    if (opt_timestamp_check_from_rev)
      g_variant_builder_add (
          &builder, "{s@v}", "timestamp-check-from-rev",
          g_variant_new_variant (g_variant_new_string (opt_timestamp_check_from_rev)));

    if (override_commit_ids)
      g_variant_builder_add (
          &builder, "{s@v}", "override-commit-ids",
          g_variant_new_variant (g_variant_new_strv (
              (const char *const *)override_commit_ids->pdata, override_commit_ids->len)));
    if (opt_localcache_repos)
      g_variant_builder_add (&builder, "{s@v}", "localcache-repos",
                             g_variant_new_variant (g_variant_new_strv (
                                 (const char *const *)opt_localcache_repos, -1)));
    if (opt_per_object_fsync)
      g_variant_builder_add (&builder, "{s@v}", "per-object-fsync",
                             g_variant_new_variant (g_variant_new_boolean (TRUE)));
    g_variant_builder_add (
        &builder, "{s@v}", "disable-verify-bindings",
        g_variant_new_variant (g_variant_new_boolean (opt_disable_verify_bindings)));
    if (opt_http_headers)
      {
        GVariantBuilder hdr_builder;
        g_variant_builder_init (&hdr_builder, G_VARIANT_TYPE ("a(ss)"));

        for (char **iter = opt_http_headers; iter && *iter; iter++)
          {
            const char *kv = *iter;
            const char *eq = strchr (kv, '=');
            g_autofree char *key = NULL;
            if (!eq)
              {
                g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Missing '=' in --http-header");
                goto out;
              }
            key = g_strndup (kv, eq - kv);
            g_variant_builder_add (&hdr_builder, "(ss)", key, eq + 1);
          }
        g_variant_builder_add (&builder, "{s@v}", "http-headers",
                               g_variant_new_variant (g_variant_builder_end (&hdr_builder)));
      }

    if (opt_append_user_agent)
      g_variant_builder_add (&builder, "{s@v}", "append-user-agent",
                             g_variant_new_variant (g_variant_new_string (opt_append_user_agent)));

    if (!opt_dry_run)
      {
        if (console.is_tty)
          progress = ostree_async_progress_new_and_connect (
              ostree_repo_pull_default_console_progress_changed, &console);
        else
          progress = ostree_async_progress_new_and_connect (noninteractive_console_progress_changed,
                                                            &console);
      }
    else
      {
        progress = ostree_async_progress_new_and_connect (dry_run_console_progress_changed, NULL);
      }

    if (console.is_tty)
      {
#ifndef OSTREE_DISABLE_GPGME
        signal_handler_id = g_signal_connect (repo, "gpg-verify-result",
                                              G_CALLBACK (gpg_verify_result_cb), &console);
#endif /* OSTREE_DISABLE_GPGME */
      }

    pull_options = g_variant_ref_sink (g_variant_builder_end (&builder));

    if (!ostree_repo_pull_with_options (repo, remote, pull_options, progress, cancellable, error))
      goto out;

    if (!console.is_tty && !opt_dry_run)
      {
        g_assert (progress);
        g_autofree char *status = ostree_async_progress_get_status (progress);
        if (status)
          g_print ("%s\n", status);
      }

    ostree_async_progress_finish (progress);

    if (opt_dry_run)
      g_assert (printed_console_progress);
  }

  ret = TRUE;
out:
  if (signal_handler_id > 0)
    g_signal_handler_disconnect (repo, signal_handler_id);
  return ret;
}
