#' Bayesian optimization of model parameters.
#'
#' [tune_bayes()] uses models to generate new candidate tuning parameter
#'  combinations based on previous results.
#'
#' @inheritParams tune_grid
#' @param metrics A [yardstick::metric_set()], or `NULL` to compute a standard
#' set of metrics. The first metric in `metrics` is the one that will be optimized.
#' @param iter The maximum number of search iterations.
#' @param objective A character string for what metric should be optimized or
#' an acquisition function object.
#' @param initial An initial set of results in a tidy format (as would result
#' from [tune_grid()]) or a positive integer. It is suggested that the number of
#' initial results be greater than the number of parameters being optimized.
#' @param control A control object created by [control_bayes()].
#' @param ... Not currently used.
#' @return A tibble of results that mirror those generated by [tune_grid()].
#' However, these results contain an `.iter` column and replicate the `rset`
#' object multiple times over iterations (at limited additional memory costs).
#' @seealso [control_bayes()], [tune()], [autoplot.tune_results()],
#'  [show_best()], [select_best()], [collect_predictions()],
#'  [collect_metrics()], [prob_improve()], [exp_improve()], [conf_bound()],
#'  [fit_resamples()]
#' @details
#'
#' The optimization starts with a set of initial results, such as those
#'  generated by [tune_grid()]. If none exist, the function will create several
#'  combinations and obtain their performance estimates.
#'
#' Using one of the performance estimates as the _model outcome_, a Gaussian
#'  process (GP) model is created where the previous tuning parameter combinations
#'  are used as the predictors.
#'
#' A large grid of potential hyperparameter combinations is predicted using
#'  the model and scored using an _acquisition function_. These functions
#'  usually combine the predicted mean and variance of the GP to decide the best
#'  parameter combination to try next. For more information, see the
#'  documentation for [exp_improve()] and the corresponding package vignette.
#'
#' The best combination is evaluated using resampling and the process continues.
#'
#' @section Parallel Processing:
#'
#' tune supports parallel processing with the \pkg{future} package. To execute
#' the resampling iterations in parallel, specify a [plan][future::plan] with
#' future first. The `allow_par` argument can be used to avoid parallelism.
#'
#' For the most part, warnings generated during training are shown as they occur
#' and are associated with a specific resample when
#' `control_bayes(verbose = TRUE)`. They are (usually) not aggregated until the
#' end of processing.
#'
#' For Bayesian optimization, parallel processing is used to estimate the
#' resampled performance values once a new candidate set of values are estimated.
#'
#' @section Initial Values:
#'
#' The results of [tune_grid()], or a previous run of [tune_bayes()] can be used
#' in the `initial` argument. `initial` can also be a positive integer. In this
#' case, a space-filling design will be used to populate a preliminary set of
#' results. For good results, the number of initial values should be more than
#' the number of parameters being optimized.
#'
#' The tuning parameter combinations that were tested are called _candidates_.
#'  Each candidate has a unique `.config` value that, for the initial grid search,
#'  has the pattern `pre{num}_mod{num}_post{num}`. The numbers include a zero
#'  when that element was static. For example, a value of `pre0_mod3_post4` means
#'  no preprocessors were tuned and the model and postprocessor(s) had at least
#'  three and four candidates, respectively. The iterative part of the
#'  search uses the pattern `iter{num}`. In each case, the numbers are
#'  zero-padded to enable proper sorting.
#'
#' @section Parameter Ranges and Values:
#'
#' In some cases, the tuning parameter values depend on the dimensions of the
#' data (they are said to contain [unknown][dials::unknown] values). For
#' example, `mtry` in random forest models depends on the number of predictors.
#' In such cases, the unknowns in the tuning parameter object must be determined
#' beforehand and passed to the function via the `param_info` argument.
#' [dials::finalize()] can be used to derive the data-dependent parameters.
#' Otherwise, a parameter set can be created via [dials::parameters()], and the
#' `dials` `update()` function can be used to specify the ranges or values.
#'
#' @section Performance Metrics:
#'
#' To use your own performance metrics, the [yardstick::metric_set()] function
#'  can be used to pick what should be measured for each model. If multiple
#'  metrics are desired, they can be bundled. For example, to estimate the area
#'  under the ROC curve as well as the sensitivity and specificity (under the
#'  typical probability cutoff of 0.50), the `metrics` argument could be given:
#'
#' \preformatted{
#'   metrics = metric_set(roc_auc, sens, spec)
#' }
#'
#' Each metric is calculated for each candidate model.
#'
#' If no metric set is provided, one is created:
#' \itemize{
#'   \item For regression models, the root mean squared error and coefficient
#'         of determination are computed.
#'   \item For classification, the area under the ROC curve and overall accuracy
#'         are computed.
#'   \item For censored regression, the dynamic Brier score
#'         ([yardstick::brier_survival()]) is used.
#'   \item For quantile regression, the weighted interval score
#'         ([yardstick::weighted_interval_score()]) is used.
#' }
#'
#' Note that the metrics also determine what type of predictions are estimated
#' during tuning. For example, in a classification problem, if metrics are used
#' that are all associated with hard class predictions, the classification
#' probabilities are not created.
#'
#' The out-of-sample estimates of these metrics are contained in a list column
#' called `.metrics`. This tibble contains a row for each metric and columns
#' for the value, the estimator type, and so on.
#'
#' [collect_metrics()] can be used for these objects to collapse the results
#' over the resampled (to obtain the final resampling estimates per tuning
#' parameter combination).
#'
#' @section Obtaining Predictions:
#'
#' When `control_bayes(save_pred = TRUE)`, the output tibble contains a list
#' column called `.predictions` that has the out-of-sample predictions for each
#' parameter combination in the grid and each fold (which can be very large).
#'
#' The elements of the tibble are tibbles with columns for the tuning
#' parameters, the row number from the original data object (`.row`), the
#' outcome data (with the same name(s) of the original data), and any columns
#' created by the predictions. For example, for simple regression problems, this
#' function generates a column called `.pred` and so on. As noted above, the
#' prediction columns that are returned are determined by the type of metric(s)
#' requested.
#'
#' This list column can be `unnested` using [tidyr::unnest()] or using the
#'  convenience function [collect_predictions()].
#'
#' @inheritSection tune_grid Extracting Information
#'
#' @template case-weights
#' @template censored-regression
#'
#' @section Optimizing Censored Regression Models:
#'
#' With dynamic performance metrics (e.g. Brier or ROC curves), performance is
#' calculated for every value of `eval_time` but the _first_ evaluation time
#' given by the user (e.g., `eval_time[1]`) is used to guide the optimization.
#'
#' @examplesIf tune:::should_run_examples(suggests = "kernlab") && !tune:::is_cran_check()
#' library(recipes)
#' library(rsample)
#' library(parsnip)
#'
#' # define resamples and minimal recipe on mtcars
#' set.seed(6735)
#' folds <- vfold_cv(mtcars, v = 5)
#'
#' car_rec <-
#'   recipe(mpg ~ ., data = mtcars) |>
#'   step_normalize(all_predictors())
#'
#' # define an svm with parameters to tune
#' svm_mod <-
#'   svm_rbf(cost = tune(), rbf_sigma = tune()) |>
#'   set_engine("kernlab") |>
#'   set_mode("regression")
#'
#' # use a space-filling design with 6 points
#' set.seed(3254)
#' svm_grid <- tune_grid(svm_mod, car_rec, folds, grid = 6)
#'
#' show_best(svm_grid, metric = "rmse")
#'
#' # use bayesian optimization to evaluate at 6 more points
#' set.seed(8241)
#' svm_bayes <- tune_bayes(svm_mod, car_rec, folds, initial = svm_grid, iter = 6)
#'
#' # note that bayesian optimization evaluated parameterizations
#' # similar to those that previously decreased rmse in svm_grid
#' show_best(svm_bayes, metric = "rmse")
#'
#' # specifying `initial` as a numeric rather than previous tuning results
#' # will result in `tune_bayes` initially evaluating an space-filling
#' # grid using `tune_grid` with `grid = initial`
#' set.seed(0239)
#' svm_init <- tune_bayes(svm_mod, car_rec, folds, initial = 6, iter = 6)
#'
#' show_best(svm_init, metric = "rmse")
#' @export
tune_bayes <- function(object, ...) {
  UseMethod("tune_bayes")
}

