DEFAULT_RSPM_REPO_ID <- "1" # cran
DEFAULT_RSPM <- "https://packagemanager.rstudio.com"

#' Write a Dockerfile for a vetiver model
#'
#' After creating a Plumber file with [vetiver_write_plumber()], use
#' `vetiver_write_docker()` to create a Dockerfile plus a `vetiver_renv.lock`
#' file for a pinned [vetiver_model()].
#'
#' @inheritParams vetiver_api
#' @param plumber_file A path for your Plumber file, created via
#' [vetiver_write_plumber()]. Defaults to `plumber.R` in the working directory.
#' @param path A path to write the Dockerfile and `lockfile`, capturing the
#' model's package dependencies. Defaults to the working directory.
#' @param ... Not currently used.
#' @param lockfile The generated lockfile in `path`. Defaults to
#' `"vetiver_renv.lock"`.
#' @param rspm A logical to use the
#' [RStudio Public Package Manager](https://packagemanager.rstudio.com/) for
#' `renv::restore()` in the Docker image. Defaults to `TRUE`.
#' @param base_image The base Docker image to start with. Defaults to
#' `rocker/r-ver` for the version of R you are working with, but models like
#' keras will require a different base image.
#' @param port The server port for listening: a number such as 8080 or an
#' expression like `'as.numeric(Sys.getenv("PORT"))'` when the port is injected
#' as an environment variable.
#' @param expose Add `EXPOSE` to the Dockerfile? This is helpful for using
#' Docker Desktop but does not work with an expression for `port`.
#' @param additional_pkgs A character vector of additional package names to add
#' to the Docker image. For example, some boards like [pins::board_s3()] require
#' additional software; you can use `required_pkgs(board)` here.
#'
#' @return The content of the Dockerfile, invisibly.
#' @export
#'
#' @examplesIf interactive() || identical(Sys.getenv("IN_PKGDOWN"), "true")
#'
#' library(pins)
#' tmp_plumber <- tempfile()
#' b <- board_temp(versioned = TRUE)
#' cars_lm <- lm(mpg ~ ., data = mtcars)
#' v <- vetiver_model(cars_lm, "cars_linear")
#' vetiver_pin_write(b, v)
#' vetiver_write_plumber(b, "cars_linear", file = tmp_plumber)
#'
#' ## default port
#' vetiver_write_docker(v, tmp_plumber, tempdir())
#' ## install more pkgs, like those required to access board
#' vetiver_write_docker(v, tmp_plumber, tempdir(),
#'                      additional_pkgs = required_pkgs(b))
#' ## port from env variable
#' vetiver_write_docker(v, tmp_plumber, tempdir(),
#'                      port = 'as.numeric(Sys.getenv("PORT"))',
#'                      expose = FALSE)
#'
vetiver_write_docker <- function(
  vetiver_model,
  plumber_file = "plumber.R",
  path = ".",
  ...,
  lockfile = "vetiver_renv.lock",
  rspm = TRUE,
  base_image = glue::glue("FROM rocker/r-ver:{getRversion()}"),
  port = 8000,
  expose = TRUE,
  additional_pkgs = character(0)
) {
  check_dots_empty()

  if (!fs::file_exists(plumber_file)) {
    cli::cli_abort(
      c(
        "{.arg plumber_file} does not exist at {.path {plumber_file}}",
        "i" = "Create your Plumber file with {.fn vetiver_write_plumber}"
      )
    )
  }

  keras <- "keras" %in% vetiver_model$metadata$required_pkgs
  default_image <- base_image == eval(fn_fmls()$base_image)
  if (keras && default_image) {
    cli::cli_warn(c(
      "Your {.arg vetiver_model} object contains a keras model",
      "i" = "Be sure to use an appropriate {.arg base_image} such as `rocker/cuda`"
    ))
  }

  plumber_file <- fs::path_rel(plumber_file)
  withr::local_dir(path)
  rspm_env <- ifelse(
    rspm,
    "ENV RENV_CONFIG_REPOS_OVERRIDE https://packagemanager.rstudio.com/cran/latest\n",
    ""
  )

  pkgs <- c(additional_pkgs, vetiver_model$metadata$required_pkgs)
  pkgs <- vetiver_required_pkgs(pkgs)
  lockfile_pkgs <-
    renv$snapshot(
      lockfile = lockfile,
      packages = pkgs,
      prompt = FALSE,
      force = TRUE
    )

  sys_reqs <- glue_sys_reqs(names(lockfile_pkgs$Packages))
  copy_renv <- glue("COPY {lockfile} renv.lock")
  copy_plumber <- glue("COPY {plumber_file} /opt/ml/plumber.R")
  expose <- ifelse(expose, glue("EXPOSE {port}"), "")
  entrypoint <- glue(
    'ENTRYPOINT ["R", "-e", ',
    '"pr <- plumber::plumb(\'/opt/ml/plumber.R\'); ',
    'pr$run(host = \'0.0.0.0\', port = {port})"]'
  )

  ret <- compact(list(
    "# Generated by the vetiver package; edit with care\n",
    ## https://github.com/rstudio/plumber/blob/main/Dockerfile:
    base_image,
    rspm_env,
    sys_reqs,
    "",
    copy_renv,
    'RUN Rscript -e "install.packages(\'renv\')"',
    'RUN Rscript -e "renv::restore()"',
    copy_plumber,
    expose,
    entrypoint
  ))

  readr::write_lines(ret, file = "Dockerfile")
}

