base_pkg_ <- c(
  "base",
  "boot",
  "class",
  "cluster",
  "codetools",
  "compiler",
  "datasets",
  "foreign",
  "graphics",
  "grDevices",
  "grid",
  "KernSmooth",
  "lattice",
  "MASS",
  "Matrix",
  "methods",
  "mgcv",
  "nlme",
  "nnet",
  "parallel",
  "rpart",
  "spatial",
  "splines",
  "stats",
  "stats4",
  "survival",
  "tcltk",
  "tools",
  "utils"
)

quote_not_na <- function(x){
  x[!is.na(x)] <- paste0('\"',x[!is.na(x)],'\"')
  x
}




#' Create a Dockerfile from a DESCRIPTION
#
#' @param path path to the DESCRIPTION file to use as an input.
#' @param FROM The FROM of the Dockerfile. Default is
#'   `paste0("rocker/r-ver:", R.Version()$major, ".", R.Version()$minor)`.
#'   Validated as a Docker image reference (alphanumerics, dot, slash,
#'   dash, underscore, optional `:tag` and / or `@sha256:<hex>`); other
#'   values raise an error to prevent shell-metacharacter injection
#'   into the generated FROM directive.
#' @param AS The build-stage name of the Dockerfile (`FROM ... AS <name>`).
#'   Default is `NULL` (no `AS`). When non-`NULL`, validated as a simple
#'   build-stage name (`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`).
#' @param sysreqs boolean. If TRUE, the Dockerfile will contain sysreq installation.
#' @param repos character. The URL(s) of the repositories to use for
#'   `options("repos")`. Default is `c(CRAN = "https://p3m.dev/cran/latest")`
#'   (Posit Public Package Manager). When `repos` is a single `CRAN`-keyed
#'   PPM URL (`packagemanager.posit.co`, `packagemanager.rstudio.com`, or
#'   `p3m.dev`), the codegen rewrites it to the
#'   `__linux__/$VERSION_CODENAME/` shape (codename resolved from
#'   `/etc/os-release` at image build time) and adds the strict
#'   `HTTPUserAgent` PPM requires, so the build pulls pre-compiled Linux
#'   binaries instead of compiling from source. Pass the legacy
#'   `c(CRAN = "https://cran.rstudio.com/")` to opt out. Each value must
#'   look like an http(s) URL (no quotes, spaces or newlines); each name
#'   (when set) must be a simple identifier (`^[A-Za-z][A-Za-z0-9._-]*$`).
#'   Other values raise an error to prevent injection into the generated
#'   `echo "options(...)"` shell command.
#' @param expand boolean. If `TRUE` each system requirement will have its own `RUN` line.
#' @param build_from_source boolean. If `TRUE` no tar.gz is created and
#'     the Dockerfile directly mount the source folder.
#' @param update_tar_gz boolean. If `TRUE` and `build_from_source` is also `TRUE`,
#'     an updated tar.gz is created.
#' @param extra_sysreqs character vector. Extra debian system requirements.
#'   Will be installed with apt-get install. Each entry must be a Debian
#'   package name (`^[a-z0-9][a-z0-9.+-]+$`); other values raise an error
#'   to prevent injection into the generated apt-get RUN.
#' @param github_pat character. How to provide a GitHub PAT to
#'   `remotes::install_github()` for private dependency repositories.
#'   One of `"none"` (default; the generated Dockerfile does not
#'   reference any PAT), `"build_arg"` (emit `ARG GITHUB_PAT` + `ENV`
#'   propagation; pass with `--build-arg GITHUB_PAT=$GITHUB_PAT`; the
#'   PAT will be visible in the image metadata), or `"secret"`
#'   (BuildKit secret mount on each `install_github()` / `install_local()`
#'   RUN; the PAT is never persisted in the image; requires BuildKit, so
#'   pass with
#'   `DOCKER_BUILDKIT=1 docker build --secret id=github_pat,env=GITHUB_PAT ...`).
#' @param strict_install boolean. When `TRUE` (the default), every
#'   install RUN in the generated Dockerfile is prefixed with
#'   `options(warn = 2);` so that any R warning during install
#'   (missing CRAN package, partial download, archived package,
#'   404 on a remote) becomes a hard error and aborts the docker
#'   build. Set to `FALSE` if your build environment routinely emits
#'   benign warnings (locale defaulting, NTP time-verification,
#'   ABI-version notices) that you do not want to fail the build.
#'   Must be a single scalar logical; `NA`, character, numeric,
#'   `NULL` and length-2+ vectors are rejected with an error.
#'
#' @details
#' Two install strategies are available for the package itself:
#' * `build_from_source = TRUE` (the default): the generated Dockerfile
#'   mounts the source folder and installs from it directly. `update_tar_gz`
#'   is ignored.
#' * `build_from_source = FALSE`: a source tarball (`<pkg>_<version>.tar.gz`)
#'   is `COPY`'d into the image and installed with
#'   `remotes::install_local()`. When `update_tar_gz = TRUE`, a fresh
#'   tarball is built with `pkgbuild::build()` first (and any stale
#'   `<pkg>_*.tar.gz` in the current directory is removed); when
#'   `update_tar_gz = FALSE`, an already-built tarball is expected
#'   alongside the `DESCRIPTION`.
#'
#' The package name and its dependency-field names are read from the
#' `DESCRIPTION` and validated against the CRAN package-name grammar
#' before being interpolated into the generated directives.
#'
#' @export
#' @rdname dockerfiles
#'
#' @examples
#' \dontrun{
#' # From the DESCRIPTION of the package in the working directory:
#' dock <- dock_from_desc("DESCRIPTION")
#' dock
#'
#' # Pull source packages from the classic CRAN mirror instead of PPM:
#' dock_from_desc(
#'   "DESCRIPTION",
#'   repos = c(CRAN = "https://cran.rstudio.com/")
#' )
#' }
#' @importFrom utils installed.packages packageVersion glob2rx
#' @importFrom remotes dev_package_deps
#' @importFrom desc desc_get_deps desc_get
#' @importFrom usethis use_build_ignore
#' @importFrom pkgbuild build
#'
#' @return Dockerfile
dock_from_desc <- function(
  path = "DESCRIPTION",
  FROM = paste0(
    "rocker/r-ver:",
    R.Version()$major,
    ".",
    R.Version()$minor
  ),
  AS = NULL,
  sysreqs = TRUE,
  repos = c(CRAN = "https://p3m.dev/cran/latest"),
  expand = FALSE,
  update_tar_gz = TRUE,
  build_from_source = TRUE,
  extra_sysreqs = NULL,
  github_pat = c("none", "build_arg", "secret"),
  strict_install = TRUE
) {
  github_pat <- match.arg(github_pat)
  .validate_scalar_logical(strict_install, "strict_install")
  .validate_FROM(FROM)
  .validate_AS(AS)
  .validate_repos(repos)
  .validate_extra_sysreqs(extra_sysreqs)
  path <- fs::path_abs(path)
  # Package name read from the (possibly untrusted) DESCRIPTION. It is
  # interpolated into the `COPY <pkg>_*.tar.gz /app.tar.gz` line and the
  # tar.gz-cleanup glob below for `build_from_source = FALSE`; validate
  # it up front so a crafted `Package:` field cannot inject an extra
  # Dockerfile directive.
  pkg_name <- read.dcf(path)[1L, "Package"]
  .validate_pkg_name(pkg_name)

  packages <- desc_get_deps(path)$package
  packages <- packages[packages != "R"] # remove R
  packages <- packages[!packages %in% base_pkg_] # remove base and recommended
  # The dependency-field names are interpolated into the generated
  # `remotes::install_version("<name>", ...)` install RUNs (and queried
  # for system requirements); validate them like the `Package:` field
  # so a crafted Imports / Depends / Suggests / LinkingTo entry cannot
  # inject an extra Dockerfile directive at `docker build` time.
  .validate_pkg_names(packages)

  if (sysreqs) {

    # please wait during system requirement calculation
    cat_bullet(
      "Please wait while we compute system requirements...",
      bullet = "info",
      bullet_col = "green"
    )

    system_requirement <- unique(
      get_sysreqs(
        packages = packages
      )
    )
    cat_green_tick("Done") # TODO animated version ?
  } else {
    system_requirement <- NULL
  }

  sr <- desc::desc_get(
    file = path,
    keys = "SystemRequirements"
  )

  if (length(extra_sysreqs) > 0) {
    system_requirement <- unique(
      c(
        system_requirement,
        extra_sysreqs
      )
    )
  } else if (!is.na(sr)) {
    message(
      paste(
        "The DESCRIPTION file contains the following SystemRequirements: ",
        sr
      )
    )
    message(
      "Please check the created Dockerfile. \n You might needed to add extra sysreqs."
    )
  }

  remotes_deps <- remotes::package_deps(packages)
  packages_on_cran <- intersect(
    remotes_deps$package[remotes_deps$is_cran],
    packages
  )

  packages_not_on_cran <- setdiff(
    packages,
    packages_on_cran
  )

  packages_with_version <- data.frame(
    package = remotes_deps$package,
    installed = remotes_deps$installed,
    stringsAsFactors = FALSE
  )
  packages_with_version <- packages_with_version[
    packages_with_version$package %in% packages_on_cran,
  ]

  packages_on_cran <- set_name(
    packages_with_version$installed,
    packages_with_version$package
  )

  dock <- Dockerfile$new(
    FROM = FROM,
    AS = AS
  )
  .github_pat_setup(dock, github_pat)

  if (length(system_requirement) > 0) {
    if (!expand) {
      dock$RUN(
        paste(
          "apt-get update && apt-get install -y ",
          paste(system_requirement, collapse = " "),
          "&& rm -rf /var/lib/apt/lists/*"
        )
      )
    } else {
      dock$RUN("apt-get update")
      for (sr in system_requirement) {
        dock$RUN(paste("apt-get install -y ", sr))
      }
      dock$RUN("rm -rf /var/lib/apt/lists/*")
    }
  }

  repos_as_character <- repos_as_character(repos)

  dock$RUN("mkdir -p /usr/local/lib/R/etc/ /usr/lib/R/etc/")




  dock$RUN(
    sprintf(
      "echo \"options(repos = %s, download.file.method = 'libcurl', Ncpus = 4)\" | tee /usr/local/lib/R/etc/Rprofile.site | tee /usr/lib/R/etc/Rprofile.site",
      repos_as_character
    )
  )
  # When `repos` is a single CRAN-keyed Posit Package Manager URL,
  # rewrite the Rprofile.site line so the build pulls pre-compiled
  # Linux binaries (the `__linux__/$VERSION_CODENAME/` shape + the
  # strict `HTTPUserAgent` PPM requires). No-op for non-PPM repos.
  .patch_rprofile_for_ppm(dock, repos)




  strict_prefix <- .r_strict_prefix(strict_install)

  dock$RUN(
    sprintf(
      "R -e '%sinstall.packages(\"remotes\")'",
      strict_prefix
    )
  )

  if (length(packages_on_cran) > 0) {
    ping <- mapply(
      function(dock, ver, nm, strict_prefix) {
        res <- dock$RUN(sprintf(
          "Rscript -e '%sremotes::install_version(\"%s\",upgrade=\"never\", version = %s)'",
          strict_prefix,
          nm,
          ver
        ))
      },
      ver = quote_not_na(packages_on_cran),
      nm = names(packages_on_cran),
      MoreArgs = list(dock = dock, strict_prefix = strict_prefix)
    )
  }

  if (length(packages_not_on_cran) > 0) {
    nn <- as.data.frame(
      do.call(
        rbind,
        lapply(
          remotes_deps$remote[!remotes_deps$is_cran],
          function(.) {
            .[c("repo", "username", "sha")]
          }
        )
      )
    )

    nn <- sprintf(
      "%s/%s@%s",
      nn$username,
      nn$repo,
      nn$sha
    )


    pong <- mapply(
      function(dock, ver, strict_prefix) {
        res <- dock$RUN(
          sprintf(
            "%sRscript -e '%sremotes::install_github(\"%s\")'",
            .github_pat_run_prefix(github_pat),
            strict_prefix,
            ver
          )
        )
      },
      ver = nn,
      MoreArgs = list(dock = dock, strict_prefix = strict_prefix)
    )
  }

  if (!build_from_source) {
    if (update_tar_gz) {
      old_version <- list.files(
        # `list.files(pattern =)` is a regex; build it from a glob so a
        # dot in `pkg_name` (allowed by the CRAN package-name grammar,
        # e.g. `R.utils`) is matched literally and a sibling tarball
        # such as `RZutils_*.tar.gz` is not swept up.
        pattern = glob2rx(sprintf("%s_*.tar.gz", pkg_name)),
        full.names = TRUE
      )

      if (length(old_version) > 0) {
        lapply(old_version, file.remove)
        lapply(old_version, unlink, force = TRUE)
        cat_red_bullet(
          sprintf(
            "%s were removed from folder",
            paste(
              old_version,
              collapse = ", "
            )
          )
        )
      }


      if (
        isTRUE(
          requireNamespace(
            "pkgbuild",
            quietly = TRUE
          )
        )
      ) {
        out <- build(
          path = ".",
          dest_path = ".",
          vignettes = FALSE
        )
        use_build_ignore(files = out)
        cat_green_tick(
          sprintf(
            " %s_%s.tar.gz created.",
            pkg_name,
            read.dcf(path)[1, ][["Version"]]
          )
        )
      } else {
        stop("please install {pkgbuild}")
      }
    }
    # we use an already built tar.gz file

    dock$COPY(
      from = paste0(pkg_name, "_*.tar.gz"),
      to = "/app.tar.gz"
    )
    dock$RUN(
      paste0(
        .github_pat_run_prefix(github_pat),
        "R -e '",
        strict_prefix,
        "remotes::install_local(\"/app.tar.gz\",upgrade=\"never\")'"
      )
    )
    dock$RUN("rm /app.tar.gz")
  } else {
    dock$RUN("mkdir /build_zone")
    dock$ADD(from = ".", to = "/build_zone")
    dock$WORKDIR("/build_zone")
    dock$RUN(
      paste0(
        .github_pat_run_prefix(github_pat),
        "R -e '",
        strict_prefix,
        "remotes::install_local(upgrade=\"never\")'"
      )
    )
    dock$RUN("rm -rf /build_zone")
  }
  # Add a dockerignore
  docker_ignore_add(
    path = dirname(path)
  )

  .github_pat_announce(github_pat)
  dock
}

#' @noRd
repos_as_character <- function(repos) {
  repos_as_character <- paste(
    utils::capture.output(
      dput(repos)
    ),
    collapse = ""
  )

  repos_as_character <- gsub(
    pattern = '\"',
    replacement = "'",
    x = repos_as_character
  )

  repos_as_character
}