#' @export
tune_bayes.default <- function(object, ...) {
  cli::cli_abort(
    "The first argument to {.fn tune_bayes} should be either a model or workflow,
    not {.obj_type_friendly {object}}."
  )
}

#' @export
#' @rdname tune_bayes
tune_bayes.model_spec <- function(
  object,
  preprocessor,
  resamples,
  ...,
  iter = 10,
  param_info = NULL,
  metrics = NULL,
  eval_time = NULL,
  objective = exp_improve(),
  initial = 5,
  control = control_bayes()
) {
  if (rlang::is_missing(preprocessor) || !is_preprocessor(preprocessor)) {
    cli::cli_abort(tune_pp_msg)
  }

  # set `seed` so that calling `control_bayes()` doesn't alter RNG state (#721)
  control <- parsnip::condense_control(control, control_bayes(seed = 1))

  wflow <- add_model(workflow(), object)

  if (is_recipe(preprocessor)) {
    wflow <- add_recipe(wflow, preprocessor)
  } else if (rlang::is_formula(preprocessor)) {
    wflow <- add_formula(wflow, preprocessor)
  }

  tune_bayes_workflow(
    wflow,
    resamples = resamples,
    iter = iter,
    param_info = param_info,
    metrics = metrics,
    eval_time = eval_time,
    objective = objective,
    initial = initial,
    control = control,
    ...
  )
}