docker_pkgs <- c("pins", "plumber", "rapidoc", "vetiver", "renv")
drop_pkgs <- "stats"

vetiver_required_pkgs <- function(pkgs) {
  pkgs <- c(docker_pkgs, pkgs)
  pkgs <- setdiff(pkgs, drop_pkgs)
  sort(unique(pkgs))
}

glue_sys_reqs <- function(pkgs, call = rlang::caller_env()) {
  rlang::check_installed(c("curl", "jsonlite"))
  rspm <- Sys.getenv("RSPM_ROOT", DEFAULT_RSPM)
  rspm_repo_id <- Sys.getenv("RSPM_REPO_ID", DEFAULT_RSPM_REPO_ID)
  rspm_repo_url <- glue("{rspm}/__api__/repos/{rspm_repo_id}")

  pkgnames <- glue_collapse(pkgs, sep = "&pkgname=")

  req_url <- glue(
    "{rspm_repo_url}/sysreqs?all=false",
    "&pkgname={pkgnames}&distribution=ubuntu&release=20.04"
  )
  res <- curl::curl_fetch_memory(req_url)
  sys_reqs <- jsonlite::fromJSON(rawToChar(res$content), simplifyVector = FALSE)
  if (!is.null(sys_reqs$error)) {
    rlang::abort(sys_reqs$error, call = call)
  }
  sys_reqs <- map(sys_reqs$requirements, pluck, "requirements", "packages")
  sys_reqs <- sort(unique(unlist(sys_reqs)))
  if (is.null(sys_reqs)) {
    return(NULL)
  }
  sys_reqs <- glue_collapse(sys_reqs, sep = " \\\n  ")
  glue(
    "RUN apt-get update -qq && ",
    "apt-get install -y --no-install-recommends \\\n  ",
    sys_reqs,
    " \\\n  && apt-get clean",
    .trim = FALSE
  )
}

#' Generate files necessary to build a Docker container for a vetiver model
#'
#' Deploying a vetiver model via Docker requires several files. Use this
#' function to create these needed files in the directory located at `path`.
#'
#' @inheritParams vetiver_write_plumber
#' @inheritParams vetiver_deploy_rsconnect
#' @param path A path to write the Plumber file, Dockerfile, and lockfile,
#' capturing the model's dependencies.
#' @param docker_args A list of optional arguments passed to
#' [vetiver_write_docker()] such as the `lockfile` name or whether to use
#' `rspm`. Do not pass `additional_pkgs` here, as this function uses
#' `additional_pkgs = required_pkgs(board)`.
#' @details
#' The function `vetiver_prepare_docker()` uses:
#' - [vetiver_write_plumber()] to create a Plumber file and
#' - [vetiver_write_docker()] to create a Dockerfile and renv lockfile
#'
#' These modular functions are available for more advanced use cases. For
#' models such as keras and torch, you will need to edit the generated
#' Dockerfile to, for example, `COPY requirements.txt requirements.txt` or
#' similar.
#'
#' @return An invisible `TRUE`.
#'
#' @examplesIf interactive() || identical(Sys.getenv("IN_PKGDOWN"), "true")
#' library(pins)
#' b <- board_temp(versioned = TRUE)
#' cars_lm <- lm(mpg ~ ., data = mtcars)
#' v <- vetiver_model(cars_lm, "cars_linear")
#' vetiver_pin_write(b, v)
#'
#' vetiver_prepare_docker(b, "cars_linear", path = tempdir())
#'
#' @export
vetiver_prepare_docker <- function(
  board,
  name,
  version = NULL,
  path = ".",
  predict_args = list(),
  docker_args = list()
) {
  withr::local_dir(path)
  if (has_name(docker_args, "additional_pkgs")) {
    abort(c(
      "Do not pass `additional_pkgs` to `docker_args`",
      "This function uses `additional_pkgs = required_pkgs(board)`",
      "For more complex use cases, call `vetiver_write_docker()` itself"
    ))
  }
  vetiver_write_plumber(
    board = board,
    name = name,
    version = version,
    !!!predict_args,
    rsconnect = FALSE
  )
  v <- vetiver_pin_read(board = board, name = name, version = version)
  inject(vetiver_write_docker(
    v,
    !!!docker_args,
    additional_pkgs = required_pkgs(board)
  ))
  invisible(TRUE)
}
