glmnet_get_lambda = function(self, pv) {
  if (is.null(self$model)) {
    stopf("Learner '%s' has no model stored", self$id)
  }

  pv = pv %??% self$param_set$get_values(tags = "predict")
  s = pv$s

  if (is.character(s)) {
    self$model[[s]]
  } else if (is.numeric(s)) {
    s
  } else {
    # null / missing
    if (inherits(self$model, "cv.glmnet")) {
      self$model[["lambda.1se"]]
    } else if (length(self$model$lambda) == 1L) {
      self$model$lambda
    } else {
      default = self$param_set$default$s
      warningf("Multiple lambdas have been fit. Lambda will be set to %s (see parameter 's').", default)
      default
    }
  }
}


glmnet_feature_names = function(model) {
  beta = model$beta
  if (is.null(beta)) {
    beta = model$glmnet.fit$beta
  }

  rownames(if (is.list(beta)) beta[[1L]] else beta)
}


glmnet_selected_features = function(self, lambda = NULL) {
  if (is.null(self$model)) {
    stopf("No model stored")
  }

  assert_number(lambda, null.ok = TRUE, lower = 0)
  lambda = lambda %??% glmnet_get_lambda(self)
  nonzero = predict(self$model, type = "nonzero", s = lambda)
  nonzero = if (is.data.frame(nonzero)) {
    nonzero[[1L]]
  } else {
    sort(unique(unlist(nonzero, use.names = FALSE)))
  }

  glmnet_feature_names(self$model)[nonzero]
}


glmnet_invoke = function(data, target, pv, cv = FALSE) {
  # glmnet for some reasons sets these parameters globally, therefore
  # we reset them afterwards
  saved_ctrl = glmnet::glmnet.control()
  on.exit(invoke(glmnet::glmnet.control, .args = saved_ctrl))
  glmnet::glmnet.control(factory = TRUE)
  is_ctrl_pars = names(pv) %in% names(saved_ctrl)

  if (any(is_ctrl_pars)) {
    invoke(glmnet::glmnet.control, .args = pv[is_ctrl_pars])
    pv = pv[!is_ctrl_pars]
  }

  # `seed` is an mlr3-only parameter that seeds the fit (e.g. cv.glmnet's random folds)
  # via invoke's `.seed`; it must not be passed on to glmnet itself.
  # We must call glmnet::glmnet / glmnet::cv.glmnet by their literal names so that the
  # call captured by glmnet works when relax.glmnet re-evaluates it.
  seed = pv[["seed"]] %??% NA_integer_
  pv = pv[names(pv) != "seed"]

  if (cv) {
    invoke(glmnet::cv.glmnet, x = data, y = target, .args = pv, .seed = seed)
  } else {
    invoke(glmnet::glmnet, x = data, y = target, .args = pv, .seed = seed)
  }
}

glmnet_set_offset = function(task, phase = "train", pv) {
  assert_choice(phase, c("train", "predict"))

  if ("offset" %nin% task$properties) {
    return(pv)
  }

  use_pred_offset = isTRUE(pv$use_pred_offset)
  is_train = phase == "train"

  # regression
  if (task$task_type == "regr") {
    pv[[if (is_train) "offset" else "newoffset"]] =
      if (is_train || use_pred_offset) task$offset$offset else rep(0, task$nrow)
  }

  # classification
  if (task$task_type == "classif") {
    is_twoclass = length(task$class_names) == 2L
    offset_cols = paste0("offset_", task$class_names)

    if (is_twoclass) {
      pv[[if (is_train) "offset" else "newoffset"]] =
        if (is_train || use_pred_offset) task$offset$offset else rep(0, task$nrow)
    } else {
      pv[[if (is_train) "offset" else "newoffset"]] =
        if (is_train || use_pred_offset) {
          as_numeric_matrix(task$offset)[, offset_cols]
        } else {
          matrix(0, nrow = task$nrow, ncol = length(task$class_names))
        }
    }
  }

  pv
}