#' @export
#' @rdname tune_bayes
tune_bayes.workflow <-
  function(
    object,
    resamples,
    ...,
    iter = 10,
    param_info = NULL,
    metrics = NULL,
    eval_time = NULL,
    objective = exp_improve(),
    initial = 5,
    control = control_bayes()
  ) {
    # set `seed` so that calling `control_bayes()` doesn't alter RNG state (#721)
    control <- parsnip::condense_control(control, control_bayes(seed = 1))

    res <-
      tune_bayes_workflow(
        object,
        resamples = resamples,
        iter = iter,
        param_info = param_info,
        metrics = metrics,
        eval_time = eval_time,
        objective = objective,
        initial = initial,
        control = control,
        ...
      )
    .stash_last_result(res)
    res
  }

## TODOs from testing

# These lines are missing:
# -   i Gaussian process model
# -   ! The Gaussian process model is being fit using 1 features but only has 2
# -     data points to do so. This may cause errors or a poor model fit.
# -   v Gaussian process model
# -   i Generating 3 candidates
# -   i Predicted candidates

# Some failures are not being caught (or ignored); see "missing performance
# values" test
# -   ! Gaussian process model: no non-missing arguments to min; returning Inf, no non-missing arguments...
# -   x Gaussian process model: Error in seq_len(n - 1L): argument must be coercible to non-negative int...

# -   Error in `check_gp_failure()`:
# +   Error in `apply()`:
# -   ! Gaussian process model was not fit.
# +   ! dim(X) must have a positive length

tune_bayes_workflow <- function(
  object,
  resamples,
  iter = 10,
  param_info = NULL,
  metrics = NULL,
  eval_time = NULL,
  objective = exp_improve(),
  initial = 5,
  control,
  ...,
  call = caller_env()
) {
  clear_gp_results()
  start_time <- proc.time()[3]

  initialize_catalog(control = control, workflow = object)

  check_rset(resamples)
  rset_info <- pull_rset_attributes(resamples)

  check_iter(iter, call = call)
  if (iter > 0) {
    iter_chr <- recipes::names0(iter, "iter")
  } else {
    iter_chr <- "iter0"
  }

  metrics <- check_metrics_arg(metrics, object, call = call)
  opt_metric <- first_metric(metrics)
  opt_metric_name <- opt_metric$metric
  maximize <- opt_metric$direction == "maximize"

  eval_time <- check_eval_time_arg(eval_time, metrics, call = call)
  opt_metric_time <- first_eval_time(
    metrics,
    metric = opt_metric_name,
    eval_time = eval_time,
    call = call
  )

  if (is.null(param_info)) {
    param_info <- hardhat::extract_parameter_set_dials(object)
  }
  check_workflow(
    object,
    check_dials = is.null(param_info),
    pset = param_info,
    call = call
  )
  check_backend_options(control$backend_options)

  unsummarized <- check_initial(
    initial,
    pset = param_info,
    wflow = object,
    resamples = resamples,
    metrics = metrics,
    eval_time = eval_time,
    ctrl = control,
    checks = "bayes"
  )

  # Pull outcome names from initialization run
  outcomes <- peek_tune_results_outcomes(unsummarized)

  # Evaluate this portion of the function in a local environment using
  # `(function() expr)()` so that `on.exit()` doesn't touch the exit
  # handlers attached to the execution environment of this function
  # by `initialize_catalog()` (#828, #845).
  (function() {
    # Return whatever we have if there is a error (or execution is stopped)
    on.exit({
      cli::cli_alert_danger(
        "Optimization stopped prematurely; returning current results."
      )

      out <- new_iteration_results(
        x = unsummarized,
        parameters = param_info,
        metrics = metrics,
        eval_time = eval_time,
        eval_time_target = opt_metric_time,
        outcomes = outcomes,
        rset_info = rset_info,
        workflow = NULL
      )

      .stash_last_result(out)

      return(out)
    })

    # Preempt `estimate_tune_results()` error and rely
    # on `on.exit()` condition to return preliminary results
    if (.is_cataclysmic(unsummarized)) {
      return()
    }

    # Get the averaged resampling stats before stripping attributes
    mean_stats <- estimate_tune_results(unsummarized)

    # Strip off `tune_results` class and drop all attributes since
    # we add on an `iteration_results` class later.
    unsummarized <- new_bare_tibble(unsummarized)

    check_time(start_time, control$time_limit)

    score_card <- initial_info(
      mean_stats,
      opt_metric_name,
      maximize,
      opt_metric_time
    )

    if (control$verbose_iter) {
      msg <- paste("Optimizing", opt_metric_name, "using", objective$label)
      if (!is.null(opt_metric_time)) {
        msg <- paste(
          msg,
          "at evaluation time",
          format(opt_metric_time, digits = 3)
        )
      }
      message_wrap(msg)
    }

    prev_gp_mod <- NULL

    for (i in seq_len(iter) + score_card$overall_iter) {
      .notes <- new_note()

      log_best(control, i, score_card)

      check_time(start_time, control$time_limit)

      # Maybe remove .catch_and_log() here and do catching inside function
      set.seed(control$seed[1] + i)

      gp_mod <- fit_gp(
        mean_stats |> dplyr::select(-.iter),
        pset = param_info,
        metric = opt_metric_name,
        eval_time = opt_metric_time,
        control = control,
        ...
      )

      check_time(start_time, control$time_limit)

      set.seed(control$seed[1] + i + 1)
      candidates <-
        pred_gp(
          gp_mod,
          param_info,
          control = control,
          current = mean_stats |> dplyr::select(dplyr::all_of(param_info$id))
        )

      check_time(start_time, control$time_limit)

      acq_summarizer(control, iter = i, objective = objective)

      candidates <-
        dplyr::bind_cols(
          candidates,
          stats::predict(
            objective,
            candidates,
            iter = i,
            maximize = maximize,
            score_card$best_val
          )
        )

      check_time(start_time, control$time_limit)

      # TODO move some messages here
      # check_and_log_flow(control, candidates)

      save_gp_results(
        gp_mod,
        param_info,
        control,
        i,
        iter,
        candidates,
        score_card
      )

      candidates <- pick_candidate(candidates, score_card, control)

      if (score_card$uncertainty >= control$uncertain) {
        score_card$uncertainty <- -1 # is updated in update_score_card() below
      }

      check_time(start_time, control$time_limit)

      param_msg(control, candidates)
      set.seed(control$seed[1] + i + 2)
      tmp_res <-
        more_results(
          object,
          resamples = resamples,
          candidates = candidates,
          metrics = metrics,
          eval_time = eval_time,
          control = control,
          param_info = param_info
        )

      check_time(start_time, control$time_limit)

      all_bad <- .is_cataclysmic(tmp_res)
      if (!inherits(tmp_res, "try-error") & !all_bad) {
        tmp_res[[".metrics"]] <- purrr::map(
          tmp_res[[".metrics"]],
          set_config,
          config = iter_chr[i]
        )
        if (control$save_pred) {
          tmp_res[[".predictions"]] <- purrr::map(
            tmp_res[[".predictions"]],
            set_config,
            config = iter_chr[i]
          )
        }
        if (".extracts" %in% names(tmp_res)) {
          tmp_res[[".extracts"]] <- purrr::map(
            tmp_res[[".extracts"]],
            set_config,
            config = iter_chr[i]
          )
        }
        unsummarized <- dplyr::bind_rows(
          unsummarized,
          tmp_res |> mutate(.iter = i)
        )
        rs_estimate <- estimate_tune_results(tmp_res)
        mean_stats <- dplyr::bind_rows(
          mean_stats,
          rs_estimate |> dplyr::mutate(.iter = i)
        )
        score_card <- update_score_card(score_card, i, tmp_res)
        log_progress(
          control,
          x = mean_stats,
          maximize = maximize,
          objective = opt_metric_name,
          eval_time = opt_metric_time
        )
      } else {
        if (all_bad) {
          update_printer(
            control,
            split_labels = NULL,
            task = "All models failed",
            type = "danger"
          )
        }
        score_card$last_impr <- score_card$last_impr + 1
      }

      if (score_card$last_impr + 1 > control$no_improve) {
        cli::cli_alert_warning(
          "No improvement for {control$no_improve} iterations; returning current results."
        )
        break
      }
      prev_gp_mod <- gp_mod
      check_time(start_time, control$time_limit)
    }

    workflow_output <- .set_workflow(object, control)

    # Reset `on.exit()` hook
    on.exit()

    res <-
      new_iteration_results(
        x = unsummarized,
        parameters = param_info,
        metrics = metrics,
        eval_time = eval_time,
        eval_time_target = opt_metric_time,
        outcomes = outcomes,
        rset_info = rset_info,
        workflow = workflow_output
      )

    .stash_last_result(res)
    res
  })() # end of self calling lambda
}

create_initial_set <- function(param, n = NULL, checks) {
  .check_param_objects(param)
  if (is.null(n)) {
    n <- nrow(param) + 1
  }
  if (any(checks == "bayes")) {
    check_bayes_initial_size(nrow(param), n)
  }
  dials::grid_space_filling(param, size = n)
}

check_iter <- function(iter, call) {
  if (!(is.numeric(iter) && length(iter) == 1L && !is.na(iter) && iter >= 0)) {
    cli::cli_abort(
      "The {.arg iter} argument must be a non-negative integer.",
      call = call
    )
  }

  iter
}

set_config <- function(x, config = NULL, prefix = NULL) {
  if (!is.null(x)) {
    if (!is.null(prefix)) {
      x <- dplyr::mutate(x, .config = paste0(prefix, "_", .config))
    } else {
      x <- dplyr::mutate(x, .config = config)
    }
  }
  x
}

# ------------------------------------------------------------------------------

#' @export
#' @keywords internal
#' @rdname empty_ellipses
#' @param pset A `parameters` object.
#' @param as_matrix A logical for the return type.
encode_set <- function(x, pset, ..., as_matrix = FALSE) {
  rlang::check_dots_empty()
  # change the numeric variables to the transformed scale (if any)
  has_trans <- purrr::map_lgl(pset$object, \(.x) !is.null(.x$trans))
  if (any(has_trans)) {
    idx <- which(has_trans)
    for (i in idx) {
      x[[pset$id[i]]] <-
        dials::value_transform(pset$object[[i]], x[[pset$id[i]]])
    }
  }

  is_quant <- purrr::map_lgl(pset$object, inherits, "quant_param")
  # Convert all data to the [0, 1] scale based on their possible range (not on
  # their observed range)
  if (any(is_quant)) {
    new_vals <- purrr::map2(
      pset$object[is_quant],
      x[, is_quant],
      encode_unit,
      direction = "forward"
    )
    names(new_vals) <- names(x)[is_quant]
    new_vals <- tibble::as_tibble(new_vals)
    x[, is_quant] <- new_vals
  }

  # Ensure that the right levels are used to create dummy variables
  if (any(!is_quant)) {
    for (i in which(!is_quant)) {
      x[[i]] <- factor(x[[i]], levels = pset$object[[i]]$values)
    }
  }

  if (as_matrix) {
    x <- stats::model.matrix(~ . + 0, data = x)
  }
  x
}

update_score_card <- function(info, iter, results, control) {
  current_val <-
    results |>
    estimate_tune_results() |>
    dplyr::filter(.metric == info$metrics)

  if (!is.null(info$eval_time)) {
    current_val <-
      current_val |>
      dplyr::filter(.eval_time == info$eval_time)
  }

  current_val <- current_val$mean

  if (info$max) {
    is_better <- current_val > info$best_val
  } else {
    is_better <- current_val < info$best_val
  }

  if (!is.na(is_better) & is_better) {
    info$last_impr <- 0
    info$best_val <- current_val
    info$best_iter <- iter
    info$uncertainty <- 0
  } else {
    info$last_impr <- info$last_impr + 1
    info$uncertainty <- info$uncertainty + 1
  }
  info
}


# ------------------------------------------------------------------------------

# save opt_metric_name and maximize to simplify!!!!!!!!!!!!!!!
initial_info <- function(stats, metrics, maximize, eval_time) {
  best_res <-
    stats |>
    dplyr::filter(.metric == metrics) |>
    dplyr::filter(!is.na(mean))

  # TODO a lot of slice_min/slice_max can be used now
  if (maximize) {
    best_res <-
      best_res |>
      dplyr::arrange(desc(mean)) |>
      slice(1)
  } else {
    best_res <-
      best_res |>
      dplyr::arrange(mean) |>
      slice(1)
  }
  best_val <- best_res$mean[1]
  best_iter <- best_res$.iter[1]
  last_impr <- 0
  overall_iter <- max(stats$.iter)

  # outputs:

  list(
    best_val = best_val,
    best_iter = best_iter,
    last_impr = last_impr,
    uncertainty = 0,
    overall_iter = overall_iter,
    metrics = metrics,
    max = maximize,
    eval_time = eval_time
  )
}


# ------------------------------------------------------------------------------

more_results <- function(
  object,
  resamples,
  candidates,
  metrics,
  eval_time = NULL,
  control,
  param_info
) {
  update_printer(
    control,
    split_labels = NULL,
    task = "Estimating performance",
    type = "info"
  )

  candidates <- candidates[,
    !(names(candidates) %in% c(".mean", ".sd", "objective"))
  ]
  p_chr <- paste0(
    names(candidates),
    "=",
    format(as.data.frame(candidates), digits = 3)
  )

  tmp_res <-
    try(
      tune_grid(
        object,
        resamples = resamples,
        param_info = param_info,
        grid = candidates,
        metrics = metrics,
        eval_time = eval_time,
        control = control
      ),
      silent = TRUE
    )

  if (inherits(tmp_res, "try-error")) {
    update_printer(
      control,
      split_labels = NULL,
      task = "Couldn't estimate performance",
      type = "danger"
    )
  } else {
    all_bad <- .is_cataclysmic(tmp_res)
    if (all_bad) {
      p_chr <- glue::glue_collapse(
        p_chr,
        width = options()$width - 28,
        sep = ", "
      )
      msg <- paste("All models failed for:", p_chr)
      update_printer(control, split_labels = NULL, task = msg, type = "danger")
      tmp_res <- simpleError(msg)
    } else {
      update_printer(
        control,
        split_labels = NULL,
        task = "Estimating performance",
        type = "success"
      )
    }
  }
  tmp_res
}


#' @export
#' @keywords internal
#' @rdname empty_ellipses
.is_cataclysmic <- function(x) {
  is_err <- purrr::map_lgl(x$.metrics, inherits, c("simpleError", "error"))
  if (any(!is_err)) {
    is_good <- purrr::map_lgl(
      x$.metrics[!is_err],
      \(.x) tibble::is_tibble(.x) && nrow(.x) > 0
    )
    is_err[!is_err] <- !is_good
  }
  all(is_err)
}


#' @export
#' @keywords internal
#' @rdname empty_ellipses
#' @param origin The calculation start time.
#' @param limit The allowable time (in minutes).
check_time <- function(origin, limit) {
  if (is.na(limit)) {
    return(invisible(NULL))
  }
  now_time <- proc.time()[3]
  if (now_time - origin >= limit * 60) {
    cli::cli_abort("The time limit of {limit} minute{?s} has been reached.")
  }
  invisible(NULL)
}

# May be better to completely refactor things to a high-level call then use
# base's setTimeLimit().

## -----------------------------------------------------------------------------

save_gp_results <- function(x, pset, ctrl, i, iter, candidates, score_card) {
  if (!ctrl$save_gp_scoring) {
    return(invisible(NULL))
  }

  nm <- recipes::names0(iter, "gp_candidates_")[i]
  file_name <- glue::glue("{tempdir()}/{nm}.RData")
  gp_fit <- x
  res <- try(
    save(gp_fit, pset, i, candidates, score_card, file = file_name),
    silent = TRUE
  )
  if (inherits(res, "try-error")) {
    cli::cli_warn(
      "Could not save GP results at iteration {i}: {as.character(res))}"
    )
  }
  invisible(res)
}


clear_gp_results <- function() {
  gp_files <-
    list.files(tempdir(), pattern = "gp_candidates_", full.names = TRUE)
  res <- try(unlink(gp_files), silent = TRUE)
  invisible(res)
}
